Skip to content

Commit 1ba6c00

Browse files
authored
Merge pull request #817 from paltalabs/deploy/testnet_cetes
feat: deploy CETES Blend strategy and vault on testnet
2 parents 59912c2 + 22fff84 commit 1ba6c00

8 files changed

Lines changed: 251 additions & 37 deletions

File tree

apps/contracts/README.md

Lines changed: 199 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,44 +73,219 @@ yarn test testnet -h
7373
```
7474
it will show the next message where you can see all the available tests and the specific flags to run them.
7575

76-
## Production deployment
76+
## Deployments
7777

78-
### Factory
79-
Make sure you have compiled the contracts:
80-
```
81-
make build
78+
---
79+
80+
### Configuration files
81+
82+
Before deploying anything, understand the three configuration sources:
83+
84+
#### `configs.json`
85+
86+
Located at `apps/contracts/configs.json`. Contains network-level settings and vault role assignments. Each network entry includes:
87+
88+
| Field | Description |
89+
|---|---|
90+
| `network` | Network identifier: `mainnet`, `testnet`, `standalone` |
91+
| `horizon_rpc_url` | Horizon REST API endpoint |
92+
| `soroban_rpc_url` | Soroban RPC endpoint |
93+
| `soroban_network_passphrase` | Network passphrase for transaction signing |
94+
| `friendbot_url` | (testnet only) Friendbot URL for account funding |
95+
| `defindex_factory_admin` | Public key of the factory admin |
96+
| `defindex_fee` | Protocol fee in basis points (e.g. `2000` = 20%) |
97+
| `defindex_fee_receiver` | Public key that receives protocol fees |
98+
| `vault_fee_receiver` | Public key that receives vault-level fees |
99+
| `vault_manager` | Can call operational functions (e.g. `investVault`) |
100+
| `vault_emergency_manager` | Can pause the vault in emergencies |
101+
| `vault_rebalance_manager` | Can rebalance allocations between strategies |
102+
| `blend_keeper` | Account authorized to call `harvest` on Blend strategies |
103+
| `vault_name` | Display name stored in the vault contract |
104+
| `vault_symbol` | Token symbol for vault shares (e.g. `DFXV`) |
105+
106+
> The deployer's secret key must be set in `.env` as `DEPLOYER_SECRET_KEY`. On testnet/standalone, the admin account is airdropped automatically.
107+
108+
---
109+
110+
#### `public/<network>.contracts.json`
111+
112+
Located at `public/testnet.contracts.json` (root of the monorepo, **not** inside `apps/contracts`). This is the **shared address book** used as input by `deploy_vault.ts`. It is also written to by `yarn publish-addresses` (which copies from `apps/contracts/.soroban/`).
113+
114+
It must contain the addresses of all external dependencies:
115+
116+
```json
117+
{
118+
"ids": {
119+
"soroswap_router": "<address>",
120+
"blend_fixed_xlm_usdc_pool": "<address>",
121+
"blend_pool_usdc": "<address>",
122+
"blend_pool_cetes": "<address>",
123+
"blnd_token": "<address>",
124+
"cetes_token": "<address>",
125+
"soroswap_usdc": "<address>",
126+
"XLM_blend_strategy": "<address after strategy deploy>",
127+
"USDC_blend_strategy": "<address after strategy deploy>",
128+
"CETES_blend_strategy": "<address after strategy deploy>"
129+
}
130+
}
82131
```
83-
and then you can deploy the factory with the following command:
132+
133+
**Strategy addresses** (keys like `<ASSET>_blend_strategy`) must be added manually after running `deploy-blend`. The vault deployment reads from this file to find the strategy address.
134+
135+
---
136+
137+
#### `apps/contracts/public/<network>.contracts.json`
138+
139+
A secondary address book scoped to the contracts workspace. Read by `deploy_blend.ts` and `constants.ts` to load token and pool addresses. Must contain the external dependency addresses (tokens, pools, router). **Must be populated manually**`yarn publish-addresses` writes to the root-level `public/`, not to this file.
140+
141+
---
142+
143+
#### `.soroban/<network>.contracts.json`
144+
145+
Located at `apps/contracts/.soroban/`. This is the **internal address book** written automatically by deployment scripts. It stores every deployed contract address using the key format:
146+
147+
- Strategies: `<asset_symbol>_blend_<name>_<pool_name>_strategy`
148+
e.g. `cetes_blend_regional_starter_pack_rsp_strategy`
149+
- Vaults: `<asset>_paltalabs_vault`
150+
e.g. `cetes_paltalabs_vault`
151+
152+
Do not edit this file manually — it is managed by the scripts.
153+
154+
---
155+
156+
#### `src/strategies/blend_deploy_config.json`
157+
158+
Contains the list of Blend strategies to deploy, organized by network. Each strategy entry:
159+
160+
```json
161+
{
162+
"name": "regional_starter_pack",
163+
"keeper": "<keeper public key>",
164+
"asset": "<token contract address>",
165+
"asset_symbol": "CETES",
166+
"reward_threshold": "40",
167+
"blend_pool_address": "<blend pool contract address>",
168+
"blend_pool_name": "rsp"
169+
}
84170
```
85-
yarn deploy-factory <network>
171+
172+
| Field | Description |
173+
|---|---|
174+
| `name` | Strategy variant name, used in the resulting contract key |
175+
| `keeper` | Public key authorized to call `harvest` |
176+
| `asset` | The underlying token address the strategy manages |
177+
| `asset_symbol` | Symbol used to filter deploys and build contract keys |
178+
| `reward_threshold` | Minimum BLND reward amount that triggers auto-compounding. **Note:** currently hardcoded to `40` in `deploy_blend.ts`; this field is not yet read by the deploy script. |
179+
| `blend_pool_address` | The Blend pool where assets are deposited |
180+
| `blend_pool_name` | Short name for the pool, used in the resulting contract key |
181+
182+
The resulting contract key is: `<asset_symbol>_blend_<name>_<pool_name>_strategy`
183+
184+
> `install_contract: "true"` at the network level controls whether the WASM is re-uploaded. Set to `"false"` to reuse an already-installed WASM hash.
185+
186+
### Deploying a Blend strategy
187+
188+
**1. Configure `blend_deploy_config.json`**
189+
Add or verify the entry for your asset under the target network. Example for CETES on testnet:
190+
191+
```json
192+
{
193+
"name": "regional_starter_pack",
194+
"keeper": "G...",
195+
"asset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
196+
"asset_symbol": "CETES",
197+
"reward_threshold": "40",
198+
"blend_pool_address": "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW",
199+
"blend_pool_name": "rsp"
200+
}
86201
```
87-
Copy the deployed factory address from the output on `.soroban/<network>.contracts.json` and paste it in `public/<network>.contracts.json`
88-
or just run the following command to copy it automatically:
89202

