Skip to content

Commit

Permalink
feat(cli,sdk): Add rebase yield route support (#4474)
Browse files Browse the repository at this point in the history
### Description
This PR adds support for **Rebase Collateral Vault** and **Synthetic
Rebase** into the SDK and CLI. The SDK enforces a single Rebase
Collateral Vault **must** be deployed with Synthetic Rebase via schema
validation and transformation. The CLI filters the subsequent token list
depending on the selection.

### Related issues
- Fixes #4512

### Backward compatibility
Yes

### Testing
Manual/Unit Tests
- Manually test deployment
- E2E test for deployment and message sending
  • Loading branch information
ltyu authored Oct 16, 2024
1 parent 777ef08 commit b1ff48b
Show file tree
Hide file tree
Showing 21 changed files with 628 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .changeset/plenty-chicken-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---

Add rebasing yield route support into CLI/SDK
4 changes: 3 additions & 1 deletion typescript/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
"devDependencies": {
"@ethersproject/abi": "*",
"@ethersproject/providers": "*",
"@types/chai-as-promised": "^8",
"@types/mocha": "^10.0.1",
"@types/node": "^18.14.5",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"chai": "4.5.0",
"chai": "^4.5.0",
"chai-as-promised": "^8.0.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"mocha": "^10.2.0",
Expand Down
40 changes: 38 additions & 2 deletions typescript/cli/src/config/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ import { createAdvancedIsmConfig } from './ism.js';

const TYPE_DESCRIPTIONS: Record<TokenType, string> = {
[TokenType.synthetic]: 'A new ERC20 with remote transfer functionality',
[TokenType.syntheticRebase]: `A rebasing ERC20 with remote transfer functionality. Must be paired with ${TokenType.collateralVaultRebase}`,
[TokenType.collateral]:
'Extends an existing ERC20 with remote transfer functionality',
[TokenType.native]:
'Extends the native token with remote transfer functionality',
[TokenType.collateralVault]:
'Extends an existing ERC4626 with remote transfer functionality',
'Extends an existing ERC4626 with remote transfer functionality. Yields are manually claimed by owner.',
[TokenType.collateralVaultRebase]:
'Extends an existing ERC4626 with remote transfer functionality. Rebases yields to token holders.',
[TokenType.collateralFiat]:
'Extends an existing FiatToken with remote transfer functionality',
[TokenType.XERC20]:
Expand Down Expand Up @@ -129,6 +132,7 @@ export async function createWarpRouteDeployConfig({
);

const result: WarpRouteDeployConfig = {};
let typeChoices = TYPE_CHOICES;
for (const chain of warpChains) {
logBlue(`${chain}: Configuring warp route...`);

Expand Down Expand Up @@ -167,7 +171,7 @@ export async function createWarpRouteDeployConfig({

const type = await select({
message: `Select ${chain}'s token type`,
choices: TYPE_CHOICES,
choices: typeChoices,
});

// TODO: restore NFT prompting
Expand All @@ -192,6 +196,34 @@ export async function createWarpRouteDeployConfig({
}),
};
break;
case TokenType.syntheticRebase:
result[chain] = {
mailbox,
type,
owner,
isNft,
collateralChainName: '', // This will be derived correctly by zod.parse() below
interchainSecurityModule,
};
typeChoices = restrictChoices([
TokenType.syntheticRebase,
TokenType.collateralVaultRebase,
]);
break;
case TokenType.collateralVaultRebase:
result[chain] = {
mailbox,
type,
owner,
isNft,
interchainSecurityModule,
token: await input({
message: `Enter the ERC-4626 vault address on chain ${chain}`,
}),
};

typeChoices = restrictChoices([TokenType.syntheticRebase]);
break;
case TokenType.collateralVault:
result[chain] = {
mailbox,
Expand Down Expand Up @@ -229,6 +261,10 @@ export async function createWarpRouteDeployConfig({
}
}

function restrictChoices(typeChoices: TokenType[]) {
return TYPE_CHOICES.filter((choice) => typeChoices.includes(choice.name));
}

// Note, this is different than the function above which reads a config
// for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig)
export function readWarpCoreConfig(filePath: string): WarpCoreConfig {
Expand Down
6 changes: 5 additions & 1 deletion typescript/cli/src/send/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { indentYamlOrJson } from '../utils/files.js';
import { stubMerkleTreeConfig } from '../utils/relay.js';
import { runTokenSelectionStep } from '../utils/tokens.js';

export const WarpSendLogs = {
SUCCESS: 'Transfer was self-relayed!',
};

export async function sendTestTransfer({
context,
warpCoreConfig,
Expand Down Expand Up @@ -183,7 +187,7 @@ async function executeDelivery({

log('Attempting self-relay of transfer...');
await relayer.relayMessage(transferTxReceipt, messageIndex, message);
logGreen('Transfer was self-relayed!');
logGreen(WarpSendLogs.SUCCESS);
return;
}

Expand Down
58 changes: 57 additions & 1 deletion typescript/cli/src/tests/commands/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core';
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
TokenRouterConfig,
Expand All @@ -10,7 +11,11 @@ import { getContext } from '../../context/context.js';
import { readYamlOrJson, writeYamlOrJson } from '../../utils/files.js';

import { hyperlaneCoreDeploy } from './core.js';
import { hyperlaneWarpApply, readWarpConfig } from './warp.js';
import {
hyperlaneWarpApply,
hyperlaneWarpSendRelay,
readWarpConfig,
} from './warp.js';

export const TEST_CONFIGS_PATH = './test-configs';
export const REGISTRY_PATH = `${TEST_CONFIGS_PATH}/anvil`;
Expand Down Expand Up @@ -123,3 +128,54 @@ export async function getChainId(chainName: string, key: string) {
const chainMetadata = await registry.getChainMetadata(chainName);
return String(chainMetadata?.chainId);
}

export async function deployToken(privateKey: string, chain: string) {
const { multiProvider } = await getContext({
registryUri: REGISTRY_PATH,
registryOverrideUri: '',
key: privateKey,
});

const token = await new ERC20Test__factory(
multiProvider.getSigner(chain),
).deploy('token', 'token', '100000000000000000000', 18);
await token.deployed();

return token;
}

export async function deploy4626Vault(
privateKey: string,
chain: string,
tokenAddress: string,
) {
const { multiProvider } = await getContext({
registryUri: REGISTRY_PATH,
registryOverrideUri: '',
key: privateKey,
});

const vault = await new ERC4626Test__factory(
multiProvider.getSigner(chain),
).deploy(tokenAddress, 'VAULT', 'VAULT');
await vault.deployed();

return vault;
}

/**
* Performs a round-trip warp relay between two chains using the specified warp core config.
*
* @param chain1 - The first chain to send the warp relay from.
* @param chain2 - The second chain to send the warp relay to and back from.
* @param warpCoreConfigPath - The path to the warp core config file.
* @returns A promise that resolves when the round-trip warp relay is complete.
*/
export async function sendWarpRouteMessageRoundTrip(
chain1: string,
chain2: string,
warpCoreConfigPath: string,
) {
await hyperlaneWarpSendRelay(chain1, chain2, warpCoreConfigPath);
return hyperlaneWarpSendRelay(chain2, chain1, warpCoreConfigPath);
}
21 changes: 21 additions & 0 deletions typescript/cli/src/tests/commands/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ $.verbose = true;
* Deploys the Warp route to the specified chain using the provided config.
*/
export async function hyperlaneWarpDeploy(warpCorePath: string) {
// --overrides is " " to allow local testing to work
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--registry ${REGISTRY_PATH} \
--overrides " " \
--config ${warpCorePath} \
--key ${ANVIL_KEY} \
--verbosity debug \
Expand All @@ -30,6 +32,7 @@ export async function hyperlaneWarpApply(
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp apply \
--registry ${REGISTRY_PATH} \
--overrides " " \
--config ${warpDeployPath} \
--warp ${warpCorePath} \
--key ${ANVIL_KEY} \
Expand All @@ -45,13 +48,31 @@ export async function hyperlaneWarpRead(
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp read \
--registry ${REGISTRY_PATH} \
--overrides " " \
--address ${warpAddress} \
--chain ${chain} \
--key ${ANVIL_KEY} \
--verbosity debug \
--config ${warpDeployPath}`;
}

export async function hyperlaneWarpSendRelay(
origin: string,
destination: string,
warpCorePath: string,
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp send \
--relay \
--registry ${REGISTRY_PATH} \
--overrides " " \
--origin ${origin} \
--destination ${destination} \
--warp ${warpCorePath} \
--key ${ANVIL_KEY} \
--verbosity debug \
--yes`;
}

/**
* Reads the Warp route deployment config to specified output path.
* @param warpCorePath path to warp core
Expand Down
2 changes: 1 addition & 1 deletion typescript/cli/src/tests/warp-apply.e2e-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh
const WARP_CONFIG_PATH_2 = `${TEMP_PATH}/anvil2/warp-route-deployment-anvil2.yaml`;
const WARP_CORE_CONFIG_PATH_2 = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-config.yaml`;

const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while
const TEST_TIMEOUT = 100_000; // Long timeout since these tests can take a while
describe('WarpApply e2e tests', async function () {
let chain2Addresses: ChainAddresses = {};
this.timeout(TEST_TIMEOUT);
Expand Down
114 changes: 114 additions & 0 deletions typescript/cli/src/tests/warp-deploy.e2e-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

import { ChainAddresses } from '@hyperlane-xyz/registry';
import { TokenType, WarpRouteDeployConfig } from '@hyperlane-xyz/sdk';

import { WarpSendLogs } from '../send/transfer.js';
import { writeYamlOrJson } from '../utils/files.js';

import {
ANVIL_KEY,
REGISTRY_PATH,
deploy4626Vault,
deployOrUseExistingCore,
deployToken,
sendWarpRouteMessageRoundTrip,
} from './commands/helpers.js';
import { hyperlaneWarpDeploy, readWarpConfig } from './commands/warp.js';

chai.use(chaiAsPromised);
const expect = chai.expect;
chai.should();

const CHAIN_NAME_2 = 'anvil2';
const CHAIN_NAME_3 = 'anvil3';

const EXAMPLES_PATH = './examples';
const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh

const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`;
const WARP_CONFIG_PATH = `${TEMP_PATH}/warp-route-deployment-deploy.yaml`;
const WARP_CORE_CONFIG_PATH_2_3 = `${REGISTRY_PATH}/deployments/warp_routes/VAULT/anvil2-anvil3-config.yaml`;

const TEST_TIMEOUT = 60_000; // Long timeout since these tests can take a while
describe('WarpDeploy e2e tests', async function () {
let chain2Addresses: ChainAddresses = {};
let token: any;
let vault: any;

this.timeout(TEST_TIMEOUT);

before(async function () {
chain2Addresses = await deployOrUseExistingCore(
CHAIN_NAME_2,
CORE_CONFIG_PATH,
ANVIL_KEY,
);

await deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY);

token = await deployToken(ANVIL_KEY, CHAIN_NAME_2);
vault = await deploy4626Vault(ANVIL_KEY, CHAIN_NAME_2, token.address);
});

it('should only allow rebasing yield route to be deployed with rebasing synthetic', async function () {
const warpConfig: WarpRouteDeployConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateralVaultRebase,
token: vault.address,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
},
[CHAIN_NAME_3]: {
type: TokenType.synthetic,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
},
};

writeYamlOrJson(WARP_CONFIG_PATH, warpConfig);
await hyperlaneWarpDeploy(WARP_CONFIG_PATH).should.be.rejected; // TODO: revisit this to figure out how to parse the error.
});

it(`should be able to bridge between ${TokenType.collateralVaultRebase} and ${TokenType.syntheticRebase}`, async function () {
const warpConfig: WarpRouteDeployConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateralVaultRebase,
token: vault.address,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
},
[CHAIN_NAME_3]: {
type: TokenType.syntheticRebase,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
collateralChainName: CHAIN_NAME_2,
},
};

writeYamlOrJson(WARP_CONFIG_PATH, warpConfig);
await hyperlaneWarpDeploy(WARP_CONFIG_PATH);

// Check collateralRebase
const collateralRebaseConfig = (
await readWarpConfig(
CHAIN_NAME_2,
WARP_CORE_CONFIG_PATH_2_3,
WARP_CONFIG_PATH,
)
)[CHAIN_NAME_2];

expect(collateralRebaseConfig.type).to.equal(
TokenType.collateralVaultRebase,
);

// Try to send a transaction
const { stdout } = await sendWarpRouteMessageRoundTrip(
CHAIN_NAME_2,
CHAIN_NAME_3,
WARP_CORE_CONFIG_PATH_2_3,
);
expect(stdout).to.include(WarpSendLogs.SUCCESS);
});
});
2 changes: 2 additions & 0 deletions typescript/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,11 @@ export {
NativeConfig,
TokenRouterConfigSchema,
WarpRouteDeployConfigSchema,
WarpRouteDeployConfigSchemaErrors,
isCollateralConfig,
isNativeConfig,
isSyntheticConfig,
isSyntheticRebaseConfig,
isTokenMetadata,
} from './token/schemas.js';
export { isCompliant } from './utils/schemas.js';
Expand Down
Loading

0 comments on commit b1ff48b

Please sign in to comment.