feat(cli,sdk): Add rebase yield route support (#4474)

### 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 https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4512

### Backward compatibility
Yes

### Testing
Manual/Unit Tests
- Manually test deployment
- E2E test for deployment and message sending
pull/4696/head
Lee 2 weeks ago committed by GitHub
parent 777ef084a6
commit b1ff48bd1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/plenty-chicken-clean.md
  2. 4
      typescript/cli/package.json
  3. 40
      typescript/cli/src/config/warp.ts
  4. 6
      typescript/cli/src/send/transfer.ts
  5. 58
      typescript/cli/src/tests/commands/helpers.ts
  6. 21
      typescript/cli/src/tests/commands/warp.ts
  7. 2
      typescript/cli/src/tests/warp-apply.e2e-test.ts
  8. 114
      typescript/cli/src/tests/warp-deploy.e2e-test.ts
  9. 2
      typescript/sdk/src/index.ts
  10. 7
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  11. 110
      typescript/sdk/src/token/EvmERC20WarpRouteReader.hardhat-test.ts
  12. 25
      typescript/sdk/src/token/EvmERC20WarpRouteReader.ts
  13. 17
      typescript/sdk/src/token/Token.test.ts
  14. 20
      typescript/sdk/src/token/Token.ts
  15. 8
      typescript/sdk/src/token/TokenStandard.ts
  16. 3
      typescript/sdk/src/token/config.ts
  17. 10
      typescript/sdk/src/token/contracts.ts
  18. 8
      typescript/sdk/src/token/deploy.ts
  19. 116
      typescript/sdk/src/token/schemas.test.ts
  20. 70
      typescript/sdk/src/token/schemas.ts
  21. 33
      yarn.lock

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add rebasing yield route support into CLI/SDK

@ -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",

@ -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]:
@ -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...`);
@ -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
@ -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,
@ -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 {

@ -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,
@ -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;
}

@ -1,3 +1,4 @@
import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core';
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
TokenRouterConfig,
@ -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`;
@ -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);
}

@ -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 \
@ -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} \
@ -45,6 +48,7 @@ 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} \
@ -52,6 +56,23 @@ export async function hyperlaneWarpRead(
--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

@ -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);

@ -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);
});
});

@ -512,9 +512,11 @@ export {
NativeConfig,
TokenRouterConfigSchema,
WarpRouteDeployConfigSchema,
WarpRouteDeployConfigSchemaErrors,
isCollateralConfig,
isNativeConfig,
isSyntheticConfig,
isSyntheticRebaseConfig,
isTokenMetadata,
} from './token/schemas.js';
export { isCompliant } from './utils/schemas.js';