203+
**2. Ensure the deployer has a trustline for the asset**
204+
The deployer account (from `DEPLOYER_SECRET_KEY`) must have a trustline for the asset token and hold at least a small balance (~1001 stroops worth), which is required for the bootstrap deposit made automatically on first deploy.
205+
206+
**3. Run the deploy script**
207+
To deploy all strategies for a network:
208+
```bash
209+
npm run deploy-blend -- <network>
90210
```
91-
yarn publish-addresses <network>
211+
212+
To deploy only a specific asset (avoids re-deploying existing strategies):
213+
```bash
214+
npm run deploy-blend -- <network> <ASSET_SYMBOL>
92215
```
93216

94-
### blend strategies
95-
First you need to complete the following steps:
96-
1. review the `blend_deploy_config.json` file to ensure that the strategies are correctly configured. In this file you can see a list of the strategies to deploy and the parameters for each one.
97-
2. ensure that the addresses in the `<network>.contracts.json` file are correct and match the ones in the `blend_deploy_config.json` file.
98-
3. ensure that the blend_keeper and blend_deployer secret keys are set in the `.env` file. Also, make sure that the `BLEND_KEEPER_SECRET_KEY` and `BLEND_DEPLOYER_SECRET_KEY` has trustlines set with all the assets that will be used in the strategies.
99-
4. run the deploy_blend script to deploy the strategies:
217+
Example:
218+
```bash
219+
npm run deploy-blend -- testnet CETES
100220
```
101-
yarn deploy-blend <network>
221+
222+
The script will:
223+
- Airdrop the admin account (testnet only)
224+
- Install the `blend_strategy` WASM (if `install_contract: "true"`)
225+
- Deploy the strategy contract
226+
- Make an automatic bootstrap deposit to prevent the first-depositor vulnerability
227+
- Save the address to `.soroban/<network>.contracts.json`
228+
229+
**4. Publish the strategy address**
230+
After deploying, copy the new strategy address from `.soroban/<network>.contracts.json` and add it to `public/<network>.contracts.json` (root level) using the key `<ASSET_SYMBOL>_blend_strategy`:
231+
232+
```json
233+
"CETES_blend_strategy": "C..."
102234
```
103-
5. Then, to make it available for the frontend, you need to copy the new deployed strategies from `.soroban/<network>.contracts.json` into the `~/public/<network>.contracts.json` file.
104235

105-
### Deploy vault
106-
Before deploying a vault, ensure that you have the `~/public/<network>.contracts.json` file updated with the latest contract addresses. This file should contain the addresses of all the necesary contracts deployed on the specified network, such as the factory, strategies, and blend addresses.
236+
---
237+
238+
### Deployment instructions
239+
240+
**Prerequisites:**
241+
242+
- Factory deployed and address in `public/<network>.contracts.json`
243+
- Strategy deployed and its address added to `public/<network>.contracts.json` as `<ASSET>_blend_strategy`
244+
- `configs.json` has correct vault roles for the target network
245+
246+
**Run the deploy script:**
247+
```bash
248+
npm run deploy-vault -- <network> <ASSET_SYMBOL>
249+
```
107250

108-
To deploy a vault, you need to run the following command:
251+
Example:
252+
```bash
253+
npm run deploy-vault -- testnet XLM
109254
```
110-
yarn deploy-vault <network> <asset>
255+
256+
The script reads:
257+
258+
- The strategy address from `public/<network>.contracts.json` → key `<ASSET>_blend_strategy`
259+
- The factory address from the same file → key `defindex_factory`
260+
- Vault roles (manager, fee receiver, etc.) from `configs.json`
261+
262+
On success it prints the vault address and saves it to `.soroban/<network>.contracts.json` as `<asset>_paltalabs_vault`.
263+
264+
> After vault deployment, users deposit into the vault and the vault manager calls `investVault` to allocate funds to the strategy. This is a separate operational step.
265+
266+
---
267+
268+
### Full deployment sequence (new asset)
269+
270+
```bash
271+
# 1. Build contracts
272+
make build
273+
274+
# 2. Deploy factory (first time only)
275+
npm run deploy-factory -- testnet
276+
npm run publish-addresses -- testnet
277+
278+
# 3. Add strategy config to blend_deploy_config.json (manual)
279+
280+
# 4. Deploy the strategy
281+
npm run deploy-blend -- testnet CETES
282+
283+
# 5. Add strategy address to public/testnet.contracts.json (manual)
284+
# "CETES_blend_strategy": "<address from .soroban/testnet.contracts.json>"
285+
286+
# 6. Deploy the vault
287+
npm run deploy-vault -- testnet CETES
111288
```
112-
where `<network>` is the network you want to deploy to (e.g., `testnet`, `mainnet`) and `<asset>` is the asset you want to use (e.g., `usdc`, `xlm`).
113-
>[!NOTE] Make sure to double-check that the configuration in the `./configs.json` and the addresses at the public/`<network>`.contracts.json files are correct before deploying.
114289

