@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
449 lines • 18.3 kB
JavaScript
import { expect } from 'chai';
import { ethers } from 'ethers';
import { assert } from '@hyperlane-xyz/utils';
import { TokenFeeType } from '../fee/types.js';
import { TokenType } from './config.js';
import { WarpRouteDeployConfigSchema, WarpRouteDeployConfigSchemaErrors, isCollateralTokenConfig, } from './types.js';
const SOME_ADDRESS = ethers.Wallet.createRandom().address;
const COLLATERAL_TYPES = [
TokenType.collateral,
TokenType.collateralUri,
TokenType.collateralVault,
];
const NON_COLLATERAL_TYPES = [TokenType.synthetic, TokenType.syntheticUri];
describe('WarpRouteDeployConfigSchema refine', () => {
let config;
beforeEach(() => {
config = {
arbitrum: {
type: TokenType.collateral,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
};
});
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 not require mailbox address', () => {
//@ts-ignore
delete config.arbitrum.mailbox;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true;
});
it('should accept deposit-address bridge config', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: SOME_ADDRESS,
feeBps: '1000',
},
},
},
},
});
assert(parseResults.success, 'must be true');
});
it('should reject invalid deposit-address bridge config', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: '0x1234',
},
},
},
},
});
expect(parseResults.success).to.be.false;
});
it('should reject deposit-address bridge config with feeBps above 10000', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: SOME_ADDRESS,
feeBps: '10001',
},
},
},
},
});
expect(parseResults.success).to.be.false;
});
it('should reject deposit-address bridge config with feeBps of 10000', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: SOME_ADDRESS,
feeBps: '10000',
},
},
},
},
});
expect(parseResults.success).to.be.false;
});
it('should reject deposit-address bridge config with invalid feeBps string', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: SOME_ADDRESS,
feeBps: 'abc',
},
},
},
},
});
expect(parseResults.success).to.be.false;
});
it('should reject deposit-address bridge config with empty feeBps string', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: SOME_ADDRESS,
feeBps: '',
},
},
},
},
});
expect(parseResults.success).to.be.false;
});
it('should reject deposit-address bridge config with negative feeBps', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.collateralDepositAddress,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
destinationConfigs: {
ethereum: {
[ethers.utils.hexZeroPad(SOME_ADDRESS, 32)]: {
depositAddress: SOME_ADDRESS,
feeBps: '-1',
},
},
},
},
});
expect(parseResults.success).to.be.false;
});
it('should throw if collateral type and token is empty', async () => {
for (const type of COLLATERAL_TYPES) {
config.arbitrum.type = type;
assert(isCollateralTokenConfig(config.arbitrum), 'must be collateral');
//@ts-ignore
config.arbitrum.token = undefined;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false;
// Set to some address
config.arbitrum.token = SOME_ADDRESS;
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.name = 'name';
for (const type of NON_COLLATERAL_TYPES) {
config.arbitrum.type = type;
config.arbitrum.symbol = undefined;
expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false;
config.arbitrum.symbol = 'symbol';
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 = parseResults.data;
assert(warpConfig.optimism.type === TokenType.syntheticRebase, 'must be syntheticRebase');
expect(warpConfig.optimism.collateralChainName).to.equal('arbitrum');
});
describe('tokenFee input schema', () => {
it('should accept LinearFee without token field', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.synthetic,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
name: 'Test Token',
symbol: 'TEST',
tokenFee: {
type: TokenFeeType.LinearFee,
bps: 100,
},
},
});
assert(parseResults.success, 'must be true');
expect(parseResults.data.arbitrum.tokenFee?.type).to.equal(TokenFeeType.LinearFee);
});
it('should accept RoutingFee without token field', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.synthetic,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
name: 'Test Token',
symbol: 'TEST',
tokenFee: {
type: TokenFeeType.RoutingFee,
feeContracts: {
ethereum: {
type: TokenFeeType.LinearFee,
bps: 100,
},
},
},
},
});
assert(parseResults.success, 'must be true');
expect(parseResults.data.arbitrum.tokenFee?.type).to.equal(TokenFeeType.RoutingFee);
});
it('should accept tokenFee for collateral tokens', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
ethereum: {
type: TokenType.collateral,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
tokenFee: {
type: TokenFeeType.LinearFee,
bps: 100,
},
},
});
assert(parseResults.success, 'must be true');
expect(parseResults.data.ethereum.tokenFee?.type).to.equal(TokenFeeType.LinearFee);
});
it('should accept tokenFee for native tokens', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.native,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
tokenFee: {
type: TokenFeeType.LinearFee,
bps: 100,
},
},
});
assert(parseResults.success, 'must be true');
expect(parseResults.data.arbitrum.tokenFee?.type).to.equal(TokenFeeType.LinearFee);
});
it('should populate owner from token config if not specified', () => {
const parseResults = WarpRouteDeployConfigSchema.safeParse({
arbitrum: {
type: TokenType.synthetic,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
name: 'Test Token',
symbol: 'TEST',
tokenFee: {
type: TokenFeeType.LinearFee,
bps: 100,
},
},
});
assert(parseResults.success, 'must be true');
expect(parseResults.data.arbitrum.tokenFee?.owner).to.equal(SOME_ADDRESS);
});
});
describe('xERC20 validation', () => {
it('should allow xERC20 with xERC20Lockbox', () => {
const config = {
ethereum: {
type: TokenType.XERC20Lockbox,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
arbitrum: {
type: TokenType.XERC20,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
};
const parseResults = WarpRouteDeployConfigSchema.safeParse(config);
expect(parseResults.success).to.be.true;
});
it('should allow xERC20 with collateral', () => {
const config = {
ethereum: {
type: TokenType.collateral,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
arbitrum: {
type: TokenType.XERC20,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
};
const parseResults = WarpRouteDeployConfigSchema.safeParse(config);
expect(parseResults.success).to.be.true;
});
it('should allow multiple xERC20 chains', () => {
const config = {
ethereum: {
type: TokenType.XERC20Lockbox,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
arbitrum: {
type: TokenType.XERC20,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
optimism: {
type: TokenType.XERC20,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
};
const parseResults = WarpRouteDeployConfigSchema.safeParse(config);
expect(parseResults.success).to.be.true;
});
for (const invalidType of [TokenType.synthetic, TokenType.native]) {
it(`should reject xERC20 with ${invalidType}`, () => {
const config = {
ethereum: {
type: TokenType.XERC20,
token: SOME_ADDRESS,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
arbitrum: {
type: invalidType,
owner: SOME_ADDRESS,
mailbox: SOME_ADDRESS,
},
};
const parseResults = WarpRouteDeployConfigSchema.safeParse(config);
assert(!parseResults.success, 'must be false');
expect(parseResults.error.issues[0].message).to.equal('xERC20 warp routes must only contain xERC20 or collateral token types');
});
}
});
});
//# sourceMappingURL=types.test.js.map