feat: WarpModule with create() (#3838)
### Description - Adds WarpModule with create logic. - Updates logic to WarpModuleReader ### Drive by - Adds CustomIsmConfig mentioned in https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3773 ### Related issues - Fixes #3839 ### Backward compatibility Yes ### Testing Unit Tests --------- Signed-off-by: Paul Balaji <paul@hyperlane.xyz> Co-authored-by: Paul Balaji <paul@hyperlane.xyz> Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz> Co-authored-by: J M Rossy <jm.rossy@gmail.com> Co-authored-by: Noah Bayindirli 🥂 <noah@primeprotocol.xyz> Co-authored-by: Noah Bayindirli 🥂 <noah@hyperlane.xyz>pull/3914/head
parent
cc65ee8fa1
commit
bdcbe1d168
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Add EvmWarpModule with create() |
@ -0,0 +1,202 @@ |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; |
||||
import { expect } from 'chai'; |
||||
import { constants } from 'ethers'; |
||||
import hre from 'hardhat'; |
||||
|
||||
import { |
||||
ERC20Test, |
||||
ERC20Test__factory, |
||||
ERC4626Test__factory, |
||||
GasRouter, |
||||
HypERC20CollateralVaultDeposit__factory, |
||||
HypERC20__factory, |
||||
HypNative__factory, |
||||
Mailbox, |
||||
Mailbox__factory, |
||||
} from '@hyperlane-xyz/core'; |
||||
import { |
||||
HyperlaneContractsMap, |
||||
RouterConfig, |
||||
TestChainName, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
|
||||
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 { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; |
||||
import { MultiProvider } from '../providers/MultiProvider.js'; |
||||
import { ChainMap } from '../types.js'; |
||||
|
||||
import { EvmERC20WarpModule } from './EvmERC20WarpModule.js'; |
||||
import { TokenType } from './config.js'; |
||||
import { TokenRouterConfig } from './schemas.js'; |
||||
|
||||
describe('EvmERC20WarpHyperlaneModule', async () => { |
||||
const TOKEN_NAME = 'fake'; |
||||
const TOKEN_SUPPLY = '100000000000000000000'; |
||||
const TOKEN_DECIMALS = 18; |
||||
const chain = TestChainName.test4; |
||||
let mailbox: Mailbox; |
||||
let hookAddress: string; |
||||
let ismFactory: HyperlaneIsmFactory; |
||||
let factories: HyperlaneContractsMap<ProxyFactoryFactories>; |
||||
let erc20Factory: ERC20Test__factory; |
||||
let token: ERC20Test; |
||||
let signer: SignerWithAddress; |
||||
let multiProvider: MultiProvider; |
||||
let coreApp: TestCoreApp; |
||||
let routerConfigMap: ChainMap<RouterConfig>; |
||||
let baseConfig: RouterConfig; |
||||
|
||||
async function validateCoreValues(deployedToken: GasRouter) { |
||||
expect(await deployedToken.mailbox()).to.equal(mailbox.address); |
||||
expect(await deployedToken.hook()).to.equal(hookAddress); |
||||
expect(await deployedToken.interchainSecurityModule()).to.equal( |
||||
constants.AddressZero, |
||||
); |
||||
expect(await deployedToken.owner()).to.equal(signer.address); |
||||
} |
||||
before(async () => { |
||||
[signer] = await hre.ethers.getSigners(); |
||||
multiProvider = MultiProvider.createTestMultiProvider({ signer }); |
||||
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); |
||||
factories = await ismFactoryDeployer.deploy( |
||||
multiProvider.mapKnownChains(() => ({})), |
||||
); |
||||
ismFactory = new HyperlaneIsmFactory(factories, multiProvider); |
||||
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); |
||||
routerConfigMap = coreApp.getRouterConfig(signer.address); |
||||
|
||||
erc20Factory = new ERC20Test__factory(signer); |
||||
token = await erc20Factory.deploy( |
||||
TOKEN_NAME, |
||||
TOKEN_NAME, |
||||
TOKEN_SUPPLY, |
||||
TOKEN_DECIMALS, |
||||
); |
||||
|
||||
baseConfig = routerConfigMap[chain]; |
||||
|
||||
mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); |
||||
hookAddress = await mailbox.defaultHook(); |
||||
}); |
||||
|
||||
it('should create with a a collateral config', async () => { |
||||
const config = { |
||||
...baseConfig, |
||||
type: TokenType.collateral, |
||||
token: token.address, |
||||
hook: hookAddress, |
||||
}; |
||||
|
||||
// Deploy using WarpModule
|
||||
const evmERC20WarpModule = await EvmERC20WarpModule.create({ |
||||
chain, |
||||
config, |
||||
multiProvider, |
||||
}); |
||||
|
||||
// Let's derive it's onchain token type
|
||||
const { deployedTokenRoute } = evmERC20WarpModule.serialize(); |
||||
const tokenType: TokenType = |
||||
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); |
||||
expect(tokenType).to.equal(TokenType.collateral); |
||||
}); |
||||
|
||||
it('should create with a collateral vault config', async () => { |
||||
const vaultFactory = new ERC4626Test__factory(signer); |
||||
const vault = await vaultFactory.deploy( |
||||
token.address, |
||||
TOKEN_NAME, |
||||
TOKEN_NAME, |
||||
); |
||||
const config = { |
||||
type: TokenType.collateralVault, |
||||
token: vault.address, |
||||
hook: hookAddress, |
||||
...baseConfig, |
||||
}; |
||||
|
||||
// Deploy using WarpModule
|
||||
const evmERC20WarpModule = await EvmERC20WarpModule.create({ |
||||
chain, |
||||
config, |
||||
multiProvider, |
||||
}); |
||||
|
||||
// Let's derive it's onchain token type
|
||||
const { deployedTokenRoute } = evmERC20WarpModule.serialize(); |
||||
const tokenType: TokenType = |
||||
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); |
||||
expect(tokenType).to.equal(TokenType.collateralVault); |
||||
|
||||
// Validate onchain token values
|
||||
const collateralVault = HypERC20CollateralVaultDeposit__factory.connect( |
||||
deployedTokenRoute, |
||||
signer, |
||||
); |
||||
await validateCoreValues(collateralVault); |
||||
expect(await collateralVault.vault()).to.equal(vault.address); |
||||
expect(await collateralVault.wrappedToken()).to.equal(token.address); |
||||
}); |
||||
|
||||
it('should create with a a synthetic config', async () => { |
||||
const config = { |
||||
type: TokenType.synthetic, |
||||
token: token.address, |
||||
hook: hookAddress, |
||||
name: TOKEN_NAME, |
||||
symbol: TOKEN_NAME, |
||||
decimals: TOKEN_DECIMALS, |
||||
totalSupply: TOKEN_SUPPLY, |
||||
...baseConfig, |
||||
}; |
||||
|
||||
// Deploy using WarpModule
|
||||
const evmERC20WarpModule = await EvmERC20WarpModule.create({ |
||||
chain, |
||||
config, |
||||
multiProvider, |
||||
}); |
||||
|
||||
// Let's derive it's onchain token type
|
||||
const { deployedTokenRoute } = evmERC20WarpModule.serialize(); |
||||
const tokenType: TokenType = |
||||
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); |
||||
expect(tokenType).to.equal(TokenType.synthetic); |
||||
|
||||
// Validate onchain token values
|
||||
const synthetic = HypERC20__factory.connect(deployedTokenRoute, signer); |
||||
await validateCoreValues(synthetic); |
||||
expect(await synthetic.name()).to.equal(TOKEN_NAME); |
||||
expect(await synthetic.symbol()).to.equal(TOKEN_NAME); |
||||
expect(await synthetic.decimals()).to.equal(TOKEN_DECIMALS); |
||||
expect(await synthetic.totalSupply()).to.equal(TOKEN_SUPPLY); |
||||
}); |
||||
|
||||
it('should create with a a native config', async () => { |
||||
const config = { |
||||
type: TokenType.native, |
||||
hook: hookAddress, |
||||
...baseConfig, |
||||
} as TokenRouterConfig; |
||||
|
||||
// Deploy using WarpModule
|
||||
const evmERC20WarpModule = await EvmERC20WarpModule.create({ |
||||
chain, |
||||
config, |
||||
multiProvider, |
||||
}); |
||||
|
||||
// Let's derive it's onchain token type
|
||||
const { deployedTokenRoute } = evmERC20WarpModule.serialize(); |
||||
const tokenType: TokenType = |
||||
await evmERC20WarpModule.reader.deriveTokenType(deployedTokenRoute); |
||||
expect(tokenType).to.equal(TokenType.native); |
||||
|
||||
// Validate onchain token values
|
||||
const native = HypNative__factory.connect(deployedTokenRoute, signer); |
||||
await validateCoreValues(native); |
||||
}); |
||||
}); |
@ -0,0 +1,93 @@ |
||||
import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
HyperlaneModule, |
||||
HyperlaneModuleParams, |
||||
} from '../core/AbstractHyperlaneModule.js'; |
||||
import { MultiProvider } from '../providers/MultiProvider.js'; |
||||
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; |
||||
import { ChainNameOrId } from '../types.js'; |
||||
|
||||
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; |
||||
import { HypERC20Deployer } from './deploy.js'; |
||||
import { TokenRouterConfig } from './schemas.js'; |
||||
|
||||
export class EvmERC20WarpModule extends HyperlaneModule< |
||||
ProtocolType.Ethereum, |
||||
TokenRouterConfig, |
||||
{ |
||||
deployedTokenRoute: Address; |
||||
} |
||||
> { |
||||
protected logger = rootLogger.child({ |
||||
module: 'EvmERC20WarpModule', |
||||
}); |
||||
reader: EvmERC20WarpRouteReader; |
||||
|
||||
constructor( |
||||
protected readonly multiProvider: MultiProvider, |
||||
args: HyperlaneModuleParams< |
||||
TokenRouterConfig, |
||||
{ |
||||
deployedTokenRoute: Address; |
||||
} |
||||
>, |
||||
) { |
||||
super(args); |
||||
|
||||
this.reader = new EvmERC20WarpRouteReader(multiProvider, args.chain); |
||||
} |
||||
|
||||
/** |
||||
* Retrieves the token router configuration for the specified address. |
||||
* |
||||
* @param address - The address to derive the token router configuration from. |
||||
* @returns A promise that resolves to the token router configuration. |
||||
*/ |
||||
public async read(): Promise<TokenRouterConfig> { |
||||
return this.reader.deriveWarpRouteConfig( |
||||
this.args.addresses.deployedTokenRoute, |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Updates the Warp Route contract with the provided configuration. |
||||
* |
||||
* @remark Currently only supports updating ISM or hook. |
||||
* |
||||
* @param expectedConfig - The configuration for the token router to be updated. |
||||
* @returns An array of Ethereum transactions that were executed to update the contract, or an error if the update failed. |
||||
*/ |
||||
public async update( |
||||
_expectedConfig: TokenRouterConfig, |
||||
): Promise<AnnotatedEV5Transaction[]> { |
||||
throw Error('Not implemented'); |
||||
} |
||||
|
||||
/** |
||||
* Deploys the Warp Route. |
||||
* |
||||
* @param chain - The chain to deploy the module on.
|
||||
* @param config - The configuration for the token router. |
||||
* @param multiProvider - The multi-provider instance to use. |
||||
* @returns A new instance of the EvmERC20WarpHyperlaneModule. |
||||
*/ |
||||
public static async create(params: { |
||||
chain: ChainNameOrId; |
||||
config: TokenRouterConfig; |
||||
multiProvider: MultiProvider; |
||||
}): Promise<EvmERC20WarpModule> { |
||||
const { chain, config, multiProvider } = params; |
||||
const chainName = multiProvider.getChainName(chain); |
||||
const deployer = new HypERC20Deployer(multiProvider); |
||||
const deployedContracts = await deployer.deployContracts(chainName, config); |
||||
|
||||
return new EvmERC20WarpModule(multiProvider, { |
||||
addresses: { |
||||
deployedTokenRoute: deployedContracts[config.type].address, |
||||
}, |
||||
chain, |
||||
config, |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,237 @@ |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; |
||||
import { expect } from 'chai'; |
||||
import hre from 'hardhat'; |
||||
|
||||
import { |
||||
ERC20Test, |
||||
ERC20Test__factory, |
||||
ERC4626, |
||||
ERC4626Test__factory, |
||||
Mailbox, |
||||
Mailbox__factory, |
||||
} from '@hyperlane-xyz/core'; |
||||
import { |
||||
HyperlaneContractsMap, |
||||
RouterConfig, |
||||
TestChainName, |
||||
TokenRouterConfig, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
|
||||
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 { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; |
||||
import { MultiProvider } from '../providers/MultiProvider.js'; |
||||
import { ChainMap } from '../types.js'; |
||||
|
||||
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; |
||||
import { TokenType } from './config.js'; |
||||
import { HypERC20Deployer } from './deploy.js'; |
||||
|
||||
describe('ERC20WarpRouterReader', async () => { |
||||
const TOKEN_NAME = 'fake'; |
||||
const TOKEN_SUPPLY = '100000000000000000000'; |
||||
const TOKEN_DECIMALS = 18; |
||||
const GAS = 65_000; |
||||
const chain = TestChainName.test4; |
||||
let ismFactory: HyperlaneIsmFactory; |
||||
let factories: HyperlaneContractsMap<ProxyFactoryFactories>; |
||||
let erc20Factory: ERC20Test__factory; |
||||
let token: ERC20Test; |
||||
let signer: SignerWithAddress; |
||||
let deployer: HypERC20Deployer; |
||||
let multiProvider: MultiProvider; |
||||
let coreApp: TestCoreApp; |
||||
let routerConfigMap: ChainMap<RouterConfig>; |
||||
let baseConfig: RouterConfig; |
||||
let mailbox: Mailbox; |
||||
let evmERC20WarpRouteReader: EvmERC20WarpRouteReader; |
||||
let vault: ERC4626; |
||||
before(async () => { |
||||
[signer] = await hre.ethers.getSigners(); |
||||
multiProvider = MultiProvider.createTestMultiProvider({ signer }); |
||||
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); |
||||
factories = await ismFactoryDeployer.deploy( |
||||
multiProvider.mapKnownChains(() => ({})), |
||||
); |
||||
ismFactory = new HyperlaneIsmFactory(factories, multiProvider); |
||||
coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); |
||||
routerConfigMap = coreApp.getRouterConfig(signer.address); |
||||
|
||||
erc20Factory = new ERC20Test__factory(signer); |
||||
token = await erc20Factory.deploy( |
||||
TOKEN_NAME, |
||||
TOKEN_NAME, |
||||
TOKEN_SUPPLY, |
||||
TOKEN_DECIMALS, |
||||
); |
||||
|
||||
baseConfig = routerConfigMap[chain]; |
||||
mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); |
||||
evmERC20WarpRouteReader = new EvmERC20WarpRouteReader(multiProvider, chain); |
||||
deployer = new HypERC20Deployer(multiProvider); |
||||
|
||||
const vaultFactory = new ERC4626Test__factory(signer); |
||||
vault = await vaultFactory.deploy(token.address, TOKEN_NAME, TOKEN_NAME); |
||||
}); |
||||
|
||||
it('should derive a token type from contract', async () => { |
||||
const typesToDerive = [ |
||||
TokenType.collateral, |
||||
TokenType.collateralVault, |
||||
TokenType.synthetic, |
||||
TokenType.native, |
||||
] as const; |
||||
|
||||
await Promise.all( |
||||
typesToDerive.map(async (type) => { |
||||
// Create config
|
||||
const config = { |
||||
[chain]: { |
||||
type, |
||||
token: |
||||
type === TokenType.collateralVault |
||||
? vault.address |
||||
: token.address, |
||||
hook: await mailbox.defaultHook(), |
||||
name: TOKEN_NAME, |
||||
symbol: TOKEN_NAME, |
||||
decimals: TOKEN_DECIMALS, |
||||
totalSupply: TOKEN_SUPPLY, |
||||
gas: GAS, |
||||
...baseConfig, |
||||
}, |
||||
}; |
||||
// Deploy warp route with config
|
||||
const warpRoute = await deployer.deploy(config); |
||||
const derivedTokenType = await evmERC20WarpRouteReader.deriveTokenType( |
||||
warpRoute[chain][type].address, |
||||
); |
||||
expect(derivedTokenType).to.equal(type); |
||||
}), |
||||
); |
||||
}); |
||||
|
||||
it('should derive collateral config correctly', async () => { |
||||
// Create config
|
||||
const config = { |
||||
[chain]: { |
||||
type: TokenType.collateral, |
||||
token: token.address, |
||||
hook: await mailbox.defaultHook(), |
||||
interchainsecurityModule: await mailbox.defaultIsm(), |
||||
...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].collateral.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 hook because they're potentially objects
|
||||
expect(derivedConfig.hook).to.deep.equal( |
||||
await evmERC20WarpRouteReader.evmHookReader.deriveHookConfig( |
||||
config[chain].hook as string, |
||||
), |
||||
); |
||||
// Check ism. should return undefined
|
||||
expect(derivedConfig.interchainSecurityModule).to.be.undefined; |
||||
|
||||
// Check if token values matches
|
||||
if (derivedConfig.type === TokenType.collateral) { |
||||
expect(derivedConfig.name).to.equal(TOKEN_NAME); |
||||
expect(derivedConfig.symbol).to.equal(TOKEN_NAME); |
||||
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); |
||||
} |
||||
}); |
||||
|
||||
it('should derive synthetic config correctly', async () => { |
||||
// Create config
|
||||
const config = { |
||||
[chain]: { |
||||
type: TokenType.synthetic, |
||||
token: token.address, |
||||
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].synthetic.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 native config correctly', async () => { |
||||
// Create config
|
||||
const config = { |
||||
[chain]: { |
||||
type: TokenType.native, |
||||
hook: await mailbox.defaultHook(), |
||||
...baseConfig, |
||||
}, |
||||
} as ChainMap<TokenRouterConfig>; |
||||
// Deploy with config
|
||||
const warpRoute = await deployer.deploy(config); |
||||
|
||||
// Derive config and check if each value matches
|
||||
const derivedConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig( |
||||
warpRoute[chain].native.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
|
||||
expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); |
||||
}); |
||||
|
||||
it('should return undefined if ism is not set onchain', async () => { |
||||
// Create config
|
||||
const config = { |
||||
[chain]: { |
||||
type: TokenType.collateral, |
||||
token: token.address, |
||||
hook: await mailbox.defaultHook(), |
||||
...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].collateral.address, |
||||
); |
||||
|
||||
expect(derivedConfig.interchainSecurityModule).to.be.undefined; |
||||
}); |
||||
}); |
Loading…
Reference in new issue