115290
## Generate Docs
116291
```bash

apps/contracts/configs.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@
5454
"soroban_rpc_url": "https://soroban-testnet.stellar.org/",
5555
"soroban_network_passphrase": "Test SDF Network ; September 2015",
5656
"defindex_factory_admin": "GCZSJFSSR44LSQZLCVCR5XXFIJ4L4NUP3G76IKECNW4PYGSU7IFBLAEK",
57-
"defindex_fee_receiver": "GDOFDSMFRPOYTOLWODK4O6BZTGDJ4GRHLHX5THXN4TIFE2SXASQYFLPJ",
57+
"defindex_fee_receiver": "GCXCHOZB44DKHUJNXRSYEQ3FH2ZHZPJM4O6BPLQVAL3H4OPIGX76JQKU",
5858
"defindex_fee":"2000",
59-
"vault_fee_receiver": "GDOFDSMFRPOYTOLWODK4O6BZTGDJ4GRHLHX5THXN4TIFE2SXASQYFLPJ",
60-
"blend_keeper": "GDOFDSMFRPOYTOLWODK4O6BZTGDJ4GRHLHX5THXN4TIFE2SXASQYFLPJ",
61-
"vault_manager": "GDOFDSMFRPOYTOLWODK4O6BZTGDJ4GRHLHX5THXN4TIFE2SXASQYFLPJ",
62-
"vault_emergency_manager": "GDOFDSMFRPOYTOLWODK4O6BZTGDJ4GRHLHX5THXN4TIFE2SXASQYFLPJ",
63-
"vault_rebalance_manager": "GDOFDSMFRPOYTOLWODK4O6BZTGDJ4GRHLHX5THXN4TIFE2SXASQYFLPJ",
59+
"vault_fee_receiver": "GCXCHOZB44DKHUJNXRSYEQ3FH2ZHZPJM4O6BPLQVAL3H4OPIGX76JQKU",
60+
"blend_keeper": "GCXCHOZB44DKHUJNXRSYEQ3FH2ZHZPJM4O6BPLQVAL3H4OPIGX76JQKU",
61+
"vault_manager": "GCXCHOZB44DKHUJNXRSYEQ3FH2ZHZPJM4O6BPLQVAL3H4OPIGX76JQKU",
62+
"vault_emergency_manager": "GCXCHOZB44DKHUJNXRSYEQ3FH2ZHZPJM4O6BPLQVAL3H4OPIGX76JQKU",
63+
"vault_rebalance_manager": "GCXCHOZB44DKHUJNXRSYEQ3FH2ZHZPJM4O6BPLQVAL3H4OPIGX76JQKU",
6464
"vault_name": "Defindex Vault",
6565
"vault_symbol": "DFXV"
6666
}

apps/contracts/public/testnet.contracts.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"soroswap_xtar": "CCZGLAUBDKJSQK72QOZHVU7CUWKW45OZWYWCLL27AEK74U2OIBK6LXF2",
66
"blend_fixed_xlm_usdc_pool": "CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF",
77
"blend_pool_usdc": "CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU",
8-
"blnd_token": "CB22KRA3YZVCNCQI64JQ5WE7UY2VAV7WFLK6A2JN3HEX56T2EDAFO7QF"
8+
"blnd_token": "CB22KRA3YZVCNCQI64JQ5WE7UY2VAV7WFLK6A2JN3HEX56T2EDAFO7QF",
9+
"blend_pool_cetes": "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW",
10+
"cetes_token": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC"
911
},
1012
"hashes": {
1113
"hodl_strategy": "c74ee193f66937b893eb175152f14eaf50b250aadd3700ee1824750d9b37f97d",

apps/contracts/src/constants.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,14 @@ export const BLEND_USDC_ADDRESS = new Address(otherAddressbook.getContractId("bl
99
export const XTAR_ADDRESS = new Address(otherAddressbook.getContractId("soroswap_xtar"));
1010
export const SOROSWAP_ROUTER = otherAddressbook.getContractId("soroswap_router");
1111
export const BLEND_POOL = otherAddressbook.getContractId("blend_fixed_xlm_usdc_pool");
12-
export const BLEND_TOKEN = otherAddressbook.getContractId("blnd_token");
12+
export const BLEND_TOKEN = otherAddressbook.getContractId("blnd_token");
13+
function tryGetContractId(key: string): string {
14+
try {
15+
return otherAddressbook.getContractId(key);
16+
} catch {
17+
return "";
18+
}
19+
}
20+
21+
export const CETES_BLEND_POOL = tryGetContractId("blend_pool_cetes");
22+
export const CETES_TOKEN = tryGetContractId("cetes_token");

apps/contracts/src/strategies/blend_deploy_config.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@
150150
"reward_threshold": "40",
151151
"blend_pool_address": "CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF",
152152
"blend_pool_name": "fixed_xlm_usdc"
153+
},
154+
{
155+
"name": "regional_starter_pack",
156+
"label": "Regional Starter Pack",
157+
"keeper": "GAZORYCZ2X3UGQXZSSOPSTZWEY44BBUA6E7YAF7GWERI26J6YNZL3HAZ",
158+
"asset": "CC72F57YTPX76HAA64JQOEGHQAPSADQWSY5DWVBR66JINPFDLNCQYHIC",
159+
"asset_symbol": "CETES",
160+
"reward_threshold": "40",
161+
"blend_pool_address": "CAPBMXIQTICKWFPWFDJWMAKBXBPJZUKLNONQH3MLPLLBKQ643CYN5PRW",
162+
"blend_pool_name": "rsp"
153163
}
154164
]
155165
}

apps/contracts/src/strategies/deploy_blend.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,18 @@ export async function deployBlendStrategy(addressBook: AddressBook, asset_symbol
154154
console.log("Not enough balance to deploy the strategy");
155155
return;
156156
}
157-
for (const strategy of blendDeployConfig[network].strategies) {
157+
let strategies = blendDeployConfig[network].strategies;
158+
if (asset_symbol) {
159+
strategies = strategies.filter(
160+
(s: any) => s.asset_symbol.toUpperCase() === asset_symbol.toUpperCase()
161+
);
162+
if (strategies.length === 0) {
163+
console.error(`No strategies found for asset: ${asset_symbol}`);
164+
return;
165+
}
166+
}
167+
168+
for (const strategy of strategies) {
158169
const depositAmount = await calculateDepositAmount(
159170
strategy.blend_pool_address,
160171
strategy.asset,
@@ -180,7 +191,7 @@ export async function deployBlendStrategy(addressBook: AddressBook, asset_symbol
180191
}
181192

182193

183-
for (const strategy of blendDeployConfig[network].strategies) {
194+
for (const strategy of strategies) {
184195
const args = constructBlendStrategyArgs(strategy);
185196

186197
let contractKey = `${strategy.asset_symbol.toLowerCase()}_blend_${strategy.name}_${strategy.blend_pool_name}_strategy`

apps/contracts/src/utils/deploy_tools.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum Strategies {
1414
export enum Assets {
1515
XLM = "XLM",
1616
USDC = "USDC",
17+
CETES = "CETES",
1718
}
1819

1920
export interface StrategyDeployArgs {
@@ -30,7 +31,7 @@ export interface StrategyData {
3031
export function AssetFromString(asset: string, config: EnvConfig, externalAddressBook:AddressBook): Address {
3132
if (!asset || !Object.values(Assets).includes(asset.toUpperCase() as Assets)) {
3233
console.warn("Please provide a valid asset symbol");
33-
console.warn("Allowed assets are: \n - XLM \n - USDC");
34+
console.warn("Allowed assets are: \n - XLM \n - USDC \n - CETES");
3435
throw new Error("Invalid asset symbol");
3536
}
3637
const xlmAddress = new Address(
@@ -46,6 +47,9 @@ export function AssetFromString(asset: string, config: EnvConfig, externalAddres
4647
case Assets.USDC:
4748
assetAddress = new Address(externalAddressBook.getContractId("blend_pool_usdc"));
4849
break;
50+
case Assets.CETES:
51+
assetAddress = new Address(externalAddressBook.getContractId("cetes_token"));
52+
break;
4953
default:
5054
throw new Error("Invalid asset symbol");
5155
}

public/testnet.contracts.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
{
22
"ids": {
3+
"defindex_factory": "CDSCWE4GLNBYYTES2OCYDFQA2LLY4RBIAX6ZI32VSUXD7GO6HRPO4A32",
34
"USDC_blend_strategy": "CALLOM5I7XLQPPOPQMYAHUWW4N7O3JKT42KQ4ASEEVBXDJQNJOALFSUY",
45
"XLM_blend_strategy": "CDVLOSPJPQOTB6ZCWO5VSGTOLGMKTXSFWYTUP572GTPNOWX4F76X3HPM",
5-
"defindex_factory": "CDSCWE4GLNBYYTES2OCYDFQA2LLY4RBIAX6ZI32VSUXD7GO6HRPO4A32",
6+
"CETES_blend_strategy": "CCP4RBDWPRNO2LWO23XFU4BBLGA73J5N3BK7EHRJUHVN33YEMMFB2MBE",
67
"usdc_paltalabs_vault": "CBMVK2JK6NTOT2O4HNQAIQFJY232BHKGLIMXDVQVHIIZKDACXDFZDWHN",
7-
"xlm_paltalabs_vault": "CCLV4H7WTLJQ7ATLHBBQV2WW3OINF3FOY5XZ7VPHZO7NH3D2ZS4GFSF6"
8+
"xlm_paltalabs_vault": "CCLV4H7WTLJQ7ATLHBBQV2WW3OINF3FOY5XZ7VPHZO7NH3D2ZS4GFSF6",
9+
"cetes_paltalabs_vault": "CBIS5TEMTNNOTBE3WXPQUAGUEDYZZVIWAKTXEQCOUJ34OJJ3FJ5NLF2P"
810
},
911
"hashes": {
1012
"blend_strategy": "0b1f49e25e7863f06acbf5d18caf82c9ad4140a46521e209221a08aa8940a6a1",

0 commit comments

Comments
 (0)