@ -111,7 +111,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
});
it('should create with a collateral config', async () => {
const config = {
const config: TokenRouterConfig = {
...baseConfig,
type: TokenType.collateral,
token: token.address,
@ -139,7 +139,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
TOKEN_NAME,
TOKEN_NAME,
);
const config = {
const config: TokenRouterConfig = {
type: TokenType.collateralVault,
token: vault.address,
hook: hookAddress,
@ -172,9 +172,8 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
});
it('should create with a synthetic config', async () => {
const config = {
const config: TokenRouterConfig = {
type: TokenType.synthetic,
token: token.address,
hook: hookAddress,
name: TOKEN_NAME,
symbol: TOKEN_NAME,

@ -14,14 +14,16 @@ import {
HyperlaneContractsMap,
RouterConfig,
TestChainName,
TokenRouterConfig,
WarpRouteDeployConfig,
test3,
} from '@hyperlane-xyz/sdk';
import { assert } from '@hyperlane-xyz/utils';
import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainMap } from '../types.js';
@ -122,12 +124,12 @@ describe('ERC20WarpRouterReader', async () => {
it('should derive collateral config correctly', async () => {
// Create config
const config = {
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.collateral,
token: token.address,
hook: await mailbox.defaultHook(),
interchainsecurityModule: await mailbox.defaultIsm(),
interchainSecurityModule: await mailbox.defaultIsm(),
...baseConfig,
},
};
@ -150,8 +152,10 @@ describe('ERC20WarpRouterReader', async () => {
config[chain].hook as string,
),
);
// Check ism. should return undefined
expect(derivedConfig.interchainSecurityModule).to.be.undefined;
// Check ism
expect(
(derivedConfig.interchainSecurityModule as DerivedIsmConfig).address,
).to.be.equal(await mailbox.defaultIsm());
// Check if token values matches
if (derivedConfig.type === TokenType.collateral) {
@ -160,13 +164,45 @@ describe('ERC20WarpRouterReader', async () => {
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS);
}
});
it('should derive synthetic rebase config correctly', async () => {
// Create config
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.syntheticRebase,
collateralChainName: TestChainName.test4,
hook: await mailbox.defaultHook(),
name: TOKEN_NAME,
symbol: TOKEN_NAME,
decimals: TOKEN_DECIMALS,
totalSupply: TOKEN_SUPPLY,
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].syntheticRebase.address,
);
for (const [key, value] of Object.entries(derivedConfig)) {
const deployedValue = (config[chain] as any)[key];
if (deployedValue && typeof value === 'string')
expect(deployedValue).to.equal(value);
}
// Check if token values matches
if (derivedConfig.type === TokenType.collateral) {
expect(derivedConfig.name).to.equal(TOKEN_NAME);
expect(derivedConfig.symbol).to.equal(TOKEN_NAME);
}
});
it('should derive synthetic config correctly', async () => {
// Create config
const config = {
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.synthetic,
token: token.address,
hook: await mailbox.defaultHook(),
name: TOKEN_NAME,
symbol: TOKEN_NAME,
@ -197,13 +233,13 @@ describe('ERC20WarpRouterReader', async () => {
it('should derive native config correctly', async () => {
// Create config
const config = {
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.native,
hook: await mailbox.defaultHook(),
...baseConfig,
},
} as ChainMap<TokenRouterConfig>;
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
@ -221,9 +257,61 @@ describe('ERC20WarpRouterReader', async () => {
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS);
});
it('should derive collateral vault config correctly', async () => {
// Create config
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.collateralVault,
token: vault.address,
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].collateralVault.address,
);
assert(
derivedConfig.type === TokenType.collateralVault,
'Must be collateralVault',
);
expect(derivedConfig.type).to.equal(config[chain].type);
expect(derivedConfig.mailbox).to.equal(config[chain].mailbox);
expect(derivedConfig.owner).to.equal(config[chain].owner);
expect(derivedConfig.token).to.equal(token.address);
});
it('should derive rebase collateral vault config correctly', async () => {
// Create config
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.collateralVaultRebase,
token: vault.address,
...baseConfig,
},
};
// Deploy with config
const warpRoute = await deployer.deploy(config);
// Derive config and check if each value matches
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
warpRoute[chain].collateralVaultRebase.address,
);
assert(
derivedConfig.type === TokenType.collateralVaultRebase,
'Must be collateralVaultRebase',
);
expect(derivedConfig.type).to.equal(config[chain].type);
expect(derivedConfig.mailbox).to.equal(config[chain].mailbox);
expect(derivedConfig.owner).to.equal(config[chain].owner);
expect(derivedConfig.token).to.equal(token.address);
});
it('should return undefined if ism is not set onchain', async () => {
// Create config
const config = {
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.collateral,
token: token.address,
@ -246,7 +334,7 @@ describe('ERC20WarpRouterReader', async () => {
// Create config
const otherChain = TestChainName.test3;
const otherChainMetadata = test3;
const config = {
const config: WarpRouteDeployConfig = {
[chain]: {
type: TokenType.collateral,
token: token.address,

@ -4,6 +4,8 @@ import {
HypERC20Collateral__factory,
HypERC20__factory,
HypERC4626Collateral__factory,
HypERC4626OwnerCollateral__factory,
HypERC4626__factory,
TokenRouter__factory,
} from '@hyperlane-xyz/core';
import {
@ -81,15 +83,23 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader {
const contractTypes: Partial<
Record<TokenType, { factory: any; method: string }>
> = {
collateralVault: {
[TokenType.collateralVaultRebase]: {
factory: HypERC4626Collateral__factory,
method: 'NULL_RECIPIENT',
},
[TokenType.collateralVault]: {
factory: HypERC4626OwnerCollateral__factory,
method: 'vault',
},
collateral: {
[TokenType.collateral]: {
factory: HypERC20Collateral__factory,
method: 'wrappedToken',
},
synthetic: {
[TokenType.syntheticRebase]: {
factory: HypERC4626__factory,
method: 'collateralDomain',
},
[TokenType.synthetic]: {
factory: HypERC20__factory,
method: 'decimals',
},
@ -106,11 +116,11 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader {
try {
const warpRoute = factory.connect(warpRouteAddress, this.provider);
await warpRoute[method]();
this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger
return tokenType as TokenType;
} catch (e) {
continue;
} finally {
this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger
}
}
@ -186,7 +196,10 @@ export class EvmERC20WarpRouteReader extends HyperlaneReader {
await this.fetchERC20Metadata(token);
return { name, symbol, decimals, totalSupply, token };
} else if (type === TokenType.synthetic) {
} else if (
type === TokenType.synthetic ||
type === TokenType.syntheticRebase
) {
return this.fetchERC20Metadata(tokenAddress);
} else if (type === TokenType.native) {
const chainMetadata = this.multiProvider.getChainMetadata(this.chain);

@ -48,6 +48,15 @@ const STANDARD_TO_TOKEN: Record<TokenStandard, TokenArgs | null> = {
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypRebaseCollateral]: {
chainName: TestChainName.test3,
standard: TokenStandard.EvmHypRebaseCollateral,
addressOrDenom: '0x31b5234A896FbC4b3e2F7237592D054716762131',
collateralAddressOrDenom: '0x64544969ed7ebf5f083679233325356ebe738930',
decimals: 18,
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypOwnerCollateral]: {
chainName: TestChainName.test3,
standard: TokenStandard.EvmHypOwnerCollateral,
@ -74,6 +83,14 @@ const STANDARD_TO_TOKEN: Record<TokenStandard, TokenArgs | null> = {
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypSyntheticRebase]: {
chainName: TestChainName.test2,
standard: TokenStandard.EvmHypSyntheticRebase,
addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147',
decimals: 6,
symbol: 'USDC',
name: 'USDC',
},
[TokenStandard.EvmHypXERC20]: {
chainName: TestChainName.test2,
standard: TokenStandard.EvmHypXERC20,

@ -189,19 +189,19 @@ export class Token implements IToken {
return new EvmHypNativeAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (standard === TokenStandard.EvmHypCollateral) {
return new EvmHypCollateralAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (standard === TokenStandard.EvmHypCollateralFiat) {
return new EvmHypCollateralAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (standard === TokenStandard.EvmHypOwnerCollateral) {
} else if (
standard === TokenStandard.EvmHypCollateral ||
standard === TokenStandard.EvmHypCollateralFiat ||
standard === TokenStandard.EvmHypOwnerCollateral ||
standard === TokenStandard.EvmHypRebaseCollateral
) {
return new EvmHypCollateralAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (standard === TokenStandard.EvmHypSynthetic) {
} else if (
standard === TokenStandard.EvmHypSynthetic ||
standard === TokenStandard.EvmHypSyntheticRebase
) {
return new EvmHypSyntheticAdapter(chainName, multiProvider, {
token: addressOrDenom,
});

@ -15,8 +15,10 @@ export enum TokenStandard {
EvmHypNative = 'EvmHypNative',
EvmHypCollateral = 'EvmHypCollateral',
EvmHypOwnerCollateral = 'EvmHypOwnerCollateral',
EvmHypRebaseCollateral = 'EvmHypRebaseCollateral',
EvmHypCollateralFiat = 'EvmHypCollateralFiat',
EvmHypSynthetic = 'EvmHypSynthetic',
EvmHypSyntheticRebase = 'EvmHypSyntheticRebase',
EvmHypXERC20 = 'EvmHypXERC20',
EvmHypXERC20Lockbox = 'EvmHypXERC20Lockbox',
@ -52,8 +54,10 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record<TokenStandard, ProtocolType> = {
EvmHypNative: ProtocolType.Ethereum,
EvmHypCollateral: ProtocolType.Ethereum,
EvmHypOwnerCollateral: ProtocolType.Ethereum,
EvmHypRebaseCollateral: ProtocolType.Ethereum,
EvmHypCollateralFiat: ProtocolType.Ethereum,
EvmHypSynthetic: ProtocolType.Ethereum,
EvmHypSyntheticRebase: ProtocolType.Ethereum,
EvmHypXERC20: ProtocolType.Ethereum,
EvmHypXERC20Lockbox: ProtocolType.Ethereum,
@ -114,7 +118,9 @@ export const TOKEN_HYP_STANDARDS = [
TokenStandard.EvmHypCollateral,
TokenStandard.EvmHypCollateralFiat,
TokenStandard.EvmHypOwnerCollateral,
TokenStandard.EvmHypRebaseCollateral,
TokenStandard.EvmHypSynthetic,
TokenStandard.EvmHypSyntheticRebase,
TokenStandard.EvmHypXERC20,
TokenStandard.EvmHypXERC20Lockbox,
TokenStandard.SealevelHypNative,
@ -148,9 +154,11 @@ export const TOKEN_TYPE_TO_STANDARD: Record<TokenType, TokenStandard> = {
[TokenType.XERC20]: TokenStandard.EvmHypXERC20,
[TokenType.XERC20Lockbox]: TokenStandard.EvmHypXERC20Lockbox,
[TokenType.collateralVault]: TokenStandard.EvmHypOwnerCollateral,
[TokenType.collateralVaultRebase]: TokenStandard.EvmHypRebaseCollateral,
[TokenType.collateralUri]: TokenStandard.EvmHypCollateral,
[TokenType.fastCollateral]: TokenStandard.EvmHypCollateral,
[TokenType.synthetic]: TokenStandard.EvmHypSynthetic,
[TokenType.syntheticRebase]: TokenStandard.EvmHypSyntheticRebase,
[TokenType.syntheticUri]: TokenStandard.EvmHypSynthetic,
[TokenType.fastSynthetic]: TokenStandard.EvmHypSynthetic,
[TokenType.nativeScaled]: TokenStandard.EvmHypNative,

@ -1,9 +1,11 @@
export enum TokenType {
synthetic = 'synthetic',
syntheticRebase = 'syntheticRebase',
fastSynthetic = 'fastSynthetic',
syntheticUri = 'syntheticUri',
collateral = 'collateral',
collateralVault = 'collateralVault',
collateralVaultRebase = 'collateralVaultRebase',
XERC20 = 'xERC20',
XERC20Lockbox = 'xERC20Lockbox',
collateralFiat = 'collateralFiat',
@ -16,6 +18,7 @@ export enum TokenType {
export const CollateralExtensions = [
TokenType.collateral,
TokenType.collateralVault,
TokenType.collateralVaultRebase,
];
export const gasOverhead = (tokenType: TokenType): number => {

@ -8,6 +8,8 @@ import {
HypERC721URIStorage__factory,
HypERC721__factory,
HypERC4626Collateral__factory,
HypERC4626OwnerCollateral__factory,
HypERC4626__factory,
HypFiatToken__factory,
HypNativeScaled__factory,
HypNative__factory,
@ -21,11 +23,13 @@ export const hypERC20contracts = {
[TokenType.fastCollateral]: 'FastHypERC20Collateral',
[TokenType.fastSynthetic]: 'FastHypERC20',
[TokenType.synthetic]: 'HypERC20',
[TokenType.syntheticRebase]: 'HypERC4626',
[TokenType.collateral]: 'HypERC20Collateral',
[TokenType.collateralFiat]: 'HypFiatToken',
[TokenType.XERC20]: 'HypXERC20',
[TokenType.XERC20Lockbox]: 'HypXERC20Lockbox',
[TokenType.collateralVault]: 'HypERC20CollateralVaultDeposit',
[TokenType.collateralVault]: 'HypERC4626OwnerCollateral',
[TokenType.collateralVaultRebase]: 'HypERC4626Collateral',
[TokenType.native]: 'HypNative',
[TokenType.nativeScaled]: 'HypNativeScaled',
};
@ -36,7 +40,9 @@ export const hypERC20factories = {
[TokenType.fastSynthetic]: new FastHypERC20__factory(),
[TokenType.synthetic]: new HypERC20__factory(),
[TokenType.collateral]: new HypERC20Collateral__factory(),
[TokenType.collateralVault]: new HypERC4626Collateral__factory(),
[TokenType.collateralVault]: new HypERC4626OwnerCollateral__factory(),
[TokenType.collateralVaultRebase]: new HypERC4626Collateral__factory(),
[TokenType.syntheticRebase]: new HypERC4626__factory(),
[TokenType.collateralFiat]: new HypFiatToken__factory(),
[TokenType.XERC20]: new HypXERC20__factory(),
[TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(),

@ -33,6 +33,7 @@ import {
isCollateralConfig,
isNativeConfig,
isSyntheticConfig,
isSyntheticRebaseConfig,
isTokenMetadata,
} from './schemas.js';
import { TokenMetadata, WarpRouteDeployConfig } from './types.js';
@ -64,6 +65,11 @@ abstract class TokenDeployer<
} else if (isSyntheticConfig(config)) {
assert(config.decimals, 'decimals is undefined for config'); // decimals must be defined by this point
return [config.decimals, config.mailbox];
} else if (isSyntheticRebaseConfig(config)) {
const collateralDomain = this.multiProvider.getDomainId(
config.collateralChainName,
);
return [config.decimals, config.mailbox, collateralDomain];
} else {
throw new Error('Unknown token type when constructing arguments');
}
@ -82,7 +88,7 @@ abstract class TokenDeployer<
];
if (isCollateralConfig(config) || isNativeConfig(config)) {
return defaultArgs;
} else if (isSyntheticConfig(config)) {
} else if (isSyntheticConfig(config) || isSyntheticRebaseConfig(config)) {
return [config.totalSupply, config.name, config.symbol, ...defaultArgs];
} else {
throw new Error('Unknown collateral type when initializing arguments');

@ -1,8 +1,15 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
import { assert } from '@hyperlane-xyz/utils';
import { TokenType } from './config.js';
import { WarpRouteDeployConfigSchema } from './schemas.js';
import {
WarpRouteDeployConfigSchema,
WarpRouteDeployConfigSchemaErrors,
isCollateralConfig,
} from './schemas.js';
import { WarpRouteDeployConfig } from './types.js';
const SOME_ADDRESS = ethers.Wallet.createRandom().address;
const COLLATERAL_TYPES = [
@ -19,7 +26,7 @@ const NON_COLLATERAL_TYPES = [
];
describe('WarpRouteDeployConfigSchema refine', () => {
let config: any;
let config: WarpRouteDeployConfig;
beforeEach(() => {
config = {
arbitrum: {
@ -33,18 +40,24 @@ describe('WarpRouteDeployConfigSchema refine', () => {
it('should require token type', () => {
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true;
//@ts-ignore
delete config.arbitrum.type;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false;
});
it('should require token address', () => {
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true;
//@ts-ignore
delete config.arbitrum.token;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false;
});
it('should require mailbox address', () => {
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true;
//@ts-ignore
delete config.arbitrum.mailbox;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false;
});
@ -52,6 +65,9 @@ describe('WarpRouteDeployConfigSchema refine', () => {
it('should throw if collateral type and token is empty', async () => {
for (const type of COLLATERAL_TYPES) {
config.arbitrum.type = type;
assert(isCollateralConfig(config.arbitrum), 'must be collateral');
//@ts-ignore
config.arbitrum.token = undefined;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false;
@ -61,13 +77,8 @@ describe('WarpRouteDeployConfigSchema refine', () => {
}
});
it('should accept native type if token is empty', async () => {
config.arbitrum.type = TokenType.native;
config.arbitrum.token = undefined;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true;
});
it('should succeed if non-collateral type, token is empty, metadata is defined', async () => {
//@ts-ignore
delete config.arbitrum.token;
config.arbitrum.totalSupply = '0';
config.arbitrum.name = 'name';
@ -81,4 +92,93 @@ describe('WarpRouteDeployConfigSchema refine', () => {
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true;
}
});
it(`should throw if deploying rebasing collateral with anything other than ${TokenType.syntheticRebase}`, async () => {
config = {
arbitrum: {
type: TokenType.collateralVaultRebase,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
ethereum: {
type: TokenType.collateralVault,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
optimism: {
type: TokenType.syntheticRebase,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
collateralChainName: '',
},
};
let parseResults = WarpRouteDeployConfigSchema.safeParse(config);
assert(!parseResults.success, 'must be false'); // Needed so 'message' shows up because parseResults is a discriminate union
expect(parseResults.error.issues[0].message).to.equal(
WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE,
);
config.ethereum.type = TokenType.syntheticRebase;
//@ts-ignore
config.ethereum.collateralChainName = '';
parseResults = WarpRouteDeployConfigSchema.safeParse(config);
//@ts-ignore
expect(parseResults.success).to.be.true;
});
it(`should throw if deploying only ${TokenType.collateralVaultRebase}`, async () => {
config = {
arbitrum: {
type: TokenType.collateralVaultRebase,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
};
let parseResults = WarpRouteDeployConfigSchema.safeParse(config);
expect(parseResults.success).to.be.false;
config.ethereum = {
type: TokenType.collateralVaultRebase,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
};
parseResults = WarpRouteDeployConfigSchema.safeParse(config);
expect(parseResults.success).to.be.false;
});
it(`should derive the collateral chain name for ${TokenType.syntheticRebase}`, async () => {
config = {
arbitrum: {
type: TokenType.collateralVaultRebase,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
ethereum: {
type: TokenType.syntheticRebase,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
collateralChainName: '',
},
optimism: {
type: TokenType.syntheticRebase,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
collateralChainName: '',
},
};
const parseResults = WarpRouteDeployConfigSchema.safeParse(config);
assert(parseResults.success, 'must be true');
const warpConfig: WarpRouteDeployConfig = parseResults.data;
assert(
warpConfig.optimism.type === TokenType.syntheticRebase,
'must be syntheticRebase',
);
expect(warpConfig.optimism.collateralChainName).to.equal('arbitrum');
});
});

@ -1,10 +1,16 @@
import { z } from 'zod';
import { objMap } from '@hyperlane-xyz/utils';
import { GasRouterConfigSchema } from '../router/schemas.js';
import { isCompliant } from '../utils/schemas.js';
import { TokenType } from './config.js';
export const WarpRouteDeployConfigSchemaErrors = {
ONLY_SYNTHETIC_REBASE: `Config with ${TokenType.collateralVaultRebase} must be deployed with ${TokenType.syntheticRebase}`,
NO_SYNTHETIC_ONLY: `Config must include Native or Collateral OR all synthetics must define token metadata`,
};
export const TokenMetadataSchema = z.object({
name: z.string(),
symbol: z.string(),
@ -18,6 +24,7 @@ export const CollateralConfigSchema = TokenMetadataSchema.partial().extend({
type: z.enum([
TokenType.collateral,
TokenType.collateralVault,
TokenType.collateralVaultRebase,
TokenType.XERC20,
TokenType.XERC20Lockbox,
TokenType.collateralFiat,
@ -33,6 +40,18 @@ export const NativeConfigSchema = TokenMetadataSchema.partial().extend({
type: z.enum([TokenType.native, TokenType.nativeScaled]),
});
export const CollateralRebaseConfigSchema =
TokenMetadataSchema.partial().extend({
type: z.literal(TokenType.collateralVaultRebase),
});
export const SyntheticRebaseConfigSchema = TokenMetadataSchema.partial().extend(
{
type: z.literal(TokenType.syntheticRebase),
collateralChainName: z.string(),
},
);
export const SyntheticConfigSchema = TokenMetadataSchema.partial().extend({
type: z.enum([
TokenType.synthetic,
@ -50,6 +69,7 @@ export const TokenConfigSchema = z.discriminatedUnion('type', [
NativeConfigSchema,
CollateralConfigSchema,
SyntheticConfigSchema,
SyntheticRebaseConfigSchema,
]);
export const TokenRouterConfigSchema = TokenConfigSchema.and(
@ -61,6 +81,10 @@ export type NativeConfig = z.infer<typeof NativeConfigSchema>;
export type CollateralConfig = z.infer<typeof CollateralConfigSchema>;
export const isSyntheticConfig = isCompliant(SyntheticConfigSchema);
export const isSyntheticRebaseConfig = isCompliant(SyntheticRebaseConfigSchema);
export const isCollateralRebaseConfig = isCompliant(
CollateralRebaseConfigSchema,
);
export const isCollateralConfig = isCompliant(CollateralConfigSchema);
export const isNativeConfig = isCompliant(NativeConfigSchema);
export const isTokenMetadata = isCompliant(TokenMetadataSchema);
@ -71,7 +95,49 @@ export const WarpRouteDeployConfigSchema = z
const entries = Object.entries(configMap);
return (
entries.some(
([_, config]) => isCollateralConfig(config) || isNativeConfig(config),
([_, config]) =>
isCollateralConfig(config) ||
isCollateralRebaseConfig(config) ||
isNativeConfig(config),
) || entries.every(([_, config]) => isTokenMetadata(config))
);
}, `Config must include Native or Collateral OR all synthetics must define token metadata`);
}, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY)
.transform((warpRouteDeployConfig, ctx) => {
const collateralRebaseEntry = Object.entries(warpRouteDeployConfig).find(
([_, config]) => isCollateralRebaseConfig(config),
);
if (!collateralRebaseEntry) return warpRouteDeployConfig; // Pass through for other token types
if (isCollateralRebasePairedCorrectly(warpRouteDeployConfig)) {
const collateralChainName = collateralRebaseEntry[0];
return objMap(warpRouteDeployConfig, (_, config) => {
if (config.type === TokenType.syntheticRebase)
config.collateralChainName = collateralChainName;
return config;
}) as Record<string, TokenRouterConfig>;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE,
});
return z.NEVER; // Causes schema validation to throw with above issue
});
function isCollateralRebasePairedCorrectly(
warpRouteDeployConfig: Record<string, TokenRouterConfig>,
): boolean {
// Filter out all the non-collateral rebase configs to check if they are only synthetic rebase tokens
const otherConfigs = Object.entries(warpRouteDeployConfig).filter(
([_, config]) => !isCollateralRebaseConfig(config),
);
if (otherConfigs.length === 0) return false;
// The other configs MUST be synthetic rebase
const allOthersSynthetic: boolean = otherConfigs.every(
([_, config], _index) => isSyntheticRebaseConfig(config),
);
return allOthersSynthetic;
}

@ -7827,6 +7827,7 @@ __metadata:
"@hyperlane-xyz/sdk": "npm:5.5.0"
"@hyperlane-xyz/utils": "npm:5.5.0"
"@inquirer/prompts": "npm:^3.0.0"
"@types/chai-as-promised": "npm:^8"
"@types/mocha": "npm:^10.0.1"
"@types/node": "npm:^18.14.5"
"@types/yargs": "npm:^17.0.24"
@ -7834,7 +7835,8 @@ __metadata:
"@typescript-eslint/parser": "npm:^7.4.0"
asn1.js: "npm:^5.4.1"
bignumber.js: "npm:^9.1.1"
chai: "npm:4.5.0"
chai: "npm:^4.5.0"
chai-as-promised: "npm:^8.0.0"
chalk: "npm:^5.3.0"
eslint: "npm:^8.57.0"
eslint-config-prettier: "npm:^9.1.0"
@ -13119,6 +13121,15 @@ __metadata:
languageName: node
linkType: hard
"@types/chai-as-promised@npm:^8":
version: 8.0.0
resolution: "@types/chai-as-promised@npm:8.0.0"
dependencies:
"@types/chai": "npm:*"
checksum: f6db5698e4f28fd6e3914740810f356269b7f4e93a0650b38a9b01a1bae030593487c80bc57a0e69dd0bfb069a61d3dd285bfcfba6d1daf66ef3939577b68169
languageName: node
linkType: hard
"@types/chai@npm:*, @types/chai@npm:^4.2.21":
version: 4.3.1
resolution: "@types/chai@npm:4.3.1"
@ -16218,7 +16229,18 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7":
"chai-as-promised@npm:^8.0.0":
version: 8.0.0
resolution: "chai-as-promised@npm:8.0.0"
dependencies:
check-error: "npm:^2.0.0"
peerDependencies:
chai: ">= 2.1.2 < 6"
checksum: 91d6a49caac7965440b8f8af421ebe6f060a3b5523599ae143816d08fc19d9a971ea2bc5401f82ce88d15d8bc7b64d356bf3e53542ace9e2f25cc454164d3247
languageName: node
linkType: hard
"chai@npm:4.5.0, chai@npm:^4.3.10, chai@npm:^4.3.7, chai@npm:^4.5.0":
version: 4.5.0
resolution: "chai@npm:4.5.0"
dependencies:
@ -16338,6 +16360,13 @@ __metadata:
languageName: node
linkType: hard
"check-error@npm:^2.0.0":
version: 2.1.1
resolution: "check-error@npm:2.1.1"
checksum: d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a
languageName: node
linkType: hard
"chokidar@npm:3.3.0":
version: 3.3.0
resolution: "chokidar@npm:3.3.0"

Loading…
Cancel
Save