diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index a970a6270..26941af52 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -112,9 +112,10 @@ jobs: test-env: runs-on: ubuntu-latest needs: [yarn-build] - strategy: + strategy: matrix: environment: [testnet3, mainnet2] + module: [core, igp] steps: - uses: actions/checkout@v3 @@ -126,8 +127,8 @@ jobs: - name: Install Foundry uses: onbjerg/foundry-toolchain@v1 - - name: Test ${{ matrix.environment }} deployment (check, deploy, govern, check again) - run: cd typescript/infra && ./fork.sh ${{ matrix.environment }} + - name: Test ${{ matrix.environment }} ${{ matrix.module }} deployment (check, deploy, govern, check again) + run: cd typescript/infra && ./fork.sh ${{ matrix.environment }} ${{ matrix.module }} test-sol: env: diff --git a/rust/.gitignore b/rust/.gitignore index 6129bbccb..1d73887c2 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -2,4 +2,4 @@ target validatordb relayerdb kathydb -config/test_* +config/test_config.json diff --git a/typescript/helloworld b/typescript/helloworld index 99bcdfca2..3b443c79b 160000 --- a/typescript/helloworld +++ b/typescript/helloworld @@ -1 +1 @@ -Subproject commit 99bcdfca24e8c9a5f2d3cca8eb6da891ff3c715e +Subproject commit 3b443c79bcb7671ef3d49ba7d6765c9adb139789 diff --git a/typescript/infra/.gitignore b/typescript/infra/.gitignore index 07fc57daf..5c78504ae 100644 --- a/typescript/infra/.gitignore +++ b/typescript/infra/.gitignore @@ -5,3 +5,4 @@ dist/ cache/ test/outputs config/environments/test/core/ +config/environments/test/igp/ diff --git a/typescript/infra/config/environments/mainnet2/agent.ts b/typescript/infra/config/environments/mainnet2/agent.ts index 8028fa620..4ef95cc28 100644 --- a/typescript/infra/config/environments/mainnet2/agent.ts +++ b/typescript/infra/config/environments/mainnet2/agent.ts @@ -1,11 +1,8 @@ -import { chainMetadata } from '@hyperlane-xyz/sdk'; +import { AgentConnectionType, chainMetadata } from '@hyperlane-xyz/sdk'; import { ALL_KEY_ROLES, KEY_ROLE_ENUM } from '../../../src/agents/roles'; import { AgentConfig } from '../../../src/config'; -import { - ConnectionType, - GasPaymentEnforcementPolicyType, -} from '../../../src/config/agent'; +import { GasPaymentEnforcementPolicyType } from '../../../src/config/agent'; import { Contexts } from '../../contexts'; import { helloworldMatchingList, routerMatchingList } from '../../utils'; @@ -36,7 +33,7 @@ export const hyperlane: AgentConfig = { }, environmentChainNames: chainNames, contextChainNames: chainNames, - connectionType: ConnectionType.HttpQuorum, + connectionType: AgentConnectionType.HttpQuorum, validators, relayer: { default: { @@ -78,7 +75,7 @@ export const releaseCandidate: AgentConfig = { }, environmentChainNames: chainNames, contextChainNames: chainNames, - connectionType: ConnectionType.HttpFallback, + connectionType: AgentConnectionType.HttpFallback, relayer: { default: { whitelist: releaseCandidateHelloworldMatchingList, diff --git a/typescript/infra/config/environments/mainnet2/core.ts b/typescript/infra/config/environments/mainnet2/core.ts index 35c8b58af..fa5c7642d 100644 --- a/typescript/infra/config/environments/mainnet2/core.ts +++ b/typescript/infra/config/environments/mainnet2/core.ts @@ -1,177 +1,20 @@ import { ChainMap, CoreConfig, - GasOracleContractType, + defaultMultisigIsmConfigs, + objMap, } from '@hyperlane-xyz/sdk'; -import { MainnetChains, chainNames } from './chains'; +import { chainNames } from './chains'; +import { owners } from './owners'; -function getGasOracles(local: MainnetChains) { - return Object.fromEntries( - chainNames - .filter((name) => name !== local) - .map((name) => [name, GasOracleContractType.StorageGasOracle]), - ); -} - -const KEY_FUNDER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; - -export const core: ChainMap = { - celo: { - owner: '0x1DE69322B55AC7E0999F8e7738a1428C8b130E4d', - multisigIsm: { - threshold: 4, - validators: [ - '0x1f20274b1210046769d48174c2f0e7c25ca7d5c5', // abacus works 0 - '0x3bc014bafa43f93d534aed34f750997cdffcf007', // dsrv - '0xd79d506d741fa735938f7b7847a926e34a6fe6b0', // everstake - '0xe4a258bc61e65914c2a477b2a8a433ab4ebdf44b', // zplabs - '0x6aea63b0be4679c1385c26a92a3ff8aa6a8379f2', // staked - '0xc0085e1a49bcc69e534272adb82c74c0e007e1ca', // zkv - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('celo'), - }, - }, - ethereum: { - owner: '0x12C5AB61Fe17dF9c65739DBa73dF294708f78d23', - multisigIsm: { - threshold: 4, - validators: [ - '0x4c327ccb881a7542be77500b2833dc84c839e7b7', // abacus works 0 - '0x84cb373148ef9112b277e68acf676fefa9a9a9a0', // dsrv - '0x0d860c2b28bec3af4fd3a5997283e460ff6f2789', // everstake - '0xd4c1211f0eefb97a846c4e6d6589832e52fc03db', // zp labs - '0x600c90404d5c9df885404d2cc5350c9b314ea3a2', // staked - '0x892DC66F5B2f8C438E03f6323394e34A9C24F2D6', // zkv - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('ethereum'), - }, - }, - avalanche: { - owner: '0xDF9B28B76877f1b1B4B8a11526Eb7D8D7C49f4f3', - multisigIsm: { - threshold: 4, - validators: [ - '0xa7aa52623fe3d78c343008c95894be669e218b8d', // abacus works 0 - '0xb6004433fb04f643e2d48ae765c0e7f890f0bc0c', // dsrv - '0xa07e213e0985b21a6128e6c22ab5fb73948b0cc2', // everstake - '0x73853ed9a5f6f2e4c521970a94d43469e3cdaea6', // zplabs - '0xbd2e136cda02ba627ca882e49b184cbe976081c8', // staked - '0x1418126f944a44dad9edbab32294a8c890e7a9e3', // zkv - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('avalanche'), - }, - }, - polygon: { - owner: '0x0D195469f76146F6ae3De8fc887e0f0DFBA691e7', - multisigIsm: { - threshold: 4, - validators: [ - '0x59a001c3451e7f9f3b4759ea215382c1e9aa5fc1', // abacus works 0 - '0x009fb042d28944017177920c1d40da02bfebf474', // dsrv - '0xba4b13e23705a5919c1901150d9697e8ffb3ea71', // everstake - '0x2faa4071b718972f9b4beec1d8cbaa4eb6cca6c6', // zp labs - '0x5ae9b0f833dfe09ef455562a1f603f1634504dd6', // staked - '0x6a163d312f7352a95c9b81dca15078d5bf77a442', // zkv - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('polygon'), - }, - }, - bsc: { - owner: '0xA0d3dcB9d61Fba32cc02Ad63983e101b29E2f28a', - multisigIsm: { - threshold: 4, - validators: [ - '0xcc84b1eb711e5076b2755cf4ad1d2b42c458a45e', // abacus works 0 - '0xefe34eae2bca1846b895d2d0762ec21796aa196a', // dsrv - '0x662674e80e189b0861d6835c287693f50ee0c2ff', // everstake - '0x8a0f59075af466841808c529624807656309c9da', // zp labs - '0xdd2ff046ccd748a456b4757a73d47f165469669f', // staked - '0x034c4924c30ec4aa1b7f3ad58548988f0971e1bf', // zkv - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('bsc'), - }, - }, - arbitrum: { - owner: '0xbA47E1b575980B7D1b1508cc48bE1Df4EE508111', - multisigIsm: { - threshold: 4, - validators: [ - '0xbcb815f38d481a5eba4d7ac4c9e74d9d0fc2a7e7', // abacus works 0 - '0xd839424e2e5ace0a81152298dc2b1e3bb3c7fb20', // dsrv - '0xb8085c954b75b7088bcce69e61d12fcef797cd8d', // everstake - '0x9856dcb10fd6e5407fa74b5ab1d3b96cc193e9b7', // zp labs - '0x505dff4e0827aa5065f5e001db888e0569d46490', // staked - '0x25c6779d4610f940bf2488732e10bcffb9d36f81', // ZKV - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('arbitrum'), - }, - }, - optimism: { - owner: '0xb523CFAf45AACF472859f8B793CB0BFDB16bD257', - multisigIsm: { - threshold: 4, - validators: [ - '0x9f2296d5cfc6b5176adc7716c7596898ded13d35', // abacus works 0 - '0x9c10bbe8efa03a8f49dfdb5c549258e3a8dca097', // dsrv - '0x62144d4a52a0a0335ea5bb84392ef9912461d9dd', // everstake - '0xaff4718d5d637466ad07441ee3b7c4af8e328dbd', // zp labs - '0xc64d1efeab8ae222bc889fe669f75d21b23005d9', // staked - '0xfa174eb2b4921bb652bc1ada3e8b00e7e280bf3c', // ZKV - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('optimism'), - }, - }, - moonbeam: { - owner: '0xF0cb1f968Df01fc789762fddBfA704AE0F952197', - multisigIsm: { - threshold: 3, - validators: [ - '0x237243d32d10e3bdbbf8dbcccc98ad44c1c172ea', // abacus works 0 - '0x9509c8cf0a06955f27342262af501b74874e98fb', // dsrv - '0xb7113c999e4d587b162dd1a28c73f3f51c6bdcdc', // everstake - '0x26725501597d47352a23cd26f122709f69ad53bc', // staked - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('moonbeam'), - }, - }, - gnosis: { - owner: '0x36b0AA0e7d04e7b825D7E409FEa3c9A3d57E4C22', - multisigIsm: { - threshold: 2, - validators: [ - '0xd0529ec8df08d0d63c0f023786bfa81e4bb51fd6', // abacus works 0 - '0x829d6ec129bc7187fb1ed161adcf7939fe0c515f', // abacus works 1 - '0x00009f8935e94bfe52ab3441df3526ab7cc38db1', // abacus works 2 - ], - }, - igp: { - beneficiary: KEY_FUNDER_ADDRESS, - gasOracles: getGasOracles('gnosis'), - }, - }, -}; +export const core: ChainMap = objMap(owners, (local, owner) => { + return { + owner, + multisigIsm: Object.fromEntries( + Object.entries(defaultMultisigIsmConfigs).filter( + ([chain]) => chain !== local && chainNames.includes(chain), + ), + ), + }; +}); diff --git a/typescript/infra/config/environments/mainnet2/funding.ts b/typescript/infra/config/environments/mainnet2/funding.ts index 7f0ad2c94..81ed72e8d 100644 --- a/typescript/infra/config/environments/mainnet2/funding.ts +++ b/typescript/infra/config/environments/mainnet2/funding.ts @@ -1,5 +1,6 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { KEY_ROLE_ENUM } from '../../../src/agents/roles'; -import { ConnectionType } from '../../../src/config/agent'; import { KeyFunderConfig } from '../../../src/config/funding'; import { Contexts } from '../../contexts'; @@ -22,5 +23,5 @@ export const keyFunderConfig: KeyFunderConfig = { [Contexts.Hyperlane]: [KEY_ROLE_ENUM.Relayer, KEY_ROLE_ENUM.Kathy], [Contexts.ReleaseCandidate]: [KEY_ROLE_ENUM.Relayer, KEY_ROLE_ENUM.Kathy], }, - connectionType: ConnectionType.Http, + connectionType: AgentConnectionType.Http, }; diff --git a/typescript/infra/config/environments/mainnet2/helloworld.ts b/typescript/infra/config/environments/mainnet2/helloworld.ts index c41762646..16963bf32 100644 --- a/typescript/infra/config/environments/mainnet2/helloworld.ts +++ b/typescript/infra/config/environments/mainnet2/helloworld.ts @@ -1,5 +1,6 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { HelloWorldConfig } from '../../../src/config'; -import { ConnectionType } from '../../../src/config/agent'; import { HelloWorldKathyRunMode } from '../../../src/config/helloworld'; import { Contexts } from '../../contexts'; @@ -23,7 +24,7 @@ export const hyperlane: HelloWorldConfig = { }, messageSendTimeout: 1000 * 60 * 8, // 8 min messageReceiptTimeout: 1000 * 60 * 20, // 20 min - connectionType: ConnectionType.HttpFallback, + connectionType: AgentConnectionType.HttpFallback, cyclesBetweenEthereumMessages: 3, // Skip 3 cycles of Ethereum, i.e. send/receive Ethereum messages every 32 hours. }, }; @@ -43,7 +44,7 @@ export const releaseCandidate: HelloWorldConfig = { }, messageSendTimeout: 1000 * 60 * 8, // 8 min messageReceiptTimeout: 1000 * 60 * 20, // 20 min - connectionType: ConnectionType.Http, + connectionType: AgentConnectionType.Http, }, }; diff --git a/typescript/infra/config/environments/mainnet2/igp.ts b/typescript/infra/config/environments/mainnet2/igp.ts new file mode 100644 index 000000000..2e33601b0 --- /dev/null +++ b/typescript/infra/config/environments/mainnet2/igp.ts @@ -0,0 +1,44 @@ +import { + ChainMap, + GasOracleContractType, + OverheadIgpConfig, + defaultMultisigIsmConfigs, + multisigIsmVerificationCost, + objMap, +} from '@hyperlane-xyz/sdk'; +import { utils } from '@hyperlane-xyz/utils'; + +import { MainnetChains, chainNames } from './chains'; +import { owners } from './owners'; + +const KEY_FUNDER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; + +function getGasOracles(local: MainnetChains) { + return Object.fromEntries( + utils + .exclude(local, chainNames) + .map((name) => [name, GasOracleContractType.StorageGasOracle]), + ); +} + +export const igp: ChainMap = objMap( + owners, + (chain, owner) => { + return { + owner, + beneficiary: KEY_FUNDER_ADDRESS, + gasOracleType: getGasOracles(chain), + overhead: Object.fromEntries( + utils + .exclude(chain, chainNames) + .map((remote) => [ + remote, + multisigIsmVerificationCost( + defaultMultisigIsmConfigs[remote].threshold, + defaultMultisigIsmConfigs[remote].validators.length, + ), + ]), + ), + }; + }, +); diff --git a/typescript/infra/config/environments/mainnet2/index.ts b/typescript/infra/config/environments/mainnet2/index.ts index 455530afc..1174f1602 100644 --- a/typescript/infra/config/environments/mainnet2/index.ts +++ b/typescript/infra/config/environments/mainnet2/index.ts @@ -1,7 +1,8 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { getMultiProviderForRole } from '../../../scripts/utils'; import { KEY_ROLE_ENUM } from '../../../src/agents/roles'; import { CoreEnvironmentConfig } from '../../../src/config'; -import { ConnectionType } from '../../../src/config/agent'; import { Contexts } from '../../contexts'; import { agents } from './agent'; @@ -10,7 +11,9 @@ import { core } from './core'; import { keyFunderConfig } from './funding'; import { storageGasOracleConfig } from './gas-oracle'; import { helloWorld } from './helloworld'; +import { igp } from './igp'; import { infrastructure } from './infrastructure'; +import { owners } from './owners'; export const environment: CoreEnvironmentConfig = { environment: environmentName, @@ -18,7 +21,7 @@ export const environment: CoreEnvironmentConfig = { getMultiProvider: ( context: Contexts = Contexts.Hyperlane, role: KEY_ROLE_ENUM = KEY_ROLE_ENUM.Deployer, - connectionType?: ConnectionType, + connectionType?: AgentConnectionType, ) => getMultiProviderForRole( mainnetConfigs, @@ -30,6 +33,8 @@ export const environment: CoreEnvironmentConfig = { ), agents, core, + igp, + owners, infra: infrastructure, helloWorld, keyFunderConfig, diff --git a/typescript/infra/config/environments/mainnet2/owners.ts b/typescript/infra/config/environments/mainnet2/owners.ts new file mode 100644 index 000000000..ecf824d9b --- /dev/null +++ b/typescript/infra/config/environments/mainnet2/owners.ts @@ -0,0 +1,13 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; + +export const owners: ChainMap = { + celo: '0x1DE69322B55AC7E0999F8e7738a1428C8b130E4d', + ethereum: '0x12C5AB61Fe17dF9c65739DBa73dF294708f78d23', + avalanche: '0xDF9B28B76877f1b1B4B8a11526Eb7D8D7C49f4f3', + polygon: '0x0D195469f76146F6ae3De8fc887e0f0DFBA691e7', + bsc: '0xA0d3dcB9d61Fba32cc02Ad63983e101b29E2f28a', + arbitrum: '0xbA47E1b575980B7D1b1508cc48bE1Df4EE508111', + optimism: '0xb523CFAf45AACF472859f8B793CB0BFDB16bD257', + moonbeam: '0xF0cb1f968Df01fc789762fddBfA704AE0F952197', + gnosis: '0x36b0AA0e7d04e7b825D7E409FEa3c9A3d57E4C22', +}; diff --git a/typescript/infra/config/environments/test/agent.ts b/typescript/infra/config/environments/test/agent.ts index 7958b2e8f..bc689517a 100644 --- a/typescript/infra/config/environments/test/agent.ts +++ b/typescript/infra/config/environments/test/agent.ts @@ -1,9 +1,8 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { ALL_KEY_ROLES } from '../../../src/agents/roles'; import { AgentConfig } from '../../../src/config'; -import { - ConnectionType, - GasPaymentEnforcementPolicyType, -} from '../../../src/config/agent'; +import { GasPaymentEnforcementPolicyType } from '../../../src/config/agent'; import { Contexts } from '../../contexts'; import { chainNames } from './chains'; @@ -19,7 +18,7 @@ export const hyperlane: AgentConfig = { }, environmentChainNames: chainNames, contextChainNames: chainNames, - connectionType: ConnectionType.Http, + connectionType: AgentConnectionType.Http, validators, relayer: { default: { diff --git a/typescript/infra/config/environments/test/core.ts b/typescript/infra/config/environments/test/core.ts index aa935cbd2..3e8d24186 100644 --- a/typescript/infra/config/environments/test/core.ts +++ b/typescript/infra/config/environments/test/core.ts @@ -1,55 +1,13 @@ -import { - ChainMap, - CoreConfig, - GasOracleContractType, -} from '@hyperlane-xyz/sdk'; - -import { TestChains, chainNames } from './chains'; - -function getGasOracles(local: TestChains) { - return Object.fromEntries( - chainNames - .filter((name) => name !== local) - .map((name) => [name, GasOracleContractType.StorageGasOracle]), - ); -} - -const OWNER_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; - -export const core: ChainMap = { - // Owner is hardhat account 0 - // Validators are hardhat accounts 1-3 - test1: { - owner: OWNER_ADDRESS, - multisigIsm: { - validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'], - threshold: 1, - }, - igp: { - beneficiary: OWNER_ADDRESS, - gasOracles: getGasOracles('test1'), - }, - }, - test2: { - owner: OWNER_ADDRESS, - multisigIsm: { - validators: ['0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'], - threshold: 1, - }, - igp: { - beneficiary: OWNER_ADDRESS, - gasOracles: getGasOracles('test2'), - }, - }, - test3: { - owner: OWNER_ADDRESS, - multisigIsm: { - validators: ['0x90f79bf6eb2c4f870365e785982e1f101e93b906'], - threshold: 1, - }, - igp: { - beneficiary: OWNER_ADDRESS, - gasOracles: getGasOracles('test3'), - }, - }, -}; +import { ChainMap, CoreConfig, objMap } from '@hyperlane-xyz/sdk'; + +import { multisigIsm } from './multisigIsm'; +import { owners } from './owners'; + +export const core: ChainMap = objMap(owners, (local, owner) => { + return { + owner, + multisigIsm: Object.fromEntries( + Object.entries(multisigIsm).filter(([chain]) => chain !== local), + ), + }; +}); diff --git a/typescript/infra/config/environments/test/igp.ts b/typescript/infra/config/environments/test/igp.ts new file mode 100644 index 000000000..cee100c8e --- /dev/null +++ b/typescript/infra/config/environments/test/igp.ts @@ -0,0 +1,42 @@ +import { + ChainMap, + GasOracleContractType, + OverheadIgpConfig, + multisigIsmVerificationCost, + objMap, +} from '@hyperlane-xyz/sdk'; +import { utils } from '@hyperlane-xyz/utils'; + +import { TestChains, chainNames } from './chains'; +import { multisigIsm } from './multisigIsm'; +import { owners } from './owners'; + +function getGasOracles(local: TestChains) { + return Object.fromEntries( + utils + .exclude(local, chainNames) + .map((name) => [name, GasOracleContractType.StorageGasOracle]), + ); +} + +export const igp: ChainMap = objMap( + owners, + (chain, owner) => { + return { + owner, + beneficiary: owner, + gasOracleType: getGasOracles(chain), + overhead: Object.fromEntries( + utils + .exclude(chain, chainNames) + .map((remote) => [ + remote, + multisigIsmVerificationCost( + multisigIsm[remote].threshold, + multisigIsm[remote].validators.length, + ), + ]), + ), + }; + }, +); diff --git a/typescript/infra/config/environments/test/index.ts b/typescript/infra/config/environments/test/index.ts index ee3b09662..3b7928602 100644 --- a/typescript/infra/config/environments/test/index.ts +++ b/typescript/infra/config/environments/test/index.ts @@ -8,13 +8,17 @@ import { agents } from './agent'; import { testConfigs } from './chains'; import { core } from './core'; import { storageGasOracleConfig } from './gas-oracle'; +import { igp } from './igp'; import { infra } from './infra'; +import { owners } from './owners'; export const environment: CoreEnvironmentConfig = { environment: 'test', chainMetadataConfigs: testConfigs, agents, core, + igp, + owners, infra, // NOTE: Does not work from hardhat.config.ts getMultiProvider: async () => { diff --git a/typescript/infra/config/environments/test/multisigIsm.ts b/typescript/infra/config/environments/test/multisigIsm.ts new file mode 100644 index 000000000..07f6aa930 --- /dev/null +++ b/typescript/infra/config/environments/test/multisigIsm.ts @@ -0,0 +1,17 @@ +import { ChainMap, MultisigIsmConfig } from '@hyperlane-xyz/sdk'; + +export const multisigIsm: ChainMap = { + // Validators are hardhat accounts 1-3 + test1: { + validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'], + threshold: 1, + }, + test2: { + validators: ['0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'], + threshold: 1, + }, + test3: { + validators: ['0x90f79bf6eb2c4f870365e785982e1f101e93b906'], + threshold: 1, + }, +}; diff --git a/typescript/infra/config/environments/test/owners.ts b/typescript/infra/config/environments/test/owners.ts new file mode 100644 index 000000000..8d2fe46c4 --- /dev/null +++ b/typescript/infra/config/environments/test/owners.ts @@ -0,0 +1,10 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { types } from '@hyperlane-xyz/utils'; + +import { chainNames } from './chains'; + +// Owner is hardhat account 0 +const OWNER_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; +export const owners: ChainMap = Object.fromEntries( + chainNames.map((chain) => [chain, OWNER_ADDRESS]), +); diff --git a/typescript/infra/config/environments/testnet3/agent.ts b/typescript/infra/config/environments/testnet3/agent.ts index 93ae6abc6..20996464c 100644 --- a/typescript/infra/config/environments/testnet3/agent.ts +++ b/typescript/infra/config/environments/testnet3/agent.ts @@ -1,11 +1,8 @@ -import { chainMetadata } from '@hyperlane-xyz/sdk'; +import { AgentConnectionType, chainMetadata } from '@hyperlane-xyz/sdk'; import { ALL_KEY_ROLES, KEY_ROLE_ENUM } from '../../../src/agents/roles'; import { AgentConfig } from '../../../src/config'; -import { - ConnectionType, - GasPaymentEnforcementPolicyType, -} from '../../../src/config/agent'; +import { GasPaymentEnforcementPolicyType } from '../../../src/config/agent'; import { Contexts } from '../../contexts'; import { helloworldMatchingList, routerMatchingList } from '../../utils'; @@ -36,7 +33,7 @@ export const hyperlane: AgentConfig = { }, environmentChainNames: chainNames, contextChainNames: chainNames, - connectionType: ConnectionType.HttpFallback, + connectionType: AgentConnectionType.HttpFallback, validators, relayer: { default: { @@ -81,7 +78,7 @@ export const releaseCandidate: AgentConfig = { }, environmentChainNames: chainNames, contextChainNames: chainNames, - connectionType: ConnectionType.HttpFallback, + connectionType: AgentConnectionType.HttpFallback, relayer: { default: { whitelist: releaseCandidateHelloworldMatchingList, diff --git a/typescript/infra/config/environments/testnet3/core.ts b/typescript/infra/config/environments/testnet3/core.ts index 06a2d95a7..fa5c7642d 100644 --- a/typescript/infra/config/environments/testnet3/core.ts +++ b/typescript/infra/config/environments/testnet3/core.ts @@ -1,155 +1,20 @@ import { ChainMap, CoreConfig, - GasOracleContractType, + defaultMultisigIsmConfigs, + objMap, } from '@hyperlane-xyz/sdk'; -import { TestnetChains, chainNames } from './chains'; +import { chainNames } from './chains'; +import { owners } from './owners'; -function getGasOracles(local: TestnetChains) { - return Object.fromEntries( - chainNames - .filter((name) => name !== local) - .map((name) => [name, GasOracleContractType.StorageGasOracle]), - ); -} - -const DEPLOYER_ADDRESS = '0xfaD1C94469700833717Fa8a3017278BC1cA8031C'; - -export const core: ChainMap = { - alfajores: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0xe6072396568e73ce6803b12b7e04164e839f1e54', - '0x9f177f51289b22515f41f95872e1511391b8e105', - '0x15f77400845eb1c971ad08de050861d5508cad6c', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('alfajores'), - }, - }, - fuji: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0x9fa19ead5ec76e437948b35e227511b106293c40', - '0x227e7d6507762ece0c94678f8c103eff9d682476', - '0x2379e43740e4aa4fde48cf4f00a3106df1d8420d', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('fuji'), - }, - }, - mumbai: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0x0a664ea799447da6b15645cf8b9e82072a68343f', - '0x6ae6f12929a960aba24ba74ea310e3d37d0ac045', - '0x51f70c047cd73bc7873273707501568857a619c4', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('mumbai'), - }, - }, - bsctestnet: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0x23338c8714976dd4a57eaeff17cbd26d7e275c08', - '0x85a618d7450ebc37e0d682371f08dac94eec7a76', - '0x95b76562e4ba1791a27ba4236801271c9115b141', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('bsctestnet'), - }, - }, - goerli: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0xf43fbd072fd38e1121d4b3b0b8a35116bbb01ea9', - '0xa33020552a21f35e75bd385c6ab95c3dfa82d930', - '0x0bba4043ff242f8bf3f39bafa8930a84d644d947', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('goerli'), - }, - }, - sepolia: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0xbc748ee311f5f2d1975d61cdf531755ce8ce3066', - '0xc4233b2bfe5aec08964a94b403052abb3eafcf07', - '0x6b36286c19f5c10bdc139ea9ee7f82287303f61d', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('sepolia'), - }, - }, - moonbasealpha: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0x890c2aeac157c3f067f3e42b8afc797939c59a32', - '0x1b06d6fe69b972ed7420c83599d5a5c0fc185904', - '0xe70b85206a968a99a597581f0fa09c99e7681093', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('moonbasealpha'), - }, - }, - optimismgoerli: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0xbb8d77eefbecc55db6e5a19b0fc3dc290776f189', - '0x69792508b4ddaa3ca52241ccfcd1e0b119a1ee65', - '0x11ddb46c6b653e0cdd7ad5bee32ae316e18f8453', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('optimismgoerli'), - }, - }, - arbitrumgoerli: { - owner: DEPLOYER_ADDRESS, - multisigIsm: { - threshold: 2, - validators: [ - '0xce798fa21e323f6b24d9838a10ffecdefdfc4f30', - '0xa792d39dca4426927e0f00c1618d61c9cb41779d', - '0xdf181fcc11dfac5d01467e4547101a856dd5aa04', - ], - }, - igp: { - beneficiary: DEPLOYER_ADDRESS, - gasOracles: getGasOracles('arbitrumgoerli'), - }, - }, -}; +export const core: ChainMap = objMap(owners, (local, owner) => { + return { + owner, + multisigIsm: Object.fromEntries( + Object.entries(defaultMultisigIsmConfigs).filter( + ([chain]) => chain !== local && chainNames.includes(chain), + ), + ), + }; +}); diff --git a/typescript/infra/config/environments/testnet3/funding.ts b/typescript/infra/config/environments/testnet3/funding.ts index f15c5281a..48d04645f 100644 --- a/typescript/infra/config/environments/testnet3/funding.ts +++ b/typescript/infra/config/environments/testnet3/funding.ts @@ -1,5 +1,6 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { KEY_ROLE_ENUM } from '../../../src/agents/roles'; -import { ConnectionType } from '../../../src/config/agent'; import { KeyFunderConfig } from '../../../src/config/funding'; import { Contexts } from '../../contexts'; @@ -22,5 +23,5 @@ export const keyFunderConfig: KeyFunderConfig = { [Contexts.Hyperlane]: [KEY_ROLE_ENUM.Relayer, KEY_ROLE_ENUM.Kathy], [Contexts.ReleaseCandidate]: [KEY_ROLE_ENUM.Relayer, KEY_ROLE_ENUM.Kathy], }, - connectionType: ConnectionType.Http, + connectionType: AgentConnectionType.Http, }; diff --git a/typescript/infra/config/environments/testnet3/helloworld.ts b/typescript/infra/config/environments/testnet3/helloworld.ts index 72add8a86..a44d01e2b 100644 --- a/typescript/infra/config/environments/testnet3/helloworld.ts +++ b/typescript/infra/config/environments/testnet3/helloworld.ts @@ -1,5 +1,6 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { HelloWorldConfig } from '../../../src/config'; -import { ConnectionType } from '../../../src/config/agent'; import { HelloWorldKathyRunMode } from '../../../src/config/helloworld'; import { Contexts } from '../../contexts'; @@ -23,7 +24,7 @@ export const hyperlane: HelloWorldConfig = { }, messageSendTimeout: 1000 * 60 * 8, // 8 min messageReceiptTimeout: 1000 * 60 * 20, // 20 min - connectionType: ConnectionType.HttpFallback, + connectionType: AgentConnectionType.HttpFallback, }, }; @@ -42,7 +43,7 @@ export const releaseCandidate: HelloWorldConfig = { }, messageSendTimeout: 1000 * 60 * 8, // 8 min messageReceiptTimeout: 1000 * 60 * 20, // 20 min - connectionType: ConnectionType.Http, + connectionType: AgentConnectionType.Http, }, }; diff --git a/typescript/infra/config/environments/testnet3/igp.ts b/typescript/infra/config/environments/testnet3/igp.ts new file mode 100644 index 000000000..315bb57cf --- /dev/null +++ b/typescript/infra/config/environments/testnet3/igp.ts @@ -0,0 +1,42 @@ +import { + ChainMap, + GasOracleContractType, + OverheadIgpConfig, + defaultMultisigIsmConfigs, + multisigIsmVerificationCost, + objMap, +} from '@hyperlane-xyz/sdk'; +import { utils } from '@hyperlane-xyz/utils'; + +import { TestnetChains, chainNames } from './chains'; +import { owners } from './owners'; + +function getGasOracles(local: TestnetChains) { + return Object.fromEntries( + utils + .exclude(local, chainNames) + .map((name) => [name, GasOracleContractType.StorageGasOracle]), + ); +} + +export const igp: ChainMap = objMap( + owners, + (chain, owner) => { + return { + owner, + beneficiary: owner, + gasOracleType: getGasOracles(chain), + overhead: Object.fromEntries( + utils + .exclude(chain, chainNames) + .map((remote) => [ + remote, + multisigIsmVerificationCost( + defaultMultisigIsmConfigs[remote].threshold, + defaultMultisigIsmConfigs[remote].validators.length, + ), + ]), + ), + }; + }, +); diff --git a/typescript/infra/config/environments/testnet3/index.ts b/typescript/infra/config/environments/testnet3/index.ts index 7598d7b58..3218d0084 100644 --- a/typescript/infra/config/environments/testnet3/index.ts +++ b/typescript/infra/config/environments/testnet3/index.ts @@ -1,7 +1,8 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { getMultiProviderForRole } from '../../../scripts/utils'; import { KEY_ROLE_ENUM } from '../../../src/agents/roles'; import { CoreEnvironmentConfig } from '../../../src/config'; -import { ConnectionType } from '../../../src/config/agent'; import { Contexts } from '../../contexts'; import { agents } from './agent'; @@ -10,8 +11,10 @@ import { core } from './core'; import { keyFunderConfig } from './funding'; import { storageGasOracleConfig } from './gas-oracle'; import { helloWorld } from './helloworld'; +import { igp } from './igp'; import { infrastructure } from './infrastructure'; import { liquidityLayerRelayerConfig } from './middleware'; +import { owners } from './owners'; export const environment: CoreEnvironmentConfig = { environment: environmentName, @@ -19,7 +22,7 @@ export const environment: CoreEnvironmentConfig = { getMultiProvider: ( context: Contexts = Contexts.Hyperlane, role: KEY_ROLE_ENUM = KEY_ROLE_ENUM.Deployer, - connectionType?: ConnectionType, + connectionType?: AgentConnectionType, ) => getMultiProviderForRole( testnetConfigs, @@ -31,8 +34,10 @@ export const environment: CoreEnvironmentConfig = { ), agents, core, + igp, infra: infrastructure, helloWorld, + owners, keyFunderConfig, liquidityLayerRelayerConfig, storageGasOracleConfig, diff --git a/typescript/infra/config/environments/testnet3/middleware.ts b/typescript/infra/config/environments/testnet3/middleware.ts index 1054fa2ef..213632442 100644 --- a/typescript/infra/config/environments/testnet3/middleware.ts +++ b/typescript/infra/config/environments/testnet3/middleware.ts @@ -1,4 +1,5 @@ -import { ConnectionType } from '../../../src/config/agent'; +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { LiquidityLayerRelayerConfig } from '../../../src/config/middleware'; import { environment } from './chains'; @@ -11,5 +12,5 @@ export const liquidityLayerRelayerConfig: LiquidityLayerRelayerConfig = { namespace: environment, prometheusPushGateway: 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091', - connectionType: ConnectionType.Http, + connectionType: AgentConnectionType.Http, }; diff --git a/typescript/infra/config/environments/testnet3/owners.ts b/typescript/infra/config/environments/testnet3/owners.ts new file mode 100644 index 000000000..6d8714e2b --- /dev/null +++ b/typescript/infra/config/environments/testnet3/owners.ts @@ -0,0 +1,10 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { types } from '@hyperlane-xyz/utils'; + +import { chainNames } from './chains'; + +const DEPLOYER_ADDRESS = '0xfaD1C94469700833717Fa8a3017278BC1cA8031C'; + +export const owners: ChainMap = Object.fromEntries( + chainNames.map((chain) => [chain, DEPLOYER_ADDRESS]), +); diff --git a/typescript/infra/fork.sh b/typescript/infra/fork.sh index 4329aad18..c3cf90395 100755 --- a/typescript/infra/fork.sh +++ b/typescript/infra/fork.sh @@ -1,7 +1,8 @@ ENVIRONMENT=$1 +MODULE=$2 if [ -z "$ENVIRONMENT" ]; then - echo "Usage: fork.sh " + echo "Usage: fork.sh " exit 1 fi @@ -24,16 +25,16 @@ while ! cast bn; do done echo "=== Run checker against forked $ENVIRONMENT ===" -DEBUG=hyperlane:* yarn ts-node ./scripts/check-deploy.ts -e $ENVIRONMENT --fork $FORK_CHAIN +DEBUG=hyperlane:* yarn ts-node ./scripts/check-deploy.ts -e $ENVIRONMENT -f $FORK_CHAIN -m $MODULE echo "=== Run core deployer against forked $ENVIRONMENT ===" -DEBUG=hyperlane:* yarn ts-node ./scripts/core.ts -e $ENVIRONMENT --fork $FORK_CHAIN +DEBUG=hyperlane:* yarn ts-node ./scripts/core.ts -e $ENVIRONMENT -f $FORK_CHAIN -m $MODULE echo "=== Run govern against forked $ENVIRONMENT ===" -DEBUG=hyperlane:* yarn ts-node ./scripts/govern.ts -e $ENVIRONMENT --fork $FORK_CHAIN +DEBUG=hyperlane:* yarn ts-node ./scripts/check-deploy.ts -e $ENVIRONMENT -f $FORK_CHAIN --govern -m $MODULE echo "=== Run checker against forked $ENVIRONMENT ===" -DEBUG=hyperlane:* yarn ts-node ./scripts/check-deploy.ts -e $ENVIRONMENT --fork $FORK_CHAIN +DEBUG=hyperlane:* yarn ts-node ./scripts/check-deploy.ts -e $ENVIRONMENT -f $FORK_CHAIN -m $MODULE SUCCESS=$? diff --git a/typescript/infra/hardhat.config.ts b/typescript/infra/hardhat.config.ts index b18721c9c..095dd06ab 100644 --- a/typescript/infra/hardhat.config.ts +++ b/typescript/infra/hardhat.config.ts @@ -4,7 +4,12 @@ import { task } from 'hardhat/config'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { TestSendReceiver__factory } from '@hyperlane-xyz/core'; -import { ChainName, HyperlaneCore, MultiProvider } from '@hyperlane-xyz/sdk'; +import { + ChainName, + HyperlaneCore, + HyperlaneIgp, + MultiProvider, +} from '@hyperlane-xyz/sdk'; import { sleep } from './src/utils/utils'; @@ -44,6 +49,7 @@ task('kathy', 'Dispatches random hyperlane messages') const [signer] = await hre.ethers.getSigners(); const multiProvider = MultiProvider.createTestMultiProvider({ signer }); const core = HyperlaneCore.fromEnvironment(environment, multiProvider); + const igps = HyperlaneIgp.fromEnvironment(environment, multiProvider); const randomElement = (list: T[]) => list[Math.floor(Math.random() * list.length)]; @@ -60,15 +66,14 @@ task('kathy', 'Dispatches random hyperlane messages') const local = core.chains()[0]; const remote: ChainName = randomElement(core.remoteChains(local)); const remoteId = multiProvider.getDomainId(remote); - const coreContracts = core.getContracts(local); - const mailbox = coreContracts.mailbox.contract; - const paymaster = coreContracts.interchainGasPaymaster; + const mailbox = core.getContracts(local).mailbox.contract; + const igp = igps.getContracts(local).interchainGasPaymaster.contract; // Send a batch of messages to the destination chain to test // the relayer submitting only greedily for (let i = 0; i < 10; i++) { await recipient.dispatchToSelf( mailbox.address, - paymaster.address, + igp.address, remoteId, '0x1234', { diff --git a/typescript/infra/scripts/announce-validators.ts b/typescript/infra/scripts/announce-validators.ts index de9afb3b3..3568ef7fc 100644 --- a/typescript/infra/scripts/announce-validators.ts +++ b/typescript/infra/scripts/announce-validators.ts @@ -14,7 +14,7 @@ import { assertContext } from '../src/utils/utils'; import { assertEnvironment, getContextAgentConfig, - getCoreEnvironmentConfig, + getEnvironmentConfig, } from './utils'; function getArgs() { @@ -45,7 +45,7 @@ function getArgs() { async function main() { const { environment, context, chain, location } = await getArgs(); - const config = getCoreEnvironmentConfig(environment); + const config = await getEnvironmentConfig(); const multiProvider = await config.getMultiProvider(); // environments union doesn't work well with typescript const core = HyperlaneCore.fromEnvironment( diff --git a/typescript/infra/scripts/check-deploy.ts b/typescript/infra/scripts/check-deploy.ts index b66ad376e..4233a22b8 100644 --- a/typescript/infra/scripts/check-deploy.ts +++ b/typescript/infra/scripts/check-deploy.ts @@ -1,57 +1,79 @@ -import { HyperlaneCore, HyperlaneCoreChecker } from '@hyperlane-xyz/sdk'; +import { + HyperlaneCore, + HyperlaneCoreChecker, + HyperlaneIgp, + HyperlaneIgpChecker, +} from '@hyperlane-xyz/sdk'; import { deployEnvToSdkEnv } from '../src/config/environment'; -import { useLocalProvider } from '../src/utils/fork'; +import { HyperlaneCoreGovernor } from '../src/core/govern'; +import { HyperlaneIgpGovernor } from '../src/gas/govern'; +import { HyperlaneAppGovernor } from '../src/govern/HyperlaneAppGovernor'; +import { impersonateAccount, useLocalProvider } from '../src/utils/fork'; -import { - assertEnvironment, - getArgsWithFork, - getCoreEnvironmentConfig, -} from './utils'; +import { getArgsWithModuleAndFork, getEnvironmentConfig } from './utils'; async function check() { - const argv = await getArgsWithFork().argv; - const environment = assertEnvironment(argv.environment); - const config = getCoreEnvironmentConfig(environment); + const { fork, govern, module, environment } = await getArgsWithModuleAndFork() + .boolean('govern') + .alias('g', 'govern').argv; + const config = await getEnvironmentConfig(); const multiProvider = await config.getMultiProvider(); // must rotate to forked provider before building core contracts - if (argv.fork) { - await useLocalProvider(multiProvider, argv.fork); + if (fork) { + await useLocalProvider(multiProvider, fork); + if (govern) { + const owner = config.core[fork].owner; + const signer = await impersonateAccount(owner); + multiProvider.setSigner(fork, signer); + } } - // environments union doesn't work well with typescript - const core = HyperlaneCore.fromEnvironment( - deployEnvToSdkEnv[environment], - multiProvider, - ); - const coreChecker = new HyperlaneCoreChecker( - multiProvider, - core, - config.core, - ); - - if (argv.fork) { - await coreChecker.checkChain(argv.fork); + let governor: HyperlaneAppGovernor; + const env = deployEnvToSdkEnv[environment]; + if (module === 'core') { + const core = HyperlaneCore.fromEnvironment(env, multiProvider); + const checker = new HyperlaneCoreChecker(multiProvider, core, config.core); + governor = new HyperlaneCoreGovernor(checker, config.owners); + } else if (module === 'igp') { + const igp = HyperlaneIgp.fromEnvironment(env, multiProvider); + const checker = new HyperlaneIgpChecker(multiProvider, igp, config.igp); + governor = new HyperlaneIgpGovernor(checker, config.owners); } else { - await coreChecker.check(); + throw new Error('Unknown module type'); } - if (coreChecker.violations.length > 0) { - console.table(coreChecker.violations, [ - 'chain', - 'remote', - 'name', - 'type', - 'subType', - 'actual', - 'expected', - ]); - throw new Error( - `Checking core deploy yielded ${coreChecker.violations.length} violations`, - ); + if (fork) { + await governor.checker.checkChain(fork); + if (govern) { + await governor.govern(false, fork); + } } else { - console.info('CoreChecker found no violations'); + await governor.checker.check(); + if (govern) { + await governor.govern(); + } + } + + if (!govern) { + const violations = governor.checker.violations; + if (violations.length > 0) { + console.table(violations, [ + 'chain', + 'remote', + 'name', + 'type', + 'subType', + 'actual', + 'expected', + ]); + throw new Error( + `Checking ${module} deploy yielded ${violations.length} violations`, + ); + } else { + console.info(`${module} Checker found no violations`); + } } } diff --git a/typescript/infra/scripts/core.ts b/typescript/infra/scripts/core.ts deleted file mode 100644 index 9847216f3..000000000 --- a/typescript/infra/scripts/core.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - buildContracts, - coreFactories, - serializeContracts, -} from '@hyperlane-xyz/sdk'; - -import { deployEnvToSdkEnv } from '../src/config/environment'; -import { HyperlaneCoreInfraDeployer } from '../src/core/deploy'; -import { impersonateAccount, useLocalProvider } from '../src/utils/fork'; -import { readJSON, writeJSON } from '../src/utils/utils'; - -import { - assertEnvironment, - getArgsWithFork, - getCoreContractsSdkFilepath, - getCoreEnvironmentConfig, - getCoreRustDirectory, - getCoreVerificationDirectory, -} from './utils'; - -async function main() { - const argv = await getArgsWithFork().argv; - const environment = assertEnvironment(argv.environment); - const config = getCoreEnvironmentConfig(environment); - const multiProvider = await config.getMultiProvider(); - - if (argv.fork) { - await useLocalProvider(multiProvider, argv.fork); - - // TODO: make this more generic - const deployerAddress = - environment === 'testnet3' - ? '0xfaD1C94469700833717Fa8a3017278BC1cA8031C' - : '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; - - const signer = await impersonateAccount(deployerAddress); - multiProvider.setSigner(argv.fork, signer); - } - - const deployer = new HyperlaneCoreInfraDeployer( - multiProvider, - config.core, - environment, - ); - - if (environment !== 'test') { - try { - const addresses = readJSON( - getCoreContractsSdkFilepath(), - `${deployEnvToSdkEnv[environment]}.json`, - ); - deployer.cacheContracts(buildContracts(addresses, coreFactories) as any); - } catch (e) { - console.info('Could not load partial core addresses, file may not exist'); - } - } - - if (argv.fork) { - await deployer.deployContracts(argv.fork, config.core[argv.fork]); - return; - } - - try { - await deployer.deploy(); - } catch (e) { - console.error(`Encountered error during deploy`); - console.error(e); - } - - // Persist artifacts, irrespective of deploy success - writeJSON( - getCoreContractsSdkFilepath(), - `${deployEnvToSdkEnv[environment]}.json`, - serializeContracts(deployer.deployedContracts), - ); - const verificationDir = getCoreVerificationDirectory(environment); - const verificationFile = 'verification.json'; - let existingVerificationInputs = []; - try { - existingVerificationInputs = readJSON(verificationDir, verificationFile); - } catch (err) { - /* ignore error */ - } - - writeJSON( - getCoreVerificationDirectory(environment), - 'verification.json', - deployer.mergeWithExistingVerificationInputs(existingVerificationInputs), - ); - - deployer.writeRustConfigs(getCoreRustDirectory()); -} - -main() - .then() - .catch(() => process.exit(1)); diff --git a/typescript/infra/scripts/deploy.ts b/typescript/infra/scripts/deploy.ts new file mode 100644 index 000000000..eb70423d3 --- /dev/null +++ b/typescript/infra/scripts/deploy.ts @@ -0,0 +1,138 @@ +import { + HyperlaneDeployer, + HyperlaneFactories, + buildAgentConfig, + buildContracts, + coreFactories, + serializeContracts, +} from '@hyperlane-xyz/sdk'; +import { igpFactories } from '@hyperlane-xyz/sdk/dist/gas/contracts'; + +import { deployEnvToSdkEnv } from '../src/config/environment'; +import { HyperlaneCoreInfraDeployer } from '../src/core/deploy'; +import { HyperlaneIgpInfraDeployer } from '../src/gas/deploy'; +import { impersonateAccount, useLocalProvider } from '../src/utils/fork'; +import { readJSON, writeJSON, writeMergedJSON } from '../src/utils/utils'; + +import { + getAgentConfigDirectory, + getArgsWithModuleAndFork, + getContractAddressesSdkFilepath, + getEnvironmentConfig, + getVerificationDirectory, +} from './utils'; + +async function main() { + const { module, fork, environment } = await getArgsWithModuleAndFork().argv; + const config = await getEnvironmentConfig(); + const multiProvider = await config.getMultiProvider(); + + if (fork) { + await useLocalProvider(multiProvider, fork); + + // TODO: make this more generic + const deployerAddress = + environment === 'testnet3' + ? '0xfaD1C94469700833717Fa8a3017278BC1cA8031C' + : '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; + + const signer = await impersonateAccount(deployerAddress); + multiProvider.setSigner(fork, signer); + } + + // Write agent config indexing from the latest block numbers. + // For non-net-new deployments, these changes will need to be + // reverted manually. + const chains = multiProvider.getKnownChainNames(); + const startBlocks = Object.fromEntries( + await Promise.all( + chains.map(async (c) => [ + c, + await multiProvider.getProvider(c).getBlockNumber, + ]), + ), + ); + + let factories: HyperlaneFactories; + let deployer: HyperlaneDeployer; + if (module === 'core') { + factories = coreFactories; + deployer = new HyperlaneCoreInfraDeployer( + multiProvider, + config.core, + environment, + ); + } else if (module === 'igp') { + factories = igpFactories; + deployer = new HyperlaneIgpInfraDeployer( + multiProvider, + config.igp, + environment, + ); + } else { + throw new Error('Unknown module type'); + } + + if (environment !== 'test') { + try { + const addresses = readJSON( + getContractAddressesSdkFilepath(), + `${deployEnvToSdkEnv[environment]}.json`, + ); + deployer.cacheContracts(buildContracts(addresses, factories) as any); + } catch (e) { + console.info('Could not load partial addresses, file may not exist'); + } + } + + if (fork) { + await deployer.deployContracts(fork, config.core[fork]); + return; + } + + try { + await deployer.deploy(); + } catch (e) { + console.error('Encountered error during deploy', e); + } + + // Persist address artifacts, irrespective of deploy success + writeMergedJSON( + getContractAddressesSdkFilepath(), + `${deployEnvToSdkEnv[environment]}.json`, + serializeContracts(deployer.deployedContracts), + ); + const verificationDir = getVerificationDirectory(environment, module); + const verificationFile = 'verification.json'; + let existingVerificationInputs = []; + try { + existingVerificationInputs = readJSON(verificationDir, verificationFile); + } catch (err) { + /* ignore error */ + } + + writeJSON( + verificationDir, + verificationFile, + deployer.mergeWithExistingVerificationInputs(existingVerificationInputs), + ); + + const sdkEnv = deployEnvToSdkEnv[environment]; + const addresses = readJSON( + getContractAddressesSdkFilepath(), + `${sdkEnv}.json`, + ); + + const agentConfig = await buildAgentConfig( + multiProvider.getKnownChainNames(), + multiProvider, + addresses, + startBlocks, + ); + + writeJSON(getAgentConfigDirectory(), `${sdkEnv}_config.json`, agentConfig); +} + +main() + .then() + .catch(() => process.exit(1)); diff --git a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts index a2632e8e4..a1cdf27a7 100644 --- a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts +++ b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts @@ -4,11 +4,12 @@ import { Gauge, Registry } from 'prom-client'; import { format } from 'util'; import { + AgentConnectionType, AllChains, ChainName, Chains, CoreChainName, - HyperlaneCore, + HyperlaneIgp, MultiProvider, } from '@hyperlane-xyz/sdk'; import { ChainMap } from '@hyperlane-xyz/sdk/dist/types'; @@ -23,7 +24,6 @@ import { } from '../../src/agents/keys'; import { KEY_ROLE_ENUM } from '../../src/agents/roles'; import { DeployEnvironment } from '../../src/config'; -import { ConnectionType } from '../../src/config/agent'; import { deployEnvToSdkEnv } from '../../src/config/environment'; import { ContextAndRoles, ContextAndRolesMap } from '../../src/config/funding'; import { submitMetrics } from '../../src/utils/metrics'; @@ -197,10 +197,10 @@ async function main() { .string('connection-type') .describe('connection-type', 'The provider connection type to use for RPCs') - .default('connection-type', ConnectionType.Http) + .default('connection-type', AgentConnectionType.Http) .choices('connection-type', [ - ConnectionType.Http, - ConnectionType.HttpQuorum, + AgentConnectionType.Http, + AgentConnectionType.HttpQuorum, ]) .demandOption('connection-type') @@ -260,7 +260,7 @@ async function main() { // Funds keys for a single context class ContextFunder { public readonly chains: ChainName[]; - core: HyperlaneCore; + igp: HyperlaneIgp; constructor( public readonly environment: DeployEnvironment, @@ -275,7 +275,7 @@ class ContextFunder { ); this.chains = Array.from(uniqueChains) as ChainName[]; - this.core = HyperlaneCore.fromEnvironment( + this.igp = HyperlaneIgp.fromEnvironment( deployEnvToSdkEnv[this.environment], multiProvider, ); @@ -497,7 +497,7 @@ class ContextFunder { const igpClaimThreshold = ethers.utils.parseEther(igpClaimThresholdEther); const provider = this.multiProvider.getProvider(chain); - const igp = this.core.getContracts(chain).interchainGasPaymaster; + const igp = this.igp.getContracts(chain).interchainGasPaymaster; const igpBalance = await provider.getBalance(igp.address); log('Checking IGP balance', { diff --git a/typescript/infra/scripts/funding/reclaim-from-igp.ts b/typescript/infra/scripts/funding/reclaim-from-igp.ts index 31cdf6daa..02b520dc3 100644 --- a/typescript/infra/scripts/funding/reclaim-from-igp.ts +++ b/typescript/infra/scripts/funding/reclaim-from-igp.ts @@ -1,6 +1,6 @@ import { BigNumber } from 'ethers'; -import { HyperlaneCore, objMap, promiseObjAll } from '@hyperlane-xyz/sdk'; +import { HyperlaneIgp, objMap, promiseObjAll } from '@hyperlane-xyz/sdk'; import { deployEnvToSdkEnv } from '../../src/config/environment'; import { getEnvironment, getEnvironmentConfig } from '../utils'; @@ -10,14 +10,14 @@ const RECLAIM_BALANCE_THRESHOLD = BigNumber.from(10).pow(17); async function main() { const environment = await getEnvironment(); - const coreConfig = await getEnvironmentConfig(); - const multiProvider = await coreConfig.getMultiProvider(); - const core: HyperlaneCore = HyperlaneCore.fromEnvironment( + const environmentConfig = await getEnvironmentConfig(); + const multiProvider = await environmentConfig.getMultiProvider(); + const igp = HyperlaneIgp.fromEnvironment( deployEnvToSdkEnv[environment], multiProvider, ); - const paymasters = core.map( + const paymasters = igp.map( (_, contracts) => contracts.interchainGasPaymaster, ); diff --git a/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts b/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts index 01376c881..c206c43b7 100644 --- a/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts +++ b/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts @@ -1,4 +1,4 @@ -import { ChainName, HyperlaneCore } from '@hyperlane-xyz/sdk'; +import { ChainName, HyperlaneIgp } from '@hyperlane-xyz/sdk'; import { RemoteGasData, StorageGasOracleConfig } from '../../src/config'; import { deployEnvToSdkEnv } from '../../src/config/environment'; @@ -32,14 +32,14 @@ async function main() { throw Error(`No storage gas oracle config for environment ${environment}`); } - const core = HyperlaneCore.fromEnvironment( + const igp = HyperlaneIgp.fromEnvironment( deployEnvToSdkEnv[environment], multiProvider, ); - for (const chain of core.chains()) { + for (const chain of igp.chains()) { await setStorageGasOracleValues( - core, + igp, storageGasOracleConfig[chain], chain, args.dryRun, @@ -49,19 +49,19 @@ async function main() { } async function setStorageGasOracleValues( - core: HyperlaneCore, + igp: HyperlaneIgp, localStorageGasOracleConfig: StorageGasOracleConfig, local: ChainName, dryRun: boolean, ) { console.log(`Setting remote gas data on local chain ${local}...`); - const storageGasOracle = core.getContracts(local).storageGasOracle; + const storageGasOracle = igp.getContracts(local).storageGasOracle; const configsToSet: RemoteGasDataConfig[] = []; for (const remote in localStorageGasOracleConfig) { const desiredGasData = localStorageGasOracleConfig[remote]!; - const remoteId = core.multiProvider.getDomainId(remote); + const remoteId = igp.multiProvider.getDomainId(remote); const existingGasData: RemoteGasData = await storageGasOracle.remoteGasData( remoteId, @@ -92,14 +92,14 @@ async function setStorageGasOracleValues( console.log(`Updating ${configsToSet.length} configs on local ${local}:`); console.log( configsToSet - .map((config) => prettyRemoteGasDataConfig(core.multiProvider, config)) + .map((config) => prettyRemoteGasDataConfig(igp.multiProvider, config)) .join('\n\t--\n'), ); if (dryRun) { console.log('Running in dry run mode, not sending tx'); } else { - await core.multiProvider.handleTx( + await igp.multiProvider.handleTx( local, storageGasOracle.setRemoteGasDataConfigs(configsToSet), ); diff --git a/typescript/infra/scripts/govern.ts b/typescript/infra/scripts/govern.ts deleted file mode 100644 index 3e308b7a6..000000000 --- a/typescript/infra/scripts/govern.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { HyperlaneCore, HyperlaneCoreChecker } from '@hyperlane-xyz/sdk'; - -import { deployEnvToSdkEnv } from '../src/config/environment'; -import { HyperlaneCoreGovernor } from '../src/core/govern'; -import { impersonateAccount, useLocalProvider } from '../src/utils/fork'; - -import { - assertEnvironment, - getArgsWithFork, - getCoreEnvironmentConfig, -} from './utils'; - -async function check() { - const argv = await getArgsWithFork().argv; - const environment = assertEnvironment(argv.environment); - const config = getCoreEnvironmentConfig(environment); - const multiProvider = await config.getMultiProvider(); - - // must rotate to forked provider before building core contracts - if (argv.fork) { - await useLocalProvider(multiProvider, argv.fork); - const owner = config.core[argv.fork].owner; - const signer = await impersonateAccount(owner); - multiProvider.setSigner(argv.fork, signer); - } - - const core = HyperlaneCore.fromEnvironment( - deployEnvToSdkEnv[environment], - multiProvider, - ); - - const coreChecker = new HyperlaneCoreChecker( - multiProvider, - core, - config.core, - ); - const governor = new HyperlaneCoreGovernor(coreChecker); - - if (argv.fork) { - await coreChecker.checkChain(argv.fork); - await governor.governChain(argv.fork, false); - } else { - await coreChecker.check(); - await governor.govern(); - } -} - -check() - .then() - .catch(() => process.exit(1)); diff --git a/typescript/infra/scripts/helloworld/check.ts b/typescript/infra/scripts/helloworld/check.ts index 21197cb11..8efeb407b 100644 --- a/typescript/infra/scripts/helloworld/check.ts +++ b/typescript/infra/scripts/helloworld/check.ts @@ -2,9 +2,14 @@ import { HelloWorldChecker } from '@hyperlane-xyz/helloworld'; import { Contexts } from '../../config/contexts'; import { KEY_ROLE_ENUM } from '../../src/agents/roles'; -import { getContext, getCoreEnvironmentConfig, getEnvironment } from '../utils'; +import { + getContext, + getCoreEnvironmentConfig, + getEnvironment, + getRouterConfig, +} from '../utils'; -import { getApp, getConfiguration } from './utils'; +import { getApp } from './utils'; async function main() { const environment = await getEnvironment(); @@ -17,7 +22,7 @@ async function main() { KEY_ROLE_ENUM.Deployer, Contexts.Hyperlane, // Owner should always be from the hyperlane context ); - const configMap = await getConfiguration(environment, multiProvider); + const configMap = await getRouterConfig(environment, multiProvider, true); const checker = new HelloWorldChecker(multiProvider, app, configMap); await checker.check(); checker.expectEmpty(); diff --git a/typescript/infra/scripts/helloworld/deploy.ts b/typescript/infra/scripts/helloworld/deploy.ts index 7a94d8112..55580868d 100644 --- a/typescript/infra/scripts/helloworld/deploy.ts +++ b/typescript/infra/scripts/helloworld/deploy.ts @@ -21,10 +21,9 @@ import { getCoreEnvironmentConfig, getEnvironment, getEnvironmentDirectory, + getRouterConfig, } from '../utils'; -import { getConfiguration } from './utils'; - async function main() { const environment = await getEnvironment(); const context = await getContext(); @@ -34,7 +33,7 @@ async function main() { Contexts.Hyperlane, KEY_ROLE_ENUM.Deployer, ); - const configMap = await getConfiguration(environment, multiProvider); + const configMap = await getRouterConfig(environment, multiProvider, true); const core = HyperlaneCore.fromEnvironment( deployEnvToSdkEnv[environment], multiProvider, diff --git a/typescript/infra/scripts/helloworld/kathy.ts b/typescript/infra/scripts/helloworld/kathy.ts index d720ec581..249638438 100644 --- a/typescript/infra/scripts/helloworld/kathy.ts +++ b/typescript/infra/scripts/helloworld/kathy.ts @@ -4,15 +4,15 @@ import { format } from 'util'; import { HelloWorldApp } from '@hyperlane-xyz/helloworld'; import { + AgentConnectionType, ChainName, DispatchedMessage, HyperlaneCore, - InterchainGasCalculator, + HyperlaneIgp, } from '@hyperlane-xyz/sdk'; import { debug, error, log, utils, warn } from '@hyperlane-xyz/utils'; import { KEY_ROLE_ENUM } from '../../src/agents/roles'; -import { ConnectionType } from '../../src/config/agent'; import { deployEnvToSdkEnv } from '../../src/config/environment'; import { startMetricsServer } from '../../src/utils/metrics'; import { assertChain, diagonalize, sleep } from '../../src/utils/utils'; @@ -104,11 +104,11 @@ function getKathyArgs() { .string('connection-type') .describe('connection-type', 'The provider connection type to use for RPCs') - .default('connection-type', ConnectionType.Http) + .default('connection-type', AgentConnectionType.Http) .choices('connection-type', [ - ConnectionType.Http, - ConnectionType.HttpQuorum, - ConnectionType.HttpFallback, + AgentConnectionType.Http, + AgentConnectionType.HttpQuorum, + AgentConnectionType.HttpFallback, ]) .demandOption('connection-type'); @@ -150,8 +150,8 @@ async function main(): Promise { undefined, connectionType, ); - const gasCalculator = InterchainGasCalculator.fromEnvironment( - deployEnvToSdkEnv[environment], + const igp = HyperlaneIgp.fromEnvironment( + deployEnvToSdkEnv[coreConfig.environment], app.multiProvider, ); const appChains = app.chains(); @@ -323,9 +323,9 @@ async function main(): Promise { try { await sendMessage( app, + igp, origin, destination, - gasCalculator, messageSendTimeout, messageReceiptTimeout, ); @@ -356,19 +356,19 @@ async function main(): Promise { async function sendMessage( app: HelloWorldApp, + igp: HyperlaneIgp, origin: ChainName, destination: ChainName, - gasCalc: InterchainGasCalculator, messageSendTimeout: number, messageReceiptTimeout: number, ) { const startTime = Date.now(); const msg = 'Hello!'; - const expectedHandleGas = BigNumber.from(100_000); + const expectedHandleGas = BigNumber.from(50_000); const value = await utils.retryAsync( () => - gasCalc.quoteGasPaymentForDefaultIsmIgp( + igp.quoteGasPaymentForDefaultIsmIgp( origin, destination, expectedHandleGas, diff --git a/typescript/infra/scripts/helloworld/utils.ts b/typescript/infra/scripts/helloworld/utils.ts index e6e20f596..458f5f9b6 100644 --- a/typescript/infra/scripts/helloworld/utils.ts +++ b/typescript/infra/scripts/helloworld/utils.ts @@ -4,45 +4,25 @@ import { helloWorldFactories, } from '@hyperlane-xyz/helloworld'; import { + AgentConnectionType, ChainMap, HyperlaneCore, MultiProvider, - RouterConfig, buildContracts, } from '@hyperlane-xyz/sdk'; import { Contexts } from '../../config/contexts'; import { KEY_ROLE_ENUM } from '../../src/agents/roles'; -import { CoreEnvironmentConfig, DeployEnvironment } from '../../src/config'; -import { ConnectionType } from '../../src/config/agent'; +import { CoreEnvironmentConfig } from '../../src/config'; import { deployEnvToSdkEnv } from '../../src/config/environment'; import { HelloWorldConfig } from '../../src/config/helloworld'; -export async function getConfiguration( - environment: DeployEnvironment, - multiProvider: MultiProvider, -): Promise> { - const ownerMap: ChainMap<{ owner: string }> = {}; - for (const chain of multiProvider.getKnownChainNames()) { - ownerMap[chain] = { - owner: await multiProvider.getSignerAddress(chain), - }; - } - - const core: HyperlaneCore = HyperlaneCore.fromEnvironment( - deployEnvToSdkEnv[environment], - multiProvider, - ); - - return core.extendWithConnectionClientConfig(ownerMap); -} - export async function getApp( coreConfig: CoreEnvironmentConfig, context: Contexts, keyRole: KEY_ROLE_ENUM, keyContext: Contexts = context, - connectionType: ConnectionType = ConnectionType.Http, + connectionType: AgentConnectionType = AgentConnectionType.Http, ) { const helloworldConfig = getHelloWorldConfig(coreConfig, context); const contracts = buildContracts( diff --git a/typescript/infra/scripts/list-validator-checkpoint-indices.ts b/typescript/infra/scripts/list-validator-checkpoint-indices.ts index c37c2b225..e56eeddec 100644 --- a/typescript/infra/scripts/list-validator-checkpoint-indices.ts +++ b/typescript/infra/scripts/list-validator-checkpoint-indices.ts @@ -4,7 +4,11 @@ import { S3Validator } from '../src/agents/aws/validator'; import { deployEnvToSdkEnv } from '../src/config/environment'; import { concurrentMap } from '../src/utils/utils'; -import { getCoreEnvironmentConfig, getEnvironment } from './utils'; +import { + getCoreEnvironmentConfig, + getEnvironment, + getValidatorsByChain, +} from './utils'; async function main() { const environment = await getEnvironment(); @@ -15,8 +19,8 @@ async function main() { multiProvider, ); - const validators = Object.entries(config.core).flatMap(([chain, set]) => - set.multisigIsm.validators.map((validator) => ({ chain, validator })), + const validators = Object.entries(getValidatorsByChain(config.core)).flatMap( + ([chain, set]) => [...set].map((validator) => ({ chain, validator })), ); const indices = await concurrentMap( diff --git a/typescript/infra/scripts/merge-sdk-contract-addresses.ts b/typescript/infra/scripts/merge-sdk-contract-addresses.ts index c42c25bde..2e756dfe2 100644 --- a/typescript/infra/scripts/merge-sdk-contract-addresses.ts +++ b/typescript/infra/scripts/merge-sdk-contract-addresses.ts @@ -9,7 +9,7 @@ import { deployEnvToSdkEnv } from '../src/config/environment'; import { readJSON, writeJSON } from '../src/utils/utils'; import { - getCoreContractsSdkFilepath, + getContractAddressesSdkFilepath, getEnvironment, getEnvironmentDirectory, } from './utils'; @@ -29,7 +29,7 @@ export function mergeWithSdkContractAddressArtifacts( ) { const sdkEnvironment = deployEnvToSdkEnv[environment]; const coreAddresses: HyperlaneAddresses = readJSON( - getCoreContractsSdkFilepath(), + getContractAddressesSdkFilepath(), `${sdkEnvironment}.json`, ); @@ -55,7 +55,7 @@ export function mergeWithSdkContractAddressArtifacts( } writeJSON( - getCoreContractsSdkFilepath(), + getContractAddressesSdkFilepath(), `${sdkEnvironment}.json`, coreAddresses, ); diff --git a/typescript/infra/scripts/middleware/deploy-accounts.ts b/typescript/infra/scripts/middleware/deploy-accounts.ts index 0be28384b..31625a190 100644 --- a/typescript/infra/scripts/middleware/deploy-accounts.ts +++ b/typescript/infra/scripts/middleware/deploy-accounts.ts @@ -6,12 +6,12 @@ import { } from '@hyperlane-xyz/sdk'; import { deployWithArtifacts } from '../../src/deploy'; -import { getConfiguration } from '../helloworld/utils'; import { mergeWithSdkContractAddressArtifacts } from '../merge-sdk-contract-addresses'; import { getCoreEnvironmentConfig, getEnvironment, getEnvironmentDirectory, + getRouterConfig, } from '../utils'; // similar to hello world deploy script but uses freshly funded account for consistent addresses across chains @@ -26,7 +26,7 @@ async function main() { ); // config gcp deployer key as owner - const configMap = await getConfiguration(environment, multiProvider); + const configMap = await getRouterConfig(environment, multiProvider); const deployer = new InterchainAccountDeployer( multiProvider, diff --git a/typescript/infra/scripts/middleware/deploy-liquidity-layer.ts b/typescript/infra/scripts/middleware/deploy-liquidity-layer.ts index e704d9444..0f20822e8 100644 --- a/typescript/infra/scripts/middleware/deploy-liquidity-layer.ts +++ b/typescript/infra/scripts/middleware/deploy-liquidity-layer.ts @@ -8,11 +8,11 @@ import { import { bridgeAdapterConfigs } from '../../config/environments/testnet3/token-bridge'; import { deployWithArtifacts } from '../../src/deploy'; -import { getConfiguration } from '../helloworld/utils'; import { getCoreEnvironmentConfig, getEnvironment, getEnvironmentDirectory, + getRouterConfig, } from '../utils'; async function main() { @@ -26,10 +26,10 @@ async function main() { ); // config gcp deployer key as owner - const ownerConfigMap = await getConfiguration(environment, multiProvider); + const routerConfig = await getRouterConfig(environment, multiProvider); const config = objMap(bridgeAdapterConfigs, (chain, conf) => ({ ...conf, - ...ownerConfigMap[chain], + ...routerConfig[chain], })); const deployer = new LiquidityLayerDeployer( multiProvider, diff --git a/typescript/infra/scripts/middleware/deploy-queries.ts b/typescript/infra/scripts/middleware/deploy-queries.ts index f2f7cf37a..57759e170 100644 --- a/typescript/infra/scripts/middleware/deploy-queries.ts +++ b/typescript/infra/scripts/middleware/deploy-queries.ts @@ -6,12 +6,12 @@ import { } from '@hyperlane-xyz/sdk'; import { deployWithArtifacts } from '../../src/deploy'; -import { getConfiguration } from '../helloworld/utils'; import { mergeWithSdkContractAddressArtifacts } from '../merge-sdk-contract-addresses'; import { getCoreEnvironmentConfig, getEnvironment, getEnvironmentDirectory, + getRouterConfig, } from '../utils'; async function main() { @@ -25,7 +25,7 @@ async function main() { ); // config gcp deployer key as owner - const configMap = await getConfiguration(environment, multiProvider); + const configMap = await getRouterConfig(environment, multiProvider); const deployer = new InterchainQueryDeployer( multiProvider, diff --git a/typescript/infra/scripts/testcontracts/deploy-testquerysender.ts b/typescript/infra/scripts/testcontracts/deploy-testquerysender.ts index 45e0e0d9a..181de3c3f 100644 --- a/typescript/infra/scripts/testcontracts/deploy-testquerysender.ts +++ b/typescript/infra/scripts/testcontracts/deploy-testquerysender.ts @@ -1,6 +1,6 @@ import path from 'path'; -import { HyperlaneCore, objMap } from '@hyperlane-xyz/sdk'; +import { HyperlaneIgp, objMap } from '@hyperlane-xyz/sdk'; import { deployEnvToSdkEnv } from '../../src/config/environment'; import { deployWithArtifacts } from '../../src/deploy'; @@ -19,7 +19,7 @@ async function main() { const environment = await getEnvironment(); const coreConfig = getCoreEnvironmentConfig(environment); const multiProvider = await coreConfig.getMultiProvider(); - const core = HyperlaneCore.fromEnvironment( + const igp = HyperlaneIgp.fromEnvironment( deployEnvToSdkEnv[environment], multiProvider, ); @@ -36,7 +36,7 @@ async function main() { const deployer = new TestQuerySenderDeployer( multiProvider, queryRouterAddresses, - core, + igp, ); const dir = path.join( diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index e0e3df89c..97ecb1ad4 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -2,11 +2,16 @@ import path from 'path'; import yargs from 'yargs'; import { + AgentConnectionType, AllChains, ChainMap, ChainMetadata, ChainName, + CoreConfig, + HyperlaneCore, + HyperlaneIgp, MultiProvider, + RouterConfig, objMap, promiseObjAll, } from '@hyperlane-xyz/sdk'; @@ -18,9 +23,8 @@ import { getCloudAgentKey } from '../src/agents/key-utils'; import { CloudAgentKey } from '../src/agents/keys'; import { KEY_ROLE_ENUM } from '../src/agents/roles'; import { CoreEnvironmentConfig, DeployEnvironment } from '../src/config'; -import { ConnectionType } from '../src/config/agent'; import { fetchProvider } from '../src/config/chain'; -import { EnvironmentNames } from '../src/config/environment'; +import { EnvironmentNames, deployEnvToSdkEnv } from '../src/config/environment'; import { assertContext } from '../src/utils/utils'; export function getArgsWithContext() { @@ -31,8 +35,16 @@ export function getArgsWithContext() { .alias('c', 'context'); } -export function getArgsWithFork() { +export function getArgsWithModule() { return getArgs() + .string('module') + .choices('module', ['core', 'igp']) + .demandOption('module') + .alias('m', 'module'); +} + +export function getArgsWithModuleAndFork() { + return getArgsWithModule() .string('fork') .describe('fork', 'network to fork') .alias('f', 'fork'); @@ -60,10 +72,8 @@ export function assertEnvironment(env: string): DeployEnvironment { ); } -export function getCoreEnvironmentConfig( - env: Env, -): CoreEnvironmentConfig { - return environments[env]; +export function getCoreEnvironmentConfig(environment: DeployEnvironment) { + return environments[environment]; } export async function getEnvironment() { @@ -117,8 +127,8 @@ async function getKeyForRole( role: KEY_ROLE_ENUM, index?: number, ): Promise { - const coreConfig = getCoreEnvironmentConfig(environment); - const agentConfig = await getAgentConfig(context, coreConfig); + const environmentConfig = environments[environment]; + const agentConfig = await getAgentConfig(context, environmentConfig); return getCloudAgentKey(agentConfig, role, chain, index); } @@ -128,7 +138,7 @@ export async function getMultiProviderForRole( context: Contexts, role: KEY_ROLE_ENUM, index?: number, - connectionType?: ConnectionType, + connectionType?: AgentConnectionType, ): Promise { if (process.env.CI === 'true') { return new MultiProvider(); // use default RPCs @@ -147,7 +157,7 @@ export async function getMultiProviderForRole( return multiProvider; } -export function getCoreContractsSdkFilepath() { +export function getContractAddressesSdkFilepath() { return path.join('../sdk/src/consts/environments'); } @@ -155,15 +165,21 @@ export function getEnvironmentDirectory(environment: DeployEnvironment) { return path.join('./config/environments/', environment); } -export function getCoreDirectory(environment: DeployEnvironment) { - return path.join(getEnvironmentDirectory(environment), 'core'); +export function getModuleDirectory( + environment: DeployEnvironment, + module: string, +) { + return path.join(getEnvironmentDirectory(environment), module); } -export function getCoreVerificationDirectory(environment: DeployEnvironment) { - return path.join(getCoreDirectory(environment), 'verification'); +export function getVerificationDirectory( + environment: DeployEnvironment, + module: string, +) { + return path.join(getModuleDirectory(environment, module), 'verification'); } -export function getCoreRustDirectory() { +export function getAgentConfigDirectory() { return path.join('../../', 'rust', 'config'); } @@ -198,3 +214,47 @@ export async function assertCorrectKubeContext( process.exit(1); } } + +export async function getRouterConfig( + environment: DeployEnvironment, + multiProvider: MultiProvider, + useMultiProviderOwners = false, +): Promise> { + const core = HyperlaneCore.fromEnvironment( + deployEnvToSdkEnv[environment], + multiProvider, + ); + const igp = HyperlaneIgp.fromEnvironment( + deployEnvToSdkEnv[environment], + multiProvider, + ); + const owners = getCoreEnvironmentConfig(environment).owners; + const config: ChainMap = {}; + for (const chain of multiProvider.getKnownChainNames()) { + config[chain] = { + owner: useMultiProviderOwners + ? await multiProvider.getSignerAddress(chain) + : owners[chain], + mailbox: core.getContracts(chain).mailbox.address, + interchainGasPaymaster: + igp.getContracts(chain).defaultIsmInterchainGasPaymaster.address, + }; + } + return config; +} + +export function getValidatorsByChain( + config: ChainMap, +): ChainMap> { + const validators: ChainMap> = {}; + objMap(config, (local, coreConfig) => { + objMap(coreConfig.multisigIsm, (remote, multisigIsmConfig) => { + if (!validators[remote]) { + validators[remote] = new Set(multisigIsmConfig.validators); + } else { + multisigIsmConfig.validators.map((v) => validators[remote].add(v)); + } + }); + }); + return validators; +} diff --git a/typescript/infra/scripts/verify-validators.ts b/typescript/infra/scripts/verify-validators.ts index 1e50cff55..801a76cb0 100644 --- a/typescript/infra/scripts/verify-validators.ts +++ b/typescript/infra/scripts/verify-validators.ts @@ -3,7 +3,11 @@ import { HyperlaneCore, objMap } from '@hyperlane-xyz/sdk'; import { CheckpointStatus, S3Validator } from '../src/agents/aws/validator'; import { deployEnvToSdkEnv } from '../src/config/environment'; -import { getCoreEnvironmentConfig, getEnvironment } from './utils'; +import { + getCoreEnvironmentConfig, + getEnvironment, + getValidatorsByChain, +} from './utils'; async function main() { const environment = await getEnvironment(); @@ -14,14 +18,12 @@ async function main() { multiProvider, ); - objMap(config.core, async (chain, coreConfig) => { + objMap(getValidatorsByChain(config.core), async (chain, set) => { const validatorAnnounce = core.getContracts(chain).validatorAnnounce; const storageLocations = - await validatorAnnounce.getAnnouncedStorageLocations( - coreConfig.multisigIsm.validators, - ); + await validatorAnnounce.getAnnouncedStorageLocations([...set]); const validators = await Promise.all( - coreConfig.multisigIsm.validators.map((validator, i) => { + [...set].map((validator, i) => { // Only use the latest announcement for now if (storageLocations[i].length != 1) { throw new Error('Only support single announcement'); diff --git a/typescript/infra/src/agents/index.ts b/typescript/infra/src/agents/index.ts index 27042f23b..f0d13788e 100644 --- a/typescript/infra/src/agents/index.ts +++ b/typescript/infra/src/agents/index.ts @@ -1,14 +1,10 @@ -import { ChainName } from '@hyperlane-xyz/sdk'; +import { AgentConnectionType, ChainName } from '@hyperlane-xyz/sdk'; import { utils } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; import { AgentConfig, DeployEnvironment } from '../config'; -import { - ChainAgentConfig, - CheckpointSyncerType, - ConnectionType, - TransactionSubmissionType, -} from '../config/agent'; +import { ChainAgentConfig, CheckpointSyncerType } from '../config/agent'; +import { TransactionSubmissionType } from '../config/agent'; import { fetchGCPSecret } from '../utils/gcloud'; import { HelmCommand, @@ -41,7 +37,7 @@ async function helmValuesForChain( let baseConnectionConfig: Record = { type: agentConfig.connectionType, }; - if (baseConnectionConfig.type == ConnectionType.HttpQuorum) { + if (baseConnectionConfig.type == AgentConnectionType.HttpQuorum) { baseConnectionConfig = { ...baseConnectionConfig, urls: '', diff --git a/typescript/infra/src/config/agent.ts b/typescript/infra/src/config/agent.ts index 53dddfce8..85129abb0 100644 --- a/typescript/infra/src/config/agent.ts +++ b/typescript/infra/src/config/agent.ts @@ -1,7 +1,6 @@ import { BigNumberish } from 'ethers'; -import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; -import { types } from '@hyperlane-xyz/utils'; +import { AgentConnectionType, ChainMap, ChainName } from '@hyperlane-xyz/sdk'; import { Contexts } from '../../config/contexts'; import { @@ -199,7 +198,7 @@ export interface AgentConfig { context: Contexts; docker: DockerConfig; quorumProvider?: boolean; - connectionType: ConnectionType; + connectionType: AgentConnectionType; index?: IndexingConfig; aws?: AwsConfig; // Names of all chains in the environment @@ -213,57 +212,6 @@ export interface AgentConfig { rolesWithKeys: KEY_ROLE_ENUM[]; } -export type RustSigner = { - key: string; - type: string; // TODO -}; - -export enum ConnectionType { - Http = 'http', - Ws = 'ws', - HttpQuorum = 'httpQuorum', - HttpFallback = 'httpFallback', -} - -export type RustConnection = - | { - type: ConnectionType.Http; - url: string; - } - | { type: ConnectionType.Ws; url: string } - | { type: ConnectionType.HttpQuorum; urls: string } - | { type: ConnectionType.HttpFallback; urls: string }; - -export type RustCoreAddresses = { - mailbox: types.Address; - interchainGasPaymaster: types.Address; - validatorAnnounce: types.Address; -}; - -export interface RustChainSetupBase { - name: ChainName; - domain: number; - signer?: RustSigner; - finalityBlocks: number; - addresses: RustCoreAddresses; - protocol: 'ethereum' | 'fuel'; - connection?: RustConnection; - index?: { from: number }; -} - -export interface RustChainSetup extends RustChainSetupBase { - signer: RustSigner; - connection: RustConnection; -} - -export type RustConfig = { - chains: Partial>; - tracing?: { - level?: string; - fmt?: 'json'; - }; -}; - // Helper to get chain-specific agent configurations export class ChainAgentConfig { constructor( diff --git a/typescript/infra/src/config/chain.ts b/typescript/infra/src/config/chain.ts index 11ae948ae..8de30d23f 100644 --- a/typescript/infra/src/config/chain.ts +++ b/typescript/infra/src/config/chain.ts @@ -1,11 +1,14 @@ import { FallbackProviderConfig } from '@ethersproject/providers'; import { ethers } from 'ethers'; -import { ChainName, providerBuilder } from '@hyperlane-xyz/sdk'; +import { + AgentConnectionType, + ChainName, + providerBuilder, +} from '@hyperlane-xyz/sdk'; import { getSecretRpcEndpoint } from '../agents'; -import { ConnectionType } from './agent'; import { DeployEnvironment } from './environment'; export const defaultRetry = { @@ -16,20 +19,20 @@ export const defaultRetry = { export async function fetchProvider( environment: DeployEnvironment, chainName: ChainName, - connectionType: ConnectionType = ConnectionType.Http, + connectionType: AgentConnectionType = AgentConnectionType.Http, ): Promise { - const single = connectionType === ConnectionType.Http; + const single = connectionType === AgentConnectionType.Http; const rpcData = await getSecretRpcEndpoint(environment, chainName, !single); switch (connectionType) { - case ConnectionType.Http: { + case AgentConnectionType.Http: { return providerBuilder({ http: rpcData, retry: defaultRetry }); } - case ConnectionType.HttpQuorum: { + case AgentConnectionType.HttpQuorum: { return new ethers.providers.FallbackProvider( (rpcData as string[]).map((url) => providerBuilder({ http: url })), // disable retry for quorum ); } - case ConnectionType.HttpFallback: { + case AgentConnectionType.HttpFallback: { return new ethers.providers.FallbackProvider( (rpcData as string[]).map((url, index) => { const fallbackProviderConfig: FallbackProviderConfig = { diff --git a/typescript/infra/src/config/environment.ts b/typescript/infra/src/config/environment.ts index 9bc6e0cf5..4e6814c8d 100644 --- a/typescript/infra/src/config/environment.ts +++ b/typescript/infra/src/config/environment.ts @@ -1,17 +1,20 @@ import { + AgentConnectionType, ChainMap, ChainMetadata, ChainName, CoreConfig, MultiProvider, + OverheadIgpConfig, } from '@hyperlane-xyz/sdk'; -import { CoreEnvironment } from '@hyperlane-xyz/sdk/dist/core/HyperlaneCore'; +import { HyperlaneEnvironment } from '@hyperlane-xyz/sdk/dist/consts/environments'; +import { types } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; import { environments } from '../../config/environments'; import { KEY_ROLE_ENUM } from '../agents/roles'; -import { AgentConfig, ConnectionType } from './agent'; +import { AgentConfig } from './agent'; import { KeyFunderConfig } from './funding'; import { AllStorageGasOracleConfigs } from './gas-oracle'; import { HelloWorldConfig } from './helloworld'; @@ -31,11 +34,13 @@ export type CoreEnvironmentConfig = { // Each AgentConfig, keyed by the context agents: Partial>; core: ChainMap; + igp: ChainMap; + owners: ChainMap; infra: InfrastructureConfig; getMultiProvider: ( context?: Contexts, role?: KEY_ROLE_ENUM, - connectionType?: ConnectionType, + connectionType?: AgentConnectionType, ) => Promise; helloWorld?: Partial>; keyFunderConfig?: KeyFunderConfig; @@ -43,7 +48,10 @@ export type CoreEnvironmentConfig = { storageGasOracleConfig?: AllStorageGasOracleConfigs; }; -export const deployEnvToSdkEnv: Record = { +export const deployEnvToSdkEnv: Record< + DeployEnvironment, + HyperlaneEnvironment +> = { mainnet2: 'mainnet', testnet3: 'testnet', test: 'test', diff --git a/typescript/infra/src/config/funding.ts b/typescript/infra/src/config/funding.ts index 081b8b2d7..f74ce6fe4 100644 --- a/typescript/infra/src/config/funding.ts +++ b/typescript/infra/src/config/funding.ts @@ -1,7 +1,9 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { Contexts } from '../../config/contexts'; import { KEY_ROLE_ENUM } from '../agents/roles'; -import { ConnectionType, DockerConfig } from './agent'; +import { DockerConfig } from './agent'; export interface ContextAndRoles { context: Contexts; @@ -18,5 +20,5 @@ export interface KeyFunderConfig { contextsAndRolesToFund: ContextAndRolesMap; cyclesBetweenEthereumMessages?: number; prometheusPushGateway: string; - connectionType: ConnectionType.Http | ConnectionType.HttpQuorum; + connectionType: AgentConnectionType.Http | AgentConnectionType.HttpQuorum; } diff --git a/typescript/infra/src/config/helloworld.ts b/typescript/infra/src/config/helloworld.ts index 29541f751..071e7464f 100644 --- a/typescript/infra/src/config/helloworld.ts +++ b/typescript/infra/src/config/helloworld.ts @@ -1,6 +1,6 @@ -import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; +import { AgentConnectionType, ChainMap, ChainName } from '@hyperlane-xyz/sdk'; -import { ConnectionType, DockerConfig } from './agent'; +import { DockerConfig } from './agent'; export enum HelloWorldKathyRunMode { // Sends messages between all pairwise chains @@ -29,7 +29,7 @@ export interface HelloWorldKathyConfig { messageReceiptTimeout: number; // Which type of provider to use - connectionType: Exclude; + connectionType: Exclude; // How many cycles to skip between a cycles that send messages to/from Ethereum. Defaults to 0. cyclesBetweenEthereumMessages?: number; } diff --git a/typescript/infra/src/config/index.ts b/typescript/infra/src/config/index.ts index d0d09eede..6ed564d05 100644 --- a/typescript/infra/src/config/index.ts +++ b/typescript/infra/src/config/index.ts @@ -1,4 +1,4 @@ -export { AgentConfig, RustConfig, RustChainSetup } from './agent'; +export { AgentConfig } from './agent'; export { CoreEnvironmentConfig, DeployEnvironment } from './environment'; export { AllStorageGasOracleConfigs, diff --git a/typescript/infra/src/config/middleware.ts b/typescript/infra/src/config/middleware.ts index 0a0cd364e..052a4360e 100644 --- a/typescript/infra/src/config/middleware.ts +++ b/typescript/infra/src/config/middleware.ts @@ -1,8 +1,10 @@ -import { ConnectionType, DockerConfig } from './agent'; +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + +import { DockerConfig } from './agent'; export interface LiquidityLayerRelayerConfig { docker: DockerConfig; namespace: string; - connectionType: ConnectionType.Http | ConnectionType.HttpQuorum; + connectionType: AgentConnectionType.Http | AgentConnectionType.HttpQuorum; prometheusPushGateway: string; } diff --git a/typescript/infra/src/core/deploy.ts b/typescript/infra/src/core/deploy.ts index c28c3e1e6..058aec5eb 100644 --- a/typescript/infra/src/core/deploy.ts +++ b/typescript/infra/src/core/deploy.ts @@ -1,34 +1,18 @@ import { ethers } from 'ethers'; -import { - InterchainGasPaymaster, - Mailbox, - OverheadIgp, - ProxyAdmin, - ValidatorAnnounce, -} from '@hyperlane-xyz/core'; +import { Mailbox, ProxyAdmin, ValidatorAnnounce } from '@hyperlane-xyz/core'; import { ChainMap, ChainName, CoreConfig, - GasOracleContracts, HyperlaneCoreDeployer, MultiProvider, ProxiedContract, TransparentProxyAddresses, - chainMetadata, - objMap, } from '@hyperlane-xyz/sdk'; import { types } from '@hyperlane-xyz/utils'; -import { DeployEnvironment, RustConfig } from '../config'; -import { - ConnectionType, - RustChainSetupBase, - RustConnection, -} from '../config/agent'; -import { deployEnvToSdkEnv } from '../config/environment'; -import { writeJSON } from '../utils/utils'; +import { DeployEnvironment } from '../config'; export class HyperlaneCoreInfraDeployer extends HyperlaneCoreDeployer { environment: DeployEnvironment; @@ -42,44 +26,6 @@ export class HyperlaneCoreInfraDeployer extends HyperlaneCoreDeployer { this.environment = environment; } - async deployInterchainGasPaymaster( - chain: ChainName, - proxyAdmin: ProxyAdmin, - gasOracleContracts: GasOracleContracts, - ): Promise< - ProxiedContract - > { - const deployOpts = { - create2Salt: ethers.utils.solidityKeccak256( - ['string', 'string', 'uint8'], - [this.environment, 'interchainGasPaymaster', 6], - ), - }; - return super.deployInterchainGasPaymaster( - chain, - proxyAdmin, - gasOracleContracts, - deployOpts, - ); - } - - async deployDefaultIsmInterchainGasPaymaster( - chain: ChainName, - interchainGasPaymasterAddress: types.Address, - ): Promise { - const deployOpts = { - create2Salt: ethers.utils.solidityKeccak256( - ['string', 'string', 'uint8'], - [this.environment, 'defaultIsmInterchainGasPaymaster', 4], - ), - }; - return super.deployDefaultIsmInterchainGasPaymaster( - chain, - interchainGasPaymasterAddress, - deployOpts, - ); - } - async deployMailbox( chain: ChainName, defaultIsmAddress: types.Address, @@ -111,53 +57,4 @@ export class HyperlaneCoreInfraDeployer extends HyperlaneCoreDeployer { }; return super.deployValidatorAnnounce(chain, mailboxAddress, deployOpts); } - - writeRustConfigs(directory: string) { - const rustConfig: RustConfig = { - chains: {}, - }; - objMap(this.configMap, (chain) => { - const contracts = this.deployedContracts[chain]; - const metadata = chainMetadata[chain]; - // Don't write config for undeployed chains - if ( - contracts == undefined || - contracts.mailbox == undefined || - contracts.interchainGasPaymaster == undefined || - contracts.validatorAnnounce == undefined - ) { - return; - } - - const chainConfig: RustChainSetupBase = { - name: chain, - domain: metadata.chainId, - addresses: { - mailbox: contracts.mailbox.contract.address, - interchainGasPaymaster: contracts.interchainGasPaymaster.address, - validatorAnnounce: contracts.validatorAnnounce.address, - }, - protocol: 'ethereum', - finalityBlocks: metadata.blocks!.reorgPeriod!, - connection: { - // not a valid connection but we want to fill in the HTTP type for - // them as a default and leave out the URL - type: ConnectionType.Http, - url: undefined, - } as any as RustConnection, - }; - - const startingBlockNumber = this.startingBlockNumbers[chain]; - if (startingBlockNumber) { - chainConfig.index = { from: startingBlockNumber }; - } - - rustConfig.chains[chain] = chainConfig; - }); - writeJSON( - directory, - `${deployEnvToSdkEnv[this.environment]}_config.json`, - rustConfig, - ); - } } diff --git a/typescript/infra/src/core/govern.ts b/typescript/infra/src/core/govern.ts index 4a74e5ac0..2a463ae82 100644 --- a/typescript/infra/src/core/govern.ts +++ b/typescript/infra/src/core/govern.ts @@ -1,149 +1,31 @@ -import { prompts } from 'prompts'; - -import { InterchainGasPaymaster, OverheadIgp } from '@hyperlane-xyz/core'; import { ChainMap, ChainName, - CoreContracts, + CoreConfig, CoreViolationType, - DefaultIsmIgpViolation, - DefaultIsmIgpViolationType, EnrolledValidatorsViolation, + HyperlaneCore, HyperlaneCoreChecker, - IgpBeneficiaryViolation, - IgpGasOraclesViolation, - IgpViolation, - IgpViolationType, MultisigIsmViolation, MultisigIsmViolationType, OwnerViolation, + ProxyKind, ProxyViolation, ViolationType, - objMap, } from '@hyperlane-xyz/sdk'; -import { ProxyKind } from '@hyperlane-xyz/sdk/dist/proxy'; import { types, utils } from '@hyperlane-xyz/utils'; -import { canProposeSafeTransactions } from '../utils/safe'; - import { - ManualMultiSend, - MultiSend, - SafeMultiSend, - SignerMultiSend, -} from './multisend'; - -enum SubmissionType { - MANUAL = 'MANUAL', - SIGNER = 'SIGNER', - SAFE = 'SAFE', -} - -type AnnotatedCallData = types.CallData & { - submissionType?: SubmissionType; - description: string; -}; - -export class HyperlaneCoreGovernor { - readonly checker: HyperlaneCoreChecker; - private calls: ChainMap; - private canPropose: ChainMap>; - - constructor(checker: HyperlaneCoreChecker) { - this.checker = checker; - this.calls = objMap(this.checker.app.contractsMap, () => []); - this.canPropose = objMap(this.checker.app.contractsMap, () => new Map()); - } - - async govern(confirm = true) { - // 1. Produce calls from checker violations. - await this.mapViolationsToCalls(); - - // 2. For each call, infer how it should be submitted on-chain. - await this.inferCallSubmissionTypes(); - - // 3. Prompt the user to confirm that the count, description, - // and submission methods look correct before submitting. - for (const chain of Object.keys(this.calls)) { - await this.sendCalls(chain, confirm); - } - } - - async governChain(chain: ChainName, confirm = true) { - // 1. Produce calls from checker violations. - await this.mapViolationsToCalls(); - - // 2. For each call, infer how it should be submitted on-chain. - await this.inferCallSubmissionTypes(); - - // 3. Prompt the user to confirm that the count, description, - // and submission methods look correct before submitting. - await this.sendCalls(chain, confirm); - } - - protected async sendCalls(chain: ChainName, confirm: boolean) { - const calls = this.calls[chain]; - console.log(`\nFound ${calls.length} transactions for ${chain}`); - const filterCalls = (submissionType: SubmissionType) => - calls.filter((call) => call.submissionType == submissionType); - const summarizeCalls = async ( - submissionType: SubmissionType, - calls: AnnotatedCallData[], - ): Promise => { - if (calls.length > 0) { - console.log( - `> ${calls.length} calls will be submitted via ${submissionType}`, - ); - calls.map((c) => - console.log(`> > ${c.description} (to: ${c.to} data: ${c.data})`), - ); - const response = - !confirm || - prompts.confirm({ - type: 'confirm', - name: 'value', - message: 'Can you confirm?', - initial: false, - }); - return response as unknown as boolean; - } - return false; - }; - - const sendCallsForType = async ( - submissionType: SubmissionType, - multiSend: MultiSend, - ) => { - const calls = filterCalls(submissionType); - if (calls.length > 0) { - const confirmed = await summarizeCalls(submissionType, calls); - if (confirmed) { - console.log(`Submitting calls on ${chain} via ${submissionType}`); - await multiSend.sendTransactions( - calls.map((call) => ({ to: call.to, data: call.data })), - ); - } else { - console.log( - `Skipping submission of calls on ${chain} via ${submissionType}`, - ); - } - } - }; - - await sendCallsForType( - SubmissionType.SIGNER, - new SignerMultiSend(this.checker.multiProvider, chain), - ); - const owner = this.checker.configMap[chain!].owner!; - await sendCallsForType( - SubmissionType.SAFE, - new SafeMultiSend(this.checker.multiProvider, chain, owner), - ); - await sendCallsForType(SubmissionType.MANUAL, new ManualMultiSend(chain)); - } - - protected pushCall(chain: ChainName, call: AnnotatedCallData) { - this.calls[chain].push(call); + AnnotatedCallData, + HyperlaneAppGovernor, +} from '../govern/HyperlaneAppGovernor'; + +export class HyperlaneCoreGovernor extends HyperlaneAppGovernor< + HyperlaneCore, + CoreConfig +> { + constructor(checker: HyperlaneCoreChecker, owners: ChainMap) { + super(checker, owners); } protected async mapViolationsToCalls() { @@ -161,96 +43,12 @@ export class HyperlaneCoreGovernor { this.handleProxyViolation(violation as ProxyViolation); break; } - case CoreViolationType.InterchainGasPaymaster: { - this.handleIgpViolation(violation as IgpViolation); - break; - } - case CoreViolationType.DefaultIsmInterchainGasPaymaster: { - this.handleDefaultIsmIgpViolation( - violation as DefaultIsmIgpViolation, - ); - break; - } default: throw new Error(`Unsupported violation type ${violation.type}`); } } } - handleProxyViolation(violation: ProxyViolation) { - const contracts: CoreContracts = - this.checker.app.contractsMap[violation.chain]; - const data = contracts.proxyAdmin.interface.encodeFunctionData('upgrade', [ - violation.data.proxyAddresses.proxy, - violation.data.proxyAddresses.implementation, - ]); - - this.pushCall(violation.chain, { - to: contracts.proxyAdmin.address, - data, - description: `Upgrade proxy ${violation.data.proxyAddresses.proxy} to implementation ${violation.data.proxyAddresses.implementation}`, - }); - } - - protected async inferCallSubmissionTypes() { - for (const chain of Object.keys(this.calls)) { - for (const call of this.calls[chain]) { - call.submissionType = await this.inferCallSubmissionType(chain, call); - } - } - } - - protected async inferCallSubmissionType( - chain: ChainName, - call: AnnotatedCallData, - ): Promise { - const multiProvider = this.checker.multiProvider; - const signer = multiProvider.getSigner(chain); - const signerAddress = await signer.getAddress(); - - const canUseSubmissionType = async ( - submitterAddress: types.Address, - ): Promise => { - try { - await multiProvider.estimateGas(chain, call, submitterAddress); - return true; - } catch (e) {} // eslint-disable-line no-empty - return false; - }; - - if (await canUseSubmissionType(signerAddress)) { - return SubmissionType.SIGNER; - } - - // 2. Check if the call will succeed via Gnosis Safe. - const safeAddress = this.checker.configMap[chain!].owner; - if (!safeAddress) throw new Error(`Owner address not found for ${chain}`); - // 2a. Confirm that the signer is a Safe owner or delegate. - // This should implicitly check whether or not the owner is a gnosis - // safe. - if (!this.canPropose[chain].has(safeAddress)) { - this.canPropose[chain].set( - safeAddress, - await canProposeSafeTransactions( - signerAddress, - chain, - multiProvider, - safeAddress, - ), - ); - } - - // 2b. Check if calling from the owner/safeAddress will succeed. - if ( - this.canPropose[chain].get(safeAddress) && - (await canUseSubmissionType(safeAddress)) - ) { - return SubmissionType.SAFE; - } - - return SubmissionType.MANUAL; - } - // pushes calls which reconcile actual and expected sets on chain protected pushSetReconcilationCalls(reconcile: { chain: ChainName; @@ -318,103 +116,4 @@ export class HyperlaneCoreGovernor { ); } } - - handleOwnerViolation(violation: OwnerViolation) { - this.pushCall(violation.chain, { - to: violation.contract.address, - data: violation.contract.interface.encodeFunctionData( - 'transferOwnership', - [violation.expected], - ), - description: `Transfer ownership of ${violation.contract.address} to ${violation.expected}`, - }); - } - - handleIgpViolation(violation: IgpViolation) { - switch (violation.subType) { - case IgpViolationType.Beneficiary: { - const beneficiaryViolation = violation as IgpBeneficiaryViolation; - this.pushCall(beneficiaryViolation.chain, { - to: beneficiaryViolation.contract.address, - data: beneficiaryViolation.contract.interface.encodeFunctionData( - 'setBeneficiary', - [beneficiaryViolation.expected], - ), - description: `Set IGP beneficiary to ${beneficiaryViolation.expected}`, - }); - break; - } - case IgpViolationType.GasOracles: { - const gasOraclesViolation = violation as IgpGasOraclesViolation; - - const configs: InterchainGasPaymaster.GasOracleConfigStruct[] = []; - for (const [remote, expected] of Object.entries( - gasOraclesViolation.expected, - )) { - const remoteId = this.checker.multiProvider.getDomainId(remote); - - configs.push({ - remoteDomain: remoteId, - gasOracle: expected, - }); - } - - this.pushCall(gasOraclesViolation.chain, { - to: gasOraclesViolation.contract.address, - data: gasOraclesViolation.contract.interface.encodeFunctionData( - 'setGasOracles', - [configs], - ), - description: `Setting ${Object.keys(gasOraclesViolation.expected) - .map((remoteStr) => { - const remote = remoteStr as ChainName; - const remoteId = this.checker.multiProvider.getDomainId(remote); - const expected = gasOraclesViolation.expected[remote]; - return `gas oracle for ${remote} (domain ID ${remoteId}) to ${expected}`; - }) - .join(', ')}`, - }); - break; - } - default: - throw new Error(`Unsupported IgpViolationType: ${violation.subType}`); - } - } - - handleDefaultIsmIgpViolation(violation: DefaultIsmIgpViolation) { - switch (violation.subType) { - case DefaultIsmIgpViolationType.DestinationGasOverheads: { - const configs: OverheadIgp.DomainConfigStruct[] = Object.entries( - violation.expected, - ).map( - ([remote, gasOverhead]) => - ({ - domain: this.checker.multiProvider.getDomainId(remote), - gasOverhead: gasOverhead, - } as OverheadIgp.DomainConfigStruct), - ); - - this.pushCall(violation.chain, { - to: violation.contract.address, - data: violation.contract.interface.encodeFunctionData( - 'setDestinationGasOverheads', - [configs], - ), - description: `Setting ${Object.keys(violation.expected) - .map((remoteStr) => { - const remote = remoteStr as ChainName; - const remoteId = this.checker.multiProvider.getDomainId(remote); - const expected = violation.expected[remote]; - return `destination gas overhead for ${remote} (domain ID ${remoteId}) to ${expected}`; - }) - .join(', ')}`, - }); - break; - } - default: - throw new Error( - `Unsupported DefaultIsmIgpViolationType: ${violation.subType}`, - ); - } - } } diff --git a/typescript/infra/src/gas/deploy.ts b/typescript/infra/src/gas/deploy.ts new file mode 100644 index 000000000..768396b60 --- /dev/null +++ b/typescript/infra/src/gas/deploy.ts @@ -0,0 +1,71 @@ +import { ethers } from 'ethers'; + +import { + InterchainGasPaymaster, + OverheadIgp, + ProxyAdmin, + StorageGasOracle, +} from '@hyperlane-xyz/core'; +import { + ChainMap, + ChainName, + HyperlaneIgpDeployer, + MultiProvider, + OverheadIgpConfig, + ProxiedContract, + TransparentProxyAddresses, +} from '@hyperlane-xyz/sdk'; +import { types } from '@hyperlane-xyz/utils'; + +import { DeployEnvironment } from '../config'; + +export class HyperlaneIgpInfraDeployer extends HyperlaneIgpDeployer { + environment: DeployEnvironment; + + constructor( + multiProvider: MultiProvider, + configMap: ChainMap, + environment: DeployEnvironment, + ) { + super(multiProvider, configMap); + this.environment = environment; + } + + async deployInterchainGasPaymaster( + chain: ChainName, + proxyAdmin: ProxyAdmin, + storageGasOracle: StorageGasOracle, + ): Promise< + ProxiedContract + > { + const deployOpts = { + create2Salt: ethers.utils.solidityKeccak256( + ['string', 'string', 'uint8'], + [this.environment, 'interchainGasPaymaster', 6], + ), + }; + return super.deployInterchainGasPaymaster( + chain, + proxyAdmin, + storageGasOracle, + deployOpts, + ); + } + + async deployOverheadInterchainGasPaymaster( + chain: ChainName, + interchainGasPaymasterAddress: types.Address, + ): Promise { + const deployOpts = { + create2Salt: ethers.utils.solidityKeccak256( + ['string', 'string', 'uint8'], + [this.environment, 'defaultIsmInterchainGasPaymaster', 4], + ), + }; + return super.deployOverheadInterchainGasPaymaster( + chain, + interchainGasPaymasterAddress, + deployOpts, + ); + } +} diff --git a/typescript/infra/src/gas/govern.ts b/typescript/infra/src/gas/govern.ts new file mode 100644 index 000000000..17fc003a1 --- /dev/null +++ b/typescript/infra/src/gas/govern.ts @@ -0,0 +1,126 @@ +import { InterchainGasPaymaster, OverheadIgp } from '@hyperlane-xyz/core'; +import { + ChainMap, + ChainName, + HyperlaneIgp, + HyperlaneIgpChecker, + IgpBeneficiaryViolation, + IgpGasOraclesViolation, + IgpOverheadViolation, + IgpViolation, + IgpViolationType, + OverheadIgpConfig, + ProxyKind, + ProxyViolation, +} from '@hyperlane-xyz/sdk'; +import { types } from '@hyperlane-xyz/utils'; + +import { HyperlaneAppGovernor } from '../govern/HyperlaneAppGovernor'; + +export class HyperlaneIgpGovernor extends HyperlaneAppGovernor< + HyperlaneIgp, + OverheadIgpConfig +> { + constructor(checker: HyperlaneIgpChecker, owners: ChainMap) { + super(checker, owners); + } + + protected async mapViolationsToCalls() { + for (const violation of this.checker.violations) { + switch (violation.type) { + case 'InterchainGasPaymaster': { + this.handleIgpViolation(violation as IgpViolation); + break; + } + case ProxyKind.Transparent: { + this.handleProxyViolation(violation as ProxyViolation); + break; + } + default: + throw new Error(`Unsupported violation type ${violation.type}`); + } + } + } + + handleIgpViolation(violation: IgpViolation) { + switch (violation.subType) { + case IgpViolationType.Beneficiary: { + const beneficiaryViolation = violation as IgpBeneficiaryViolation; + this.pushCall(beneficiaryViolation.chain, { + to: beneficiaryViolation.contract.address, + data: beneficiaryViolation.contract.interface.encodeFunctionData( + 'setBeneficiary', + [beneficiaryViolation.expected], + ), + description: `Set IGP beneficiary to ${beneficiaryViolation.expected}`, + }); + break; + } + case IgpViolationType.GasOracles: { + const gasOraclesViolation = violation as IgpGasOraclesViolation; + + const configs: InterchainGasPaymaster.GasOracleConfigStruct[] = []; + for (const [remote, expected] of Object.entries( + gasOraclesViolation.expected, + )) { + const remoteId = this.checker.multiProvider.getDomainId(remote); + + configs.push({ + remoteDomain: remoteId, + gasOracle: expected, + }); + } + + this.pushCall(gasOraclesViolation.chain, { + to: gasOraclesViolation.contract.address, + data: gasOraclesViolation.contract.interface.encodeFunctionData( + 'setGasOracles', + [configs], + ), + description: `Setting ${Object.keys(gasOraclesViolation.expected) + .map((remoteStr) => { + const remote = remoteStr as ChainName; + const remoteId = this.checker.multiProvider.getDomainId(remote); + const expected = gasOraclesViolation.expected[remote]; + return `gas oracle for ${remote} (domain ID ${remoteId}) to ${expected}`; + }) + .join(', ')}`, + }); + break; + } + case IgpViolationType.Overhead: { + const overheadViolation = violation as IgpOverheadViolation; + const configs: OverheadIgp.DomainConfigStruct[] = Object.entries( + violation.expected, + ).map( + ([remote, gasOverhead]) => + ({ + domain: this.checker.multiProvider.getDomainId(remote), + gasOverhead: gasOverhead, + } as OverheadIgp.DomainConfigStruct), + ); + + this.pushCall(violation.chain, { + to: overheadViolation.contract.address, + data: overheadViolation.contract.interface.encodeFunctionData( + 'setDestinationGasOverheads', + [configs], + ), + description: `Setting ${Object.keys(violation.expected) + .map((remoteStr) => { + const remote = remoteStr as ChainName; + const remoteId = this.checker.multiProvider.getDomainId(remote); + const expected = violation.expected[remote]; + return `destination gas overhead for ${remote} (domain ID ${remoteId}) to ${expected}`; + }) + .join(', ')}`, + }); + break; + } + default: + throw new Error( + `Unsupported IgpViolation subType: ${violation.subType}`, + ); + } + } +} diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts new file mode 100644 index 000000000..fcf9f0e13 --- /dev/null +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -0,0 +1,219 @@ +import { prompts } from 'prompts'; + +import { + ChainMap, + ChainName, + CoreContracts, + HyperlaneApp, + HyperlaneAppChecker, + OwnerViolation, + ProxyViolation, + objMap, +} from '@hyperlane-xyz/sdk'; +import { types } from '@hyperlane-xyz/utils'; + +import { canProposeSafeTransactions } from '../utils/safe'; + +import { + ManualMultiSend, + MultiSend, + SafeMultiSend, + SignerMultiSend, +} from './multisend'; + +export enum SubmissionType { + MANUAL = 'MANUAL', + SIGNER = 'SIGNER', + SAFE = 'SAFE', +} + +export type AnnotatedCallData = types.CallData & { + submissionType?: SubmissionType; + description: string; +}; + +export abstract class HyperlaneAppGovernor< + App extends HyperlaneApp, + Config, +> { + readonly checker: HyperlaneAppChecker; + private owners: ChainMap; + private calls: ChainMap; + private canPropose: ChainMap>; + + constructor( + checker: HyperlaneAppChecker, + owners: ChainMap, + ) { + this.checker = checker; + this.owners = owners; + this.calls = objMap(this.checker.app.contractsMap, () => []); + this.canPropose = objMap(this.checker.app.contractsMap, () => new Map()); + } + + async govern(confirm = true, chain?: ChainName) { + // 1. Produce calls from checker violations. + await this.mapViolationsToCalls(); + + // 2. For each call, infer how it should be submitted on-chain. + await this.inferCallSubmissionTypes(); + + // 3. Prompt the user to confirm that the count, description, + // and submission methods look correct before submitting. + const chains = chain ? [chain] : Object.keys(this.calls); + for (const chain of chains) { + await this.sendCalls(chain, confirm); + } + } + + protected async sendCalls(chain: ChainName, confirm: boolean) { + const calls = this.calls[chain]; + console.log(`\nFound ${calls.length} transactions for ${chain}`); + const filterCalls = (submissionType: SubmissionType) => + calls.filter((call) => call.submissionType == submissionType); + const summarizeCalls = async ( + submissionType: SubmissionType, + calls: AnnotatedCallData[], + ): Promise => { + if (calls.length > 0) { + console.log( + `> ${calls.length} calls will be submitted via ${submissionType}`, + ); + calls.map((c) => + console.log(`> > ${c.description} (to: ${c.to} data: ${c.data})`), + ); + const response = + !confirm || + prompts.confirm({ + type: 'confirm', + name: 'value', + message: 'Can you confirm?', + initial: false, + }); + return !!response; + } + return false; + }; + + const sendCallsForType = async ( + submissionType: SubmissionType, + multiSend: MultiSend, + ) => { + const calls = filterCalls(submissionType); + if (calls.length > 0) { + const confirmed = await summarizeCalls(submissionType, calls); + if (confirmed) { + console.log(`Submitting calls on ${chain} via ${submissionType}`); + await multiSend.sendTransactions( + calls.map((call) => ({ to: call.to, data: call.data })), + ); + } else { + console.log( + `Skipping submission of calls on ${chain} via ${submissionType}`, + ); + } + } + }; + + await sendCallsForType( + SubmissionType.SIGNER, + new SignerMultiSend(this.checker.multiProvider, chain), + ); + await sendCallsForType( + SubmissionType.SAFE, + new SafeMultiSend(this.checker.multiProvider, chain, this.owners[chain]), + ); + await sendCallsForType(SubmissionType.MANUAL, new ManualMultiSend(chain)); + } + + protected pushCall(chain: ChainName, call: AnnotatedCallData) { + this.calls[chain].push(call); + } + + protected abstract mapViolationsToCalls(): Promise; + + handleProxyViolation(violation: ProxyViolation) { + const contracts: CoreContracts = + this.checker.app.contractsMap[violation.chain]; + const data = contracts.proxyAdmin.interface.encodeFunctionData('upgrade', [ + violation.data.proxyAddresses.proxy, + violation.data.proxyAddresses.implementation, + ]); + + this.pushCall(violation.chain, { + to: contracts.proxyAdmin.address, + data, + description: `Upgrade proxy ${violation.data.proxyAddresses.proxy} to implementation ${violation.data.proxyAddresses.implementation}`, + }); + } + + protected async inferCallSubmissionTypes() { + for (const chain of Object.keys(this.calls)) { + for (const call of this.calls[chain]) { + call.submissionType = await this.inferCallSubmissionType(chain, call); + } + } + } + + protected async inferCallSubmissionType( + chain: ChainName, + call: AnnotatedCallData, + ): Promise { + const multiProvider = this.checker.multiProvider; + const signer = multiProvider.getSigner(chain); + const signerAddress = await signer.getAddress(); + + const canUseSubmissionType = async ( + submitterAddress: types.Address, + ): Promise => { + try { + await multiProvider.estimateGas(chain, call, submitterAddress); + return true; + } catch (e) {} // eslint-disable-line no-empty + return false; + }; + + if (await canUseSubmissionType(signerAddress)) { + return SubmissionType.SIGNER; + } + + // 2. Check if the call will succeed via Gnosis Safe. + const safeAddress = this.owners[chain]; + if (!safeAddress) throw new Error(`Owner address not found for ${chain}`); + // 2a. Confirm that the signer is a Safe owner or delegate. + // This should implicitly check whether or not the owner is a gnosis + // safe. + if (!this.canPropose[chain].has(safeAddress)) { + this.canPropose[chain].set( + safeAddress, + await canProposeSafeTransactions( + signerAddress, + chain, + multiProvider, + safeAddress, + ), + ); + } + + // 2b. Check if calling from the owner/safeAddress will succeed. + if ( + this.canPropose[chain].get(safeAddress) && + (await canUseSubmissionType(safeAddress)) + ) { + return SubmissionType.SAFE; + } + + return SubmissionType.MANUAL; + } + + handleOwnerViolation(violation: OwnerViolation) { + this.pushCall(violation.chain, { + to: violation.contract.address, + data: violation.contract.interface.encodeFunctionData( + 'transferOwnership', + [violation.expected], + ), + description: `Transfer ownership of ${violation.contract.address} to ${violation.expected}`, + }); + } +} diff --git a/typescript/infra/src/core/multisend.ts b/typescript/infra/src/govern/multisend.ts similarity index 100% rename from typescript/infra/src/core/multisend.ts rename to typescript/infra/src/govern/multisend.ts diff --git a/typescript/infra/src/scraper/deploy.ts b/typescript/infra/src/scraper/deploy.ts index 1f095f1bc..7212d1bd2 100644 --- a/typescript/infra/src/scraper/deploy.ts +++ b/typescript/infra/src/scraper/deploy.ts @@ -1,5 +1,6 @@ +import { AgentConnectionType } from '@hyperlane-xyz/sdk'; + import { AgentConfig } from '../config'; -import { ConnectionType } from '../config/agent'; import { HelmCommand, buildHelmChartDependencies, @@ -59,7 +60,7 @@ async function scraperHelmValues(agentConfig: AgentConfig) { let baseConnectionConfig: Record = { type: agentConfig.connectionType, }; - if (baseConnectionConfig.type == ConnectionType.HttpQuorum) { + if (baseConnectionConfig.type == AgentConnectionType.HttpQuorum) { baseConnectionConfig = { ...baseConnectionConfig, urls: '', diff --git a/typescript/infra/src/testcontracts/testquerysender.ts b/typescript/infra/src/testcontracts/testquerysender.ts index 244ffcd50..c1ae22311 100644 --- a/typescript/infra/src/testcontracts/testquerysender.ts +++ b/typescript/infra/src/testcontracts/testquerysender.ts @@ -2,8 +2,8 @@ import { TestQuerySender, TestQuerySender__factory } from '@hyperlane-xyz/core'; import { ChainMap, ChainName, - HyperlaneCore, HyperlaneDeployer, + HyperlaneIgp, MultiProvider, } from '@hyperlane-xyz/sdk'; @@ -25,7 +25,7 @@ export class TestQuerySenderDeployer extends HyperlaneDeployer< constructor( multiProvider: MultiProvider, queryRouters: ChainMap, - protected core: HyperlaneCore, + protected igp: HyperlaneIgp, ) { super(multiProvider, queryRouters, factories); } @@ -35,7 +35,7 @@ export class TestQuerySenderDeployer extends HyperlaneDeployer< 'initialize', [ config.queryRouterAddress, - this.core.getContracts(chain).interchainGasPaymaster.address, + this.igp.getContracts(chain).interchainGasPaymaster.address, ], ); const TestQuerySender = await this.deployContract( diff --git a/typescript/infra/src/utils/utils.ts b/typescript/infra/src/utils/utils.ts index b6c32b2bd..737d0ff85 100644 --- a/typescript/infra/src/utils/utils.ts +++ b/typescript/infra/src/utils/utils.ts @@ -5,7 +5,12 @@ import { ethers } from 'ethers'; import fs from 'fs'; import path from 'path'; -import { AllChains, ChainName, CoreChainName } from '@hyperlane-xyz/sdk'; +import { + AllChains, + ChainName, + CoreChainName, + objMerge, +} from '@hyperlane-xyz/sdk'; import { Contexts } from '../../config/contexts'; import { ALL_KEY_ROLES, KEY_ROLE_ENUM } from '../agents/roles'; @@ -152,6 +157,15 @@ export function warn(text: string, padded = false) { } } +export function writeMergedJSON(directory: string, filename: string, obj: any) { + if (fs.existsSync(path.join(directory, filename))) { + const previous = readJSON(directory, filename); + writeJSON(directory, filename, objMerge(previous, obj)); + } else { + writeJSON(directory, filename, obj); + } +} + export function writeJSON(directory: string, filename: string, obj: any) { if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }); diff --git a/typescript/infra/test/core.test.ts b/typescript/infra/test/core.test.ts index 2f086b9cf..a875e4f08 100644 --- a/typescript/infra/test/core.test.ts +++ b/typescript/infra/test/core.test.ts @@ -50,7 +50,7 @@ describe('core', async () => { const base = './test/outputs/core'; writeJSON(base, 'contracts.json', serializeContracts(contracts)); writeJSON(base, 'verification.json', deployer.verificationInputs); - deployer.writeRustConfigs(base); + // deployer.writeRustConfigs(base); }); describe('failure modes', async () => { @@ -113,13 +113,13 @@ describe('core', async () => { it('deploys a new implementation if it has been removed from the artifacts', async () => { // Copy the old addresses const oldAddresses = { - ...deployer.deployedContracts.test2!.interchainGasPaymaster!.addresses, + ...deployer.deployedContracts.test2!.mailbox!.addresses, }; // @ts-ignore - delete deployer.deployedContracts.test2!.interchainGasPaymaster!.addresses + delete deployer.deployedContracts.test2!.mailbox!.addresses .implementation; const result = await deployer.deploy(); - const newAddresses = result.test2.interchainGasPaymaster.addresses; + const newAddresses = result.test2.mailbox.addresses; // New implementation expect(newAddresses.implementation).to.not.be.undefined; expect(newAddresses.implementation).to.not.equal( diff --git a/typescript/sdk/src/HyperlaneApp.ts b/typescript/sdk/src/HyperlaneApp.ts index 1dd4a1ce0..200b94f26 100644 --- a/typescript/sdk/src/HyperlaneApp.ts +++ b/typescript/sdk/src/HyperlaneApp.ts @@ -1,13 +1,15 @@ import { HyperlaneAddresses, HyperlaneContracts, + HyperlaneFactories, + buildContracts, connectContracts, serializeContracts, } from './contracts'; import { MultiProvider } from './providers/MultiProvider'; import { ChainMap, ChainName } from './types'; import { MultiGeneric } from './utils/MultiGeneric'; -import { objMap } from './utils/objects'; +import { objMap, pick } from './utils/objects'; export class HyperlaneApp< Contracts extends HyperlaneContracts, @@ -22,6 +24,23 @@ export class HyperlaneApp< super(connectedContractsMap); } + static buildContracts( + addresses: ChainMap, + factories: HyperlaneFactories, + multiProvider: MultiProvider, + ): { contracts: ChainMap; intersectionProvider: MultiProvider } { + const chains = Object.keys(addresses); + const { intersection, multiProvider: intersectionProvider } = + multiProvider.intersect(chains, true); + + const intersectionAddresses = pick(addresses, intersection); + const contracts = buildContracts( + intersectionAddresses, + factories, + ) as ChainMap; + return { contracts, intersectionProvider }; + } + getContracts(chain: ChainName): Contracts { return this.get(chain); } diff --git a/typescript/sdk/src/agents/types.ts b/typescript/sdk/src/agents/types.ts new file mode 100644 index 000000000..fce031b5c --- /dev/null +++ b/typescript/sdk/src/agents/types.ts @@ -0,0 +1,97 @@ +import { types } from '@hyperlane-xyz/utils'; + +import { MultiProvider } from '../providers/MultiProvider'; +import { getProxyAddress } from '../proxy'; +import { ChainMap, ChainName } from '../types'; + +export type AgentSigner = { + key: string; + type: string; // TODO +}; + +export enum AgentConnectionType { + Http = 'http', + Ws = 'ws', + HttpQuorum = 'httpQuorum', + HttpFallback = 'httpFallback', +} + +export type AgentConnection = + | { + type: AgentConnectionType.Http; + url: string; + } + | { type: AgentConnectionType.Ws; url: string } + | { type: AgentConnectionType.HttpQuorum; urls: string } + | { type: AgentConnectionType.HttpFallback; urls: string }; + +export type HyperlaneAgentAddresses = { + mailbox: types.Address; + interchainGasPaymaster: types.Address; + validatorAnnounce: types.Address; +}; + +export type AgentChainSetupBase = { + name: ChainName; + domain: number; + signer?: AgentSigner; + finalityBlocks: number; + addresses: HyperlaneAgentAddresses; + protocol: 'ethereum' | 'fuel'; + connection?: AgentConnection; + index?: { from: number }; +}; + +export interface AgentChainSetup extends AgentChainSetupBase { + signer: AgentSigner; + connection: AgentConnection; +} + +export type AgentConfig = { + chains: Partial>; + tracing?: { + level?: string; + fmt?: 'json'; + }; +}; + +export function buildAgentConfig( + chains: ChainName[], + multiProvider: MultiProvider, + addresses: ChainMap, + startBlocks: ChainMap, +): AgentConfig { + const agentConfig: AgentConfig = { + chains: {}, + }; + + for (const chain of [...chains].sort()) { + const metadata = multiProvider.getChainMetadata(chain); + const chainConfig: AgentChainSetupBase = { + name: chain, + domain: metadata.chainId, + addresses: { + mailbox: getProxyAddress(addresses[chain].mailbox), + interchainGasPaymaster: getProxyAddress( + addresses[chain].interchainGasPaymaster, + ), + validatorAnnounce: getProxyAddress(addresses[chain].validatorAnnounce), + }, + protocol: 'ethereum', + finalityBlocks: metadata.blocks?.reorgPeriod ?? 1, + connection: { + // not a valid connection but we want to fill in the HTTP type for + // them as a default and leave out the URL + type: AgentConnectionType.Http, + url: undefined, + } as any as AgentConnection, + }; + + chainConfig.index = { + from: startBlocks[chain], + }; + + agentConfig.chains[chain] = chainConfig; + } + return agentConfig; +} diff --git a/typescript/sdk/src/consts/bytecode.ts b/typescript/sdk/src/consts/bytecode.ts new file mode 100644 index 000000000..94f0c608b --- /dev/null +++ b/typescript/sdk/src/consts/bytecode.ts @@ -0,0 +1,10 @@ +export enum BytecodeHash { + MAILBOX_WITHOUT_LOCAL_DOMAIN_BYTE_CODE_HASH = '0x29b7294ab3ad2e8587e5cce0e2289ce65e12a2ea2f1e7ab34a05e7737616f457', + MAILBOX_WITHOUT_LOCAL_DOMAIN_NONZERO_PAUSE_BYTE_CODE_HASH = '0x4e73e34c0982b93eebb4ac4889e9e4e1611f7c24feacf016c3a13e389f146d9c', + TRANSPARENT_PROXY_BYTECODE_HASH = '0x4dde3d0906b6492bf1d4947f667afe8d53c8899f1d8788cabafd082938dceb2d', + MULTISIG_ISM_BYTECODE_HASH = '0x5565704ffa5b10fdf37d57abfddcf137101d5fb418ded21fa6c5f90262c57dc2', + PROXY_ADMIN_BYTECODE_HASH = '0x7c378e9d49408861ca754fe684b9f7d1ea525bddf095ee0463902df701453ba0', + INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH = '0xe995bcd732f4861606036357edb2a4d4c3e9b8d7e599fe548790ac1cf26888f8', + OWNER_INITIALIZABLE_INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH = '0xd2c5b00ac2d058117491d581d63c3c4fcf6aeb2667c6cc0c7caed359c9eebea1', + OVERHEAD_IGP_BYTECODE_HASH = '0x3cfed1f24f1e9b28a76d5a8c61696a04f7bc474404b823a2fcc210ea52346252', +} diff --git a/typescript/sdk/src/consts/environments/index.ts b/typescript/sdk/src/consts/environments/index.ts index 8a2035af8..a19464f33 100644 --- a/typescript/sdk/src/consts/environments/index.ts +++ b/typescript/sdk/src/consts/environments/index.ts @@ -1,22 +1,23 @@ -import { types } from '@hyperlane-xyz/utils'; - -import { ChainMap } from '../../types'; +import { HyperlaneAgentAddresses } from '../../agents/types'; +import { CoreAddresses } from '../../core/contracts'; +import { IgpAddresses } from '../../gas/contracts'; +import { ChainMap, ChainName } from '../../types'; import { objMap } from '../../utils/objects'; import mainnet from './mainnet.json'; import test from './test.json'; import testnet from './testnet.json'; -export const environments = { test, testnet, mainnet }; +export const hyperlaneEnvironments = { test, testnet, mainnet }; + +export type HyperlaneEnvironment = keyof typeof hyperlaneEnvironments; +export type HyperlaneEnvironmentChain = Extract< + keyof typeof hyperlaneEnvironments[E], + ChainName +>; -type HyperlaneCoreAddressMap = ChainMap<{ - mailbox: types.Address; - multisigIsm: types.Address; - interchainGasPaymaster: types.Address; - interchainAccountRouter?: types.Address; - interchainQueryRouter?: types.Address; - create2Factory: types.Address; -}>; +// TODO: Add middleware addresses +export type HyperlaneContractAddresses = CoreAddresses & IgpAddresses; // Export developer-relevant addresses export const hyperlaneCoreAddresses = objMap( @@ -24,9 +25,32 @@ export const hyperlaneCoreAddresses = objMap( (_chain, addresses) => ({ mailbox: addresses.mailbox.proxy, multisigIsm: addresses.multisigIsm, + proxyAdmin: addresses.proxyAdmin, + validatorAnnounce: addresses.validatorAnnounce, + }), +) as ChainMap; + +export const hyperlaneContractAddresses = objMap( + { ...testnet, ...mainnet }, + (_chain, addresses) => ({ + mailbox: addresses.mailbox.proxy, + multisigIsm: addresses.multisigIsm, + proxyAdmin: addresses.proxyAdmin, + validatorAnnounce: addresses.validatorAnnounce, + interchainGasPaymaster: addresses.interchainGasPaymaster.proxy, + storageGasOracle: addresses.storageGasOracle, + defaultIsmInterchainGasPaymaster: + addresses.defaultIsmInterchainGasPaymaster, + //interchainAccountRouter: undefined, + //interchainQueryRouter: undefined, + }), +) as ChainMap; + +export const hyperlaneAgentAddresses = objMap( + { ...testnet, ...mainnet }, + (_chain, addresses) => ({ + mailbox: addresses.mailbox.proxy, + validatorAnnounce: addresses.validatorAnnounce, interchainGasPaymaster: addresses.interchainGasPaymaster.proxy, - interchainAccountRouter: undefined, - interchainQueryRouter: undefined, - create2Factory: addresses.create2Factory, }), -) as HyperlaneCoreAddressMap; +) as ChainMap; diff --git a/typescript/sdk/src/consts/environments/mainnet.json b/typescript/sdk/src/consts/environments/mainnet.json index b7f209e31..f30d33fa1 100644 --- a/typescript/sdk/src/consts/environments/mainnet.json +++ b/typescript/sdk/src/consts/environments/mainnet.json @@ -16,6 +16,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0x9bDE63104EE030d9De419EEd6bA7D14b86D6fE3f", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -36,6 +37,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0xec48E52D960E54a179f70907bF28b105813877ee", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -56,6 +58,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0xeE80ab5B563cB3825133f29502bA34eD3707cb8C", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -76,6 +79,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0x61A80297e77FC5395bd6Ff60EEacf7CD4f18d4a4", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -96,6 +100,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0x3a579C0bd04FC4C98A8D70EEABD9094e7be4B26D", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -116,6 +121,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0x32B92bd3e5045B67FDD8dbb7A58D25980836d04C", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -136,6 +142,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0xAab1D11E2063Bae5EB01fa946cA8d2FDe3db05D5", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -156,6 +163,7 @@ "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0xf3b1F415740A26568C45b1c771A737E31C198F09", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35", "interchainAccountRouter": "0xE0Be420779cAd6E2bEA1E4F7C02F996D9ED1fCB5", "interchainQueryRouter": "0x234b19282985882d6d6fd54dEBa272271f4eb784" }, @@ -175,6 +183,7 @@ }, "defaultIsmInterchainGasPaymaster": "0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc", "multisigIsm": "0xC343A7054838FE9F249D7E3Ec1Fa6f1D108694b8", - "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a" + "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35" } } diff --git a/typescript/sdk/src/consts/environments/testnet.json b/typescript/sdk/src/consts/environments/testnet.json index cc5f0e45f..f6554e314 100644 --- a/typescript/sdk/src/consts/environments/testnet.json +++ b/typescript/sdk/src/consts/environments/testnet.json @@ -16,6 +16,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0x4D06A1671A2a345B14B15cbD50027979A5D1d8C9", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -36,6 +37,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0xD713Db664509bd057aC2b378F4B65Db468F634A5", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -56,6 +58,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0xd71f1A64659beC0781b2aa21bc7a72F7290F6Bf3", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -76,6 +79,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0x34add51924C500b4428067E251168807b3f5faED", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -96,6 +100,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0x32B34F0D86b275b92e9289d9054Db5Ec32d2CC6C", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -116,6 +121,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0xec8875C7cE0a814A56654618D366641859F32C7A", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -136,6 +142,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0x47384E33E67007B7fE4326fb096Bdf9CbA7AB6E4", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -156,6 +163,7 @@ "defaultIsmInterchainGasPaymaster": "0xF90cB82a76492614D07B82a7658917f3aC811Ac1", "multisigIsm": "0x47384E33E67007B7fE4326fb096Bdf9CbA7AB6E4", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "interchainAccountRouter": "0xc61Bbf8eAb0b748Ecb532A7ffC49Ab7ca6D3a39D", "interchainQueryRouter": "0xF782C6C4A02f2c71BB8a1Db0166FAB40ea956818" }, @@ -175,6 +183,7 @@ }, "defaultIsmInterchainGasPaymaster": "0xF987d7edcb5890cB321437d8145E3D51131298b6", "multisigIsm": "0xD3d062a5dcBA85ae863618d4c264d2358300c283", + "testRecipient": "0xBC3cFeca7Df5A45d61BC60E7898E63670e1654aE", "create2Factory": "0xc97D8e6f57b0d64971453dDc6EB8483fec9d163a" } } diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts new file mode 100644 index 000000000..17f84a326 --- /dev/null +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -0,0 +1,173 @@ +import { MultisigIsmConfig } from '../core/types'; +import { ChainMap } from '../types'; + +export const defaultMultisigIsmConfigs: ChainMap = { + // ----------------- Mainnets ----------------- + celo: { + threshold: 4, + validators: [ + '0x1f20274b1210046769d48174c2f0e7c25ca7d5c5', // abacus + '0x3bc014bafa43f93d534aed34f750997cdffcf007', // dsrv + '0xd79d506d741fa735938f7b7847a926e34a6fe6b0', // everstake + '0xe4a258bc61e65914c2a477b2a8a433ab4ebdf44b', // zee prime + '0x6aea63b0be4679c1385c26a92a3ff8aa6a8379f2', // staked + '0xc0085e1a49bcc69e534272adb82c74c0e007e1ca', // zkv + ], + }, + ethereum: { + threshold: 4, + validators: [ + '0x4c327ccb881a7542be77500b2833dc84c839e7b7', // abacus + '0x84cb373148ef9112b277e68acf676fefa9a9a9a0', // dsrv + '0x0d860c2b28bec3af4fd3a5997283e460ff6f2789', // everstake + '0xd4c1211f0eefb97a846c4e6d6589832e52fc03db', // zee prime + '0x600c90404d5c9df885404d2cc5350c9b314ea3a2', // staked + '0x892DC66F5B2f8C438E03f6323394e34A9C24F2D6', // zkv + ], + }, + avalanche: { + threshold: 4, + validators: [ + '0xa7aa52623fe3d78c343008c95894be669e218b8d', // abacus + '0xb6004433fb04f643e2d48ae765c0e7f890f0bc0c', // dsrv + '0xa07e213e0985b21a6128e6c22ab5fb73948b0cc2', // everstake + '0x73853ed9a5f6f2e4c521970a94d43469e3cdaea6', // zee prime + '0xbd2e136cda02ba627ca882e49b184cbe976081c8', // staked + '0x1418126f944a44dad9edbab32294a8c890e7a9e3', // zkv + ], + }, + polygon: { + threshold: 4, + validators: [ + '0x59a001c3451e7f9f3b4759ea215382c1e9aa5fc1', // abacus + '0x009fb042d28944017177920c1d40da02bfebf474', // dsrv + '0xba4b13e23705a5919c1901150d9697e8ffb3ea71', // everstake + '0x2faa4071b718972f9b4beec1d8cbaa4eb6cca6c6', // zee prime + '0x5ae9b0f833dfe09ef455562a1f603f1634504dd6', // staked + '0x6a163d312f7352a95c9b81dca15078d5bf77a442', // zkv + ], + }, + bsc: { + threshold: 4, + validators: [ + '0xcc84b1eb711e5076b2755cf4ad1d2b42c458a45e', // abacus + '0xefe34eae2bca1846b895d2d0762ec21796aa196a', // dsrv + '0x662674e80e189b0861d6835c287693f50ee0c2ff', // everstake + '0x8a0f59075af466841808c529624807656309c9da', // zee prime + '0xdd2ff046ccd748a456b4757a73d47f165469669f', // staked + '0x034c4924c30ec4aa1b7f3ad58548988f0971e1bf', // zkv + ], + }, + arbitrum: { + threshold: 4, + validators: [ + '0xbcb815f38d481a5eba4d7ac4c9e74d9d0fc2a7e7', // abacus + '0xd839424e2e5ace0a81152298dc2b1e3bb3c7fb20', // dsrv + '0xb8085c954b75b7088bcce69e61d12fcef797cd8d', // everstake + '0x9856dcb10fd6e5407fa74b5ab1d3b96cc193e9b7', // zee prime + '0x505dff4e0827aa5065f5e001db888e0569d46490', // staked + '0x25c6779d4610f940bf2488732e10bcffb9d36f81', // ZKV + ], + }, + optimism: { + threshold: 4, + validators: [ + '0x9f2296d5cfc6b5176adc7716c7596898ded13d35', // abacus + '0x9c10bbe8efa03a8f49dfdb5c549258e3a8dca097', // dsrv + '0x62144d4a52a0a0335ea5bb84392ef9912461d9dd', // everstake + '0xaff4718d5d637466ad07441ee3b7c4af8e328dbd', // zee prime + '0xc64d1efeab8ae222bc889fe669f75d21b23005d9', // staked + '0xfa174eb2b4921bb652bc1ada3e8b00e7e280bf3c', // ZKV + ], + }, + moonbeam: { + threshold: 3, + validators: [ + '0x237243d32d10e3bdbbf8dbcccc98ad44c1c172ea', // abacus + '0x9509c8cf0a06955f27342262af501b74874e98fb', // dsrv + '0xb7113c999e4d587b162dd1a28c73f3f51c6bdcdc', // everstake + '0x26725501597d47352a23cd26f122709f69ad53bc', // staked + ], + }, + gnosis: { + threshold: 2, + validators: [ + '0xd0529ec8df08d0d63c0f023786bfa81e4bb51fd6', // abacus + '0x829d6ec129bc7187fb1ed161adcf7939fe0c515f', // abacus + '0x00009f8935e94bfe52ab3441df3526ab7cc38db1', // abacus + ], + }, + // ----------------- Testnets ----------------- + alfajores: { + threshold: 2, + validators: [ + '0xe6072396568e73ce6803b12b7e04164e839f1e54', + '0x9f177f51289b22515f41f95872e1511391b8e105', + '0x15f77400845eb1c971ad08de050861d5508cad6c', + ], + }, + fuji: { + threshold: 2, + validators: [ + '0x9fa19ead5ec76e437948b35e227511b106293c40', + '0x227e7d6507762ece0c94678f8c103eff9d682476', + '0x2379e43740e4aa4fde48cf4f00a3106df1d8420d', + ], + }, + mumbai: { + threshold: 2, + validators: [ + '0x0a664ea799447da6b15645cf8b9e82072a68343f', + '0x6ae6f12929a960aba24ba74ea310e3d37d0ac045', + '0x51f70c047cd73bc7873273707501568857a619c4', + ], + }, + bsctestnet: { + threshold: 2, + validators: [ + '0x23338c8714976dd4a57eaeff17cbd26d7e275c08', + '0x85a618d7450ebc37e0d682371f08dac94eec7a76', + '0x95b76562e4ba1791a27ba4236801271c9115b141', + ], + }, + goerli: { + threshold: 2, + validators: [ + '0xf43fbd072fd38e1121d4b3b0b8a35116bbb01ea9', + '0xa33020552a21f35e75bd385c6ab95c3dfa82d930', + '0x0bba4043ff242f8bf3f39bafa8930a84d644d947', + ], + }, + sepolia: { + threshold: 2, + validators: [ + '0xbc748ee311f5f2d1975d61cdf531755ce8ce3066', + '0xc4233b2bfe5aec08964a94b403052abb3eafcf07', + '0x6b36286c19f5c10bdc139ea9ee7f82287303f61d', + ], + }, + moonbasealpha: { + threshold: 2, + validators: [ + '0x890c2aeac157c3f067f3e42b8afc797939c59a32', + '0x1b06d6fe69b972ed7420c83599d5a5c0fc185904', + '0xe70b85206a968a99a597581f0fa09c99e7681093', + ], + }, + optimismgoerli: { + threshold: 2, + validators: [ + '0xbb8d77eefbecc55db6e5a19b0fc3dc290776f189', + '0x69792508b4ddaa3ca52241ccfcd1e0b119a1ee65', + '0x11ddb46c6b653e0cdd7ad5bee32ae316e18f8453', + ], + }, + arbitrumgoerli: { + threshold: 2, + validators: [ + '0xce798fa21e323f6b24d9838a10ffecdefdfc4f30', + '0xa792d39dca4426927e0f00c1618d61c9cb41779d', + '0xdf181fcc11dfac5d01467e4547101a856dd5aa04', + ], + }, +}; diff --git a/typescript/sdk/src/contracts.ts b/typescript/sdk/src/contracts.ts index 5c0918cd4..c4fbe3603 100644 --- a/typescript/sdk/src/contracts.ts +++ b/typescript/sdk/src/contracts.ts @@ -5,7 +5,7 @@ import type { types } from '@hyperlane-xyz/utils'; import { MultiProvider } from './providers/MultiProvider'; import { ProxiedContract, ProxyAddresses, isProxyAddresses } from './proxy'; import { ChainMap, Connection } from './types'; -import { objMap } from './utils/objects'; +import { isObject, objMap } from './utils/objects'; export type HyperlaneFactories = { [key: string]: ethers.ContractFactory; @@ -55,14 +55,50 @@ function getFactory( return factories[key]; } +function isAddress(addressOrObject: any) { + return ( + isProxyAddresses(addressOrObject) || typeof addressOrObject === 'string' + ); +} + +export function filterAddresses( + addressOrObject: HyperlaneAddresses, + contractNames: string[], + max_depth = 5, +): HyperlaneAddresses { + if (max_depth === 0) { + throw new Error('filterAddresses tried to go too deep'); + } + const ret: HyperlaneAddresses = {}; + for (const key of Object.keys(addressOrObject)) { + if (isAddress(addressOrObject[key]) && contractNames.includes(key)) { + ret[key] = addressOrObject[key]; + } else if (isObject(addressOrObject[key])) { + const obj = filterAddresses( + addressOrObject[key] as HyperlaneAddresses, + contractNames, + max_depth - 1, + ); + if (Object.keys(obj).length > 0) { + ret[key] = obj; + } + } + } + return ret; +} + export function buildContracts( addressOrObject: HyperlaneAddresses, factories: HyperlaneFactories, + filter = true, max_depth = 5, ): HyperlaneContracts { if (max_depth === 0) { throw new Error('buildContracts tried to go too deep'); } + if (filter) { + addressOrObject = filterAddresses(addressOrObject, Object.keys(factories)); + } return objMap(addressOrObject, (key, address: any) => { if (isProxyAddresses(address)) { const contract = getFactory(key, factories).attach(address.proxy); @@ -73,6 +109,7 @@ export function buildContracts( return buildContracts( address as HyperlaneAddresses, factories, + false, max_depth - 1, ); } diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index 4c9d6ba5e..bce03ccaf 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -4,24 +4,17 @@ import { Mailbox, Mailbox__factory } from '@hyperlane-xyz/core'; import { types, utils } from '@hyperlane-xyz/utils'; import { HyperlaneApp } from '../HyperlaneApp'; -import { environments } from '../consts/environments'; -import { buildContracts } from '../contracts'; +import { + HyperlaneEnvironment, + hyperlaneEnvironments, +} from '../consts/environments'; +import { HyperlaneAddresses } from '../contracts'; import { MultiProvider } from '../providers/MultiProvider'; -import { ConnectionClientConfig } from '../router/types'; import { ChainMap, ChainName } from '../types'; -import { objMap, pick } from '../utils/objects'; import { CoreContracts, coreFactories } from './contracts'; -export type CoreEnvironment = keyof typeof environments; -export type CoreEnvironmentChain = Extract< - keyof typeof environments[E], - ChainName ->; - -export type CoreContractsMap = { - [chain: ChainName]: CoreContracts; -}; +export type CoreContractsMap = ChainMap; export type DispatchedMessage = { id: string; @@ -34,61 +27,34 @@ export class HyperlaneCore extends HyperlaneApp { super(contractsMap, multiProvider); } - static fromEnvironment( + static fromAddresses( + addresses: ChainMap, + multiProvider: MultiProvider, + ): HyperlaneCore { + const { contracts, intersectionProvider } = + this.buildContracts( + addresses, + coreFactories, + multiProvider, + ); + return new HyperlaneCore(contracts, intersectionProvider); + } + + static fromEnvironment( env: Env, multiProvider: MultiProvider, ): HyperlaneCore { - const envConfig = environments[env]; - if (!envConfig) { - throw new Error(`No default env config found for ${env}`); + const envAddresses = hyperlaneEnvironments[env]; + if (!envAddresses) { + throw new Error(`No addresses found for ${env}`); } - - const envChains = Object.keys(envConfig); - - const { intersection, multiProvider: intersectionProvider } = - multiProvider.intersect(envChains, true); - - const intersectionConfig = pick(envConfig, intersection); - const contractsMap = buildContracts( - intersectionConfig, - coreFactories, - ) as CoreContractsMap; - - return new HyperlaneCore(contractsMap, intersectionProvider); + return HyperlaneCore.fromAddresses(envAddresses, multiProvider); } getContracts(chain: ChainName): CoreContracts { return super.getContracts(chain); } - getConnectionClientConfig(chain: ChainName): ConnectionClientConfig { - const contracts = this.getContracts(chain); - return { - mailbox: contracts.mailbox.address, - // TODO allow these to be more easily changed - interchainGasPaymaster: - contracts.defaultIsmInterchainGasPaymaster.address, - }; - } - - getConnectionClientConfigMap(): ChainMap { - return objMap(this.contractsMap, (chain) => - this.getConnectionClientConfig(chain), - ); - } - - extendWithConnectionClientConfig( - configMap: ChainMap, - ): ChainMap { - const connectionClientConfigMap = this.getConnectionClientConfigMap(); - return objMap(configMap, (chain, config) => { - return { - ...config, - ...connectionClientConfigMap[chain], - }; - }); - } - protected getDestination(message: DispatchedMessage): { destinationChain: ChainName; mailbox: Mailbox; diff --git a/typescript/sdk/src/core/HyperlaneCoreChecker.ts b/typescript/sdk/src/core/HyperlaneCoreChecker.ts index a4e246eef..e62ecd619 100644 --- a/typescript/sdk/src/core/HyperlaneCoreChecker.ts +++ b/typescript/sdk/src/core/HyperlaneCoreChecker.ts @@ -1,8 +1,8 @@ -import { BigNumber, utils as ethersUtils } from 'ethers'; +import { utils as ethersUtils } from 'ethers'; -import { types, utils } from '@hyperlane-xyz/utils'; +import { utils } from '@hyperlane-xyz/utils'; -import multisigIsmVerifyCosts from '../consts/multisigIsmVerifyCosts.json'; +import { BytecodeHash } from '../consts/bytecode'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker'; import { ChainName } from '../types'; @@ -10,13 +10,7 @@ import { HyperlaneCore } from './HyperlaneCore'; import { CoreConfig, CoreViolationType, - DefaultIsmIgpDestinationGasOverheadsViolation, - DefaultIsmIgpViolationType, EnrolledValidatorsViolation, - GasOracleContractType, - IgpBeneficiaryViolation, - IgpGasOraclesViolation, - IgpViolationType, MailboxViolation, MailboxViolationType, MultisigIsmViolationType, @@ -24,23 +18,6 @@ import { ValidatorAnnounceViolation, } from './types'; -const MAILBOX_WITHOUT_LOCAL_DOMAIN_BYTE_CODE_HASH = - '0x29b7294ab3ad2e8587e5cce0e2289ce65e12a2ea2f1e7ab34a05e7737616f457'; -const MAILBOX_WITHOUT_LOCAL_DOMAIN_NONZERO_PAUSE_BYTE_CODE_HASH = - '0x4e73e34c0982b93eebb4ac4889e9e4e1611f7c24feacf016c3a13e389f146d9c'; -const TRANSPARENT_PROXY_BYTECODE_HASH = - '0x4dde3d0906b6492bf1d4947f667afe8d53c8899f1d8788cabafd082938dceb2d'; -const MULTISIG_ISM_BYTECODE_HASH = - '0x5565704ffa5b10fdf37d57abfddcf137101d5fb418ded21fa6c5f90262c57dc2'; -const PROXY_ADMIN_BYTECODE_HASH = - '0x7c378e9d49408861ca754fe684b9f7d1ea525bddf095ee0463902df701453ba0'; -const INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH = - '0xe995bcd732f4861606036357edb2a4d4c3e9b8d7e599fe548790ac1cf26888f8'; -const OWNER_INITIALIZABLE_INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH = - '0xd2c5b00ac2d058117491d581d63c3c4fcf6aeb2667c6cc0c7caed359c9eebea1'; -const OVERHEAD_IGP_BYTECODE_HASH = - '0x3cfed1f24f1e9b28a76d5a8c61696a04f7bc474404b823a2fcc210ea52346252'; - export class HyperlaneCoreChecker extends HyperlaneAppChecker< HyperlaneCore, CoreConfig @@ -58,8 +35,6 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< await this.checkMultisigIsm(chain); await this.checkBytecodes(chain); await this.checkValidatorAnnounce(chain); - await this.checkDefaultIsmInterchainGasPaymaster(chain); - await this.checkInterchainGasPaymaster(chain); } async checkDomainOwnership(chain: ChainName): Promise { @@ -70,8 +45,6 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< contracts.proxyAdmin, contracts.mailbox.contract, contracts.multisigIsm, - contracts.interchainGasPaymaster.contract, - contracts.defaultIsmInterchainGasPaymaster, ]; return this.checkOwnership(chain, config.owner, ownables); } @@ -108,16 +81,16 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< 'Mailbox implementation', contracts.mailbox.addresses.implementation, [ - MAILBOX_WITHOUT_LOCAL_DOMAIN_BYTE_CODE_HASH, - MAILBOX_WITHOUT_LOCAL_DOMAIN_NONZERO_PAUSE_BYTE_CODE_HASH, + BytecodeHash.MAILBOX_WITHOUT_LOCAL_DOMAIN_BYTE_CODE_HASH, + BytecodeHash.MAILBOX_WITHOUT_LOCAL_DOMAIN_NONZERO_PAUSE_BYTE_CODE_HASH, ], - (_) => + (bytecode) => // This is obviously super janky but basically we are searching // for the ocurrences of localDomain in the bytecode and remove // that to compare, but some coincidental ocurrences of // localDomain in the bytecode should be not be removed which // are just done via an offset guard - _.replaceAll( + bytecode.replaceAll( ethersUtils.defaultAbiCoder .encode(['uint32'], [localDomain]) .slice(2), @@ -129,52 +102,19 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< chain, 'Mailbox proxy', contracts.mailbox.address, - [TRANSPARENT_PROXY_BYTECODE_HASH], - ); - await this.checkBytecode( - chain, - 'InterchainGasPaymaster proxy', - contracts.interchainGasPaymaster.address, - [TRANSPARENT_PROXY_BYTECODE_HASH], + [BytecodeHash.TRANSPARENT_PROXY_BYTECODE_HASH], ); await this.checkBytecode( chain, 'ProxyAdmin', contracts.proxyAdmin.address, - [PROXY_ADMIN_BYTECODE_HASH], + [BytecodeHash.PROXY_ADMIN_BYTECODE_HASH], ); await this.checkBytecode( chain, 'MultisigIsm implementation', contracts.multisigIsm.address, - [MULTISIG_ISM_BYTECODE_HASH], - ); - await this.checkBytecode( - chain, - 'InterchainGasPaymaster implementation', - contracts.interchainGasPaymaster.addresses.implementation, - [ - INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH, - OWNER_INITIALIZABLE_INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH, - ], - ); - - await this.checkBytecode( - chain, - 'OverheadIGP', - contracts.defaultIsmInterchainGasPaymaster.address, - [OVERHEAD_IGP_BYTECODE_HASH], - (_) => - // Remove the address of the wrapped ISM from the bytecode - _.replaceAll( - ethersUtils.defaultAbiCoder - .encode( - ['address'], - [contracts.interchainGasPaymaster.addresses.proxy], - ) - .slice(2), - '', - ), + [BytecodeHash.MULTISIG_ISM_BYTECODE_HASH], ); } @@ -186,20 +126,21 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< contracts.mailbox.addresses, contracts.proxyAdmin.address, ); - await this.checkProxiedContract( - chain, - 'InterchainGasPaymaster', - contracts.interchainGasPaymaster.addresses, - contracts.proxyAdmin.address, - ); } async checkValidatorAnnounce(chain: ChainName): Promise { - const expectedValidators = this.configMap[chain].multisigIsm.validators; + const expectedValidators = new Set(); + const remotes = Object.keys(this.configMap).filter((c) => c !== chain); + remotes.forEach((remote) => + this.configMap[remote].multisigIsm[chain].validators.forEach( + expectedValidators.add, + expectedValidators, + ), + ); const validatorAnnounce = this.app.getContracts(chain).validatorAnnounce; const announcedValidators = await validatorAnnounce.getAnnouncedValidators(); - expectedValidators.map((validator) => { + [...expectedValidators].forEach((validator) => { const matches = announcedValidators.filter((x) => utils.eqAddress(x, validator), ); @@ -230,10 +171,10 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< ): Promise { const coreContracts = this.app.getContracts(local); const multisigIsm = coreContracts.multisigIsm; - const config = this.configMap[remote]; + const config = this.configMap[local]; const remoteDomain = this.multiProvider.getDomainId(remote); - const multisigIsmConfig = config.multisigIsm; + const multisigIsmConfig = config.multisigIsm[remote]; const expectedValidators = multisigIsmConfig.validators; const actualValidators = await multisigIsm.validators(remoteDomain); @@ -275,126 +216,4 @@ export class HyperlaneCoreChecker extends HyperlaneAppChecker< this.addViolation(violation); } } - - async checkDefaultIsmInterchainGasPaymaster(local: ChainName): Promise { - const coreContracts = this.app.getContracts(local); - const defaultIsmIgp = coreContracts.defaultIsmInterchainGasPaymaster; - - // Construct the violation, updating the actual & expected - // objects as violations are found. - // A single violation is used so that only a single `setDestinationGasOverheads` - // call is generated to set multiple gas overheads. - const gasOverheadViolation: DefaultIsmIgpDestinationGasOverheadsViolation = - { - type: CoreViolationType.DefaultIsmInterchainGasPaymaster, - subType: DefaultIsmIgpViolationType.DestinationGasOverheads, - contract: defaultIsmIgp, - chain: local, - actual: {}, - expected: {}, - }; - - const remotes = this.app.remoteChains(local); - for (const remote of remotes) { - const { validators, threshold } = this.configMap[remote].multisigIsm; - const expectedOverhead = this.getExpectedOverheadGas( - threshold, - validators.length, - ); - - const remoteId = this.multiProvider.getDomainId(remote); - const existingOverhead = await defaultIsmIgp.destinationGasOverhead( - remoteId, - ); - if (!expectedOverhead.eq(existingOverhead)) { - const remoteChain = remote as ChainName; - gasOverheadViolation.actual[remoteChain] = existingOverhead; - gasOverheadViolation.expected[remoteChain] = expectedOverhead; - } - } - - if (Object.keys(gasOverheadViolation.actual).length > 0) { - this.addViolation(gasOverheadViolation); - } - } - - async checkInterchainGasPaymaster(local: ChainName): Promise { - const coreContracts = this.app.getContracts(local); - const igp = coreContracts.interchainGasPaymaster.contract; - - // Construct the violation, updating the actual & expected - // objects as violations are found. - // A single violation is used so that only a single `setGasOracles` - // call is generated to set multiple gas oracles. - const gasOraclesViolation: IgpGasOraclesViolation = { - type: CoreViolationType.InterchainGasPaymaster, - subType: IgpViolationType.GasOracles, - contract: igp, - chain: local, - actual: {}, - expected: {}, - }; - - const remotes = this.app.remoteChains(local); - for (const remote of remotes) { - const remoteId = this.multiProvider.getDomainId(remote); - const actualGasOracle = await igp.gasOracles(remoteId); - const expectedGasOracle = this.getGasOracleAddress(local, remote); - - if (!utils.eqAddress(actualGasOracle, expectedGasOracle)) { - const remoteChain = remote as ChainName; - gasOraclesViolation.actual[remoteChain] = actualGasOracle; - gasOraclesViolation.expected[remoteChain] = expectedGasOracle; - } - } - // Add the violation only if it's been populated with gas oracle inconsistencies - if (Object.keys(gasOraclesViolation.actual).length > 0) { - this.addViolation(gasOraclesViolation); - } - - const actualBeneficiary = await igp.beneficiary(); - const expectedBeneficiary = this.configMap[local].igp.beneficiary; - if (!utils.eqAddress(actualBeneficiary, expectedBeneficiary)) { - const violation: IgpBeneficiaryViolation = { - type: CoreViolationType.InterchainGasPaymaster, - subType: IgpViolationType.Beneficiary, - contract: igp, - chain: local, - actual: actualBeneficiary, - expected: expectedBeneficiary, - }; - this.addViolation(violation); - } - } - - getGasOracleAddress(local: ChainName, remote: ChainName): types.Address { - const config = this.configMap[local]; - const gasOracleType = config.igp.gasOracles[remote]; - if (!gasOracleType) { - throw Error( - `Expected gas oracle type for local ${local} and remote ${remote}`, - ); - } - const coreContracts = this.app.getContracts(local); - switch (gasOracleType) { - case GasOracleContractType.StorageGasOracle: - return coreContracts.storageGasOracle.address; - default: - throw Error(`Unsupported gas oracle type ${gasOracleType}`); - } - } - - private getExpectedOverheadGas( - threshold: number, - validatorSetCount: number, - ): BigNumber { - const expectedOverhead: number | undefined = - // @ts-ignore - multisigIsmVerifyCosts[`${validatorSetCount}`][`${threshold}`]; - if (!expectedOverhead) - throw new Error( - `Unknown verification cost for ${threshold} of ${validatorSetCount}`, - ); - return BigNumber.from(expectedOverhead); - } } diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts index 112226404..2581703b4 100644 --- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts +++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts @@ -2,27 +2,22 @@ import debug from 'debug'; import { ethers } from 'ethers'; import { - InterchainGasPaymaster, LegacyMultisigIsm, Mailbox, - OverheadIgp, Ownable, - Ownable__factory, ProxyAdmin, - StorageGasOracle, ValidatorAnnounce, } from '@hyperlane-xyz/core'; -import { types, utils } from '@hyperlane-xyz/utils'; +import { types } from '@hyperlane-xyz/utils'; -import multisigIsmVerifyCosts from '../consts/multisigIsmVerifyCosts.json'; import { DeployOptions, HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; import { MultiProvider } from '../providers/MultiProvider'; import { ProxiedContract, TransparentProxyAddresses } from '../proxy'; import { ChainMap, ChainName } from '../types'; import { objMap } from '../utils/objects'; -import { CoreContracts, GasOracleContracts, coreFactories } from './contracts'; -import { CoreConfig, GasOracleContractType } from './types'; +import { CoreContracts, coreFactories } from './contracts'; +import { CoreConfig } from './types'; export class HyperlaneCoreDeployer extends HyperlaneDeployer< CoreConfig, @@ -30,7 +25,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< typeof coreFactories > { startingBlockNumbers: ChainMap; - gasOverhead: ChainMap; constructor( multiProvider: MultiProvider, @@ -40,154 +34,9 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< super(multiProvider, configMap, factoriesOverride, { logger: debug('hyperlane:CoreDeployer'), }); - this.gasOverhead = objMap(configMap, (chain, config) => { - const { validators, threshold } = config.multisigIsm; - const verifyCost = - // @ts-ignore - multisigIsmVerifyCosts[`${validators.length}`][`${threshold}`]; - if (!verifyCost) - throw new Error( - `Unknown verification cost for ${threshold} of ${validators.length}`, - ); - return { - domain: multiProvider.getDomainId(chain), - gasOverhead: verifyCost, - }; - }); this.startingBlockNumbers = objMap(configMap, () => undefined); } - async deployInterchainGasPaymaster( - chain: ChainName, - proxyAdmin: ProxyAdmin, - gasOracleContracts: GasOracleContracts, - deployOpts?: DeployOptions, - ): Promise< - ProxiedContract - > { - const owner = this.configMap[chain].owner; - const beneficiary = this.configMap[chain].igp.beneficiary; - const igp = await this.deployProxiedContract( - chain, - 'interchainGasPaymaster', - [beneficiary], - proxyAdmin, - [owner, beneficiary], - deployOpts, - ); - - // Set the gas oracles - - const configChains = Object.keys(this.configMap); - const remotes = this.multiProvider - .intersect(configChains, false) - .multiProvider.getRemoteChains(chain); - - const gasOracleConfigsToSet: InterchainGasPaymaster.GasOracleConfigStruct[] = - []; - - for (const remote of remotes) { - const remoteId = this.multiProvider.getDomainId(remote); - const currentGasOracle = await igp.contract.gasOracles(remoteId); - const desiredGasOracle = this.getGasOracleAddress( - chain, - remote, - gasOracleContracts, - ); - if (!utils.eqAddress(currentGasOracle, desiredGasOracle)) { - gasOracleConfigsToSet.push({ - remoteDomain: remoteId, - gasOracle: desiredGasOracle, - }); - } - } - - if (gasOracleConfigsToSet.length > 0) { - await this.runIfOwner(chain, igp.contract, async () => - this.multiProvider.handleTx( - chain, - igp.contract.setGasOracles(gasOracleConfigsToSet), - ), - ); - } - - return igp; - } - - async deployDefaultIsmInterchainGasPaymaster( - chain: ChainName, - interchainGasPaymasterAddress: types.Address, - deployOpts?: DeployOptions, - ): Promise { - const deployer = await this.multiProvider.getSignerAddress(chain); - // Transfer ownership to the deployer so the destination gas overheads can be set - const initCalldata = Ownable__factory.createInterface().encodeFunctionData( - 'transferOwnership', - [deployer], - ); - const defaultIsmInterchainGasPaymaster = await this.deployContract( - chain, - 'defaultIsmInterchainGasPaymaster', - [interchainGasPaymasterAddress], - { - ...deployOpts, - initCalldata, - }, - ); - - const configChains = Object.keys(this.configMap); - const remotes = this.multiProvider - .intersect(configChains, false) - .multiProvider.getRemoteChains(chain); - - // Only set gas overhead configs if they differ from what's on chain - const configs: OverheadIgp.DomainConfigStruct[] = []; - for (const remote of remotes) { - const gasOverhead = this.gasOverhead[remote]; - const existingOverhead = - await defaultIsmInterchainGasPaymaster.destinationGasOverhead( - gasOverhead.domain, - ); - if (!existingOverhead.eq(gasOverhead.gasOverhead)) { - configs.push(gasOverhead); - } - } - - if (configs.length > 0) { - await this.runIfOwner(chain, defaultIsmInterchainGasPaymaster, () => - this.multiProvider.handleTx( - chain, - defaultIsmInterchainGasPaymaster.setDestinationGasOverheads( - configs, - this.multiProvider.getTransactionOverrides(chain), - ), - ), - ); - } - - return defaultIsmInterchainGasPaymaster; - } - - async deployGasOracleContracts( - chain: ChainName, - deployOpts?: DeployOptions, - ): Promise { - const storageGasOracle = await this.deployStorageGasOracle( - chain, - deployOpts, - ); - return { - storageGasOracle, - }; - } - - async deployStorageGasOracle( - chain: ChainName, - deployOpts?: DeployOptions, - ): Promise { - return this.deployContract(chain, 'storageGasOracle', [], deployOpts); - } - async deployMailbox( chain: ChainName, defaultIsmAddress: types.Address, @@ -224,10 +73,7 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< async deployLegacyMultisigIsm(chain: ChainName): Promise { const multisigIsm = await this.deployContract(chain, 'multisigIsm', []); - const configChains = Object.keys(this.configMap); - const remotes = this.multiProvider - .intersect(configChains, false) - .multiProvider.getRemoteChains(chain); + const remotes = Object.keys(this.configMap[chain].multisigIsm); const overrides = this.multiProvider.getTransactionOverrides(chain); await super.runIfOwner(chain, multisigIsm, async () => { @@ -237,7 +83,7 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< remoteDomains.map((id) => multisigIsm.validators(id)), ); const expectedValidators = remotes.map( - (chain) => this.configMap[chain].multisigIsm.validators, + (remote) => this.configMap[chain].multisigIsm[remote].validators, ); const validatorsToEnroll = expectedValidators.map((validators, i) => validators.filter( @@ -248,12 +94,10 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< const chainsToEnrollValidators = remotes.filter( (_, i) => validatorsToEnroll[i].length > 0, ); - if (chainsToEnrollValidators.length > 0) { this.logger( `Enroll ${chainsToEnrollValidators} validators on ${chain}`, ); - await this.multiProvider.handleTx( chain, multisigIsm.enrollValidators( @@ -265,12 +109,11 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< ), ); } - const actualThresholds = await Promise.all( remoteDomains.map((id) => multisigIsm.threshold(id)), ); const expectedThresholds = remotes.map( - (chain) => this.configMap[chain].multisigIsm.threshold, + (remote) => this.configMap[chain].multisigIsm[remote].threshold, ); const chainsToSetThreshold = remotes.filter( (_, i) => actualThresholds[i] !== expectedThresholds[i], @@ -284,14 +127,13 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< multisigIsm.setThresholds( chainsToSetThreshold.map((c) => this.multiProvider.getDomainId(c)), chainsToSetThreshold.map( - (c) => this.configMap[c].multisigIsm.threshold, + (remote) => this.configMap[chain].multisigIsm[remote].threshold, ), overrides, ), ); } }); - return multisigIsm; } @@ -311,17 +153,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< const proxyAdmin = await this.deployContract(chain, 'proxyAdmin', []); - const gasOracleContracts = await this.deployGasOracleContracts(chain); - const interchainGasPaymaster = await this.deployInterchainGasPaymaster( - chain, - proxyAdmin, - gasOracleContracts, - ); - const defaultIsmInterchainGasPaymaster = - await this.deployDefaultIsmInterchainGasPaymaster( - chain, - interchainGasPaymaster.address, - ); const mailbox = await this.deployMailbox( chain, multisigIsm.address, @@ -332,68 +163,14 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< mailbox.address, ); // Ownership of the Mailbox and the interchainGasPaymaster is transferred upon initialization. - const ownables: Ownable[] = [ - multisigIsm, - proxyAdmin, - defaultIsmInterchainGasPaymaster, - ]; - await this.transferOwnershipOfContracts(chain, ownables); + const ownables: Ownable[] = [multisigIsm, proxyAdmin]; + await this.transferOwnershipOfContracts(chain, config.owner, ownables); return { - ...gasOracleContracts, validatorAnnounce, proxyAdmin, mailbox, - interchainGasPaymaster, - defaultIsmInterchainGasPaymaster, multisigIsm, }; } - - async transferOwnershipOfContracts( - chain: ChainName, - ownables: Ownable[], - ): Promise { - const owner = this.configMap[chain].owner; - const receipts: ethers.ContractReceipt[] = []; - for (const ownable of ownables) { - const currentOwner = await ownable.owner(); - if (currentOwner.toLowerCase() !== owner.toLowerCase()) { - const receipt = await super.runIfOwner(chain, ownable, () => - this.multiProvider.handleTx( - chain, - ownable.transferOwnership( - owner, - this.multiProvider.getTransactionOverrides(chain), - ), - ), - ); - if (receipt) receipts.push(receipt); - } - } - - return receipts.filter((x) => x !== undefined) as ethers.ContractReceipt[]; - } - - private getGasOracleAddress( - local: ChainName, - remote: ChainName, - gasOracleContracts: GasOracleContracts, - ): types.Address { - const localConfig = this.configMap[local]; - const gasOracleType = localConfig.igp.gasOracles[remote]; - if (!gasOracleType) { - throw Error( - `Expected gas oracle type for local ${local} and remote ${remote}`, - ); - } - switch (gasOracleType) { - case GasOracleContractType.StorageGasOracle: { - return gasOracleContracts.storageGasOracle.address; - } - default: { - throw Error(`Unsupported gas oracle type ${gasOracleType}`); - } - } - } } diff --git a/typescript/sdk/src/core/TestCoreDeployer.ts b/typescript/sdk/src/core/TestCoreDeployer.ts index a364b6040..2158db143 100644 --- a/typescript/sdk/src/core/TestCoreDeployer.ts +++ b/typescript/sdk/src/core/TestCoreDeployer.ts @@ -7,32 +7,15 @@ import { TestMailbox__factory, } from '@hyperlane-xyz/core'; +import { TestChains } from '../consts/chains'; import { MultiProvider } from '../providers/MultiProvider'; +import { testCoreConfig } from '../test/testUtils'; import { ChainMap, ChainName } from '../types'; import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer'; import { TestCoreApp } from './TestCoreApp'; import { coreFactories } from './contracts'; -import { CoreConfig, GasOracleContractType } from './types'; - -const nonZeroAddress = ethers.constants.AddressZero.replace('00', '01'); - -// dummy config as TestInbox and TestOutbox do not use deployed ISM -const testConfig: CoreConfig = { - owner: nonZeroAddress, - multisigIsm: { - validators: [nonZeroAddress], - threshold: 1, - }, - igp: { - beneficiary: nonZeroAddress, - gasOracles: { - test1: GasOracleContractType.StorageGasOracle, - test2: GasOracleContractType.StorageGasOracle, - test3: GasOracleContractType.StorageGasOracle, - }, - }, -}; +import { CoreConfig } from './types'; const testCoreFactories = { ...coreFactories, @@ -47,11 +30,7 @@ export class TestCoreDeployer extends HyperlaneCoreDeployer { configMap?: ChainMap, ) { // Note that the multisig module configs are unused. - const configs = configMap ?? { - test1: testConfig, - test2: testConfig, - test3: testConfig, - }; + const configs = configMap ?? testCoreConfig(TestChains); super(multiProvider, configs, testCoreFactories); } diff --git a/typescript/sdk/src/core/contracts.ts b/typescript/sdk/src/core/contracts.ts index 25bb3d6d9..7477dab2a 100644 --- a/typescript/sdk/src/core/contracts.ts +++ b/typescript/sdk/src/core/contracts.ts @@ -1,54 +1,40 @@ import { Create2Factory__factory, InterchainAccountRouter__factory, - InterchainGasPaymaster, - InterchainGasPaymaster__factory, InterchainQueryRouter__factory, LegacyMultisigIsm, LegacyMultisigIsm__factory, Mailbox, Mailbox__factory, - OverheadIgp, - OverheadIgp__factory, ProxyAdmin, ProxyAdmin__factory, - StorageGasOracle, - StorageGasOracle__factory, ValidatorAnnounce, ValidatorAnnounce__factory, } from '@hyperlane-xyz/core'; +import { types } from '@hyperlane-xyz/utils'; import { ProxiedContract, TransparentProxyAddresses } from '../proxy'; -export type GasOracleContracts = { - storageGasOracle: StorageGasOracle; +export type CoreAddresses = { + mailbox: types.Address | TransparentProxyAddresses; + multisigIsm: types.Address; + proxyAdmin: types.Address; + validatorAnnounce: types.Address; }; -export type ConnectionClientContracts = { - interchainGasPaymaster: ProxiedContract< - InterchainGasPaymaster, - TransparentProxyAddresses - >; - defaultIsmInterchainGasPaymaster: OverheadIgp; +export type CoreContracts = { + mailbox: ProxiedContract; + multisigIsm: LegacyMultisigIsm; + proxyAdmin: ProxyAdmin; + validatorAnnounce: ValidatorAnnounce; }; -export type CoreContracts = GasOracleContracts & - ConnectionClientContracts & { - mailbox: ProxiedContract; - multisigIsm: LegacyMultisigIsm; - proxyAdmin: ProxyAdmin; - validatorAnnounce: ValidatorAnnounce; - }; - export const coreFactories = { interchainAccountRouter: new InterchainAccountRouter__factory(), interchainQueryRouter: new InterchainQueryRouter__factory(), validatorAnnounce: new ValidatorAnnounce__factory(), create2Factory: new Create2Factory__factory(), proxyAdmin: new ProxyAdmin__factory(), - interchainGasPaymaster: new InterchainGasPaymaster__factory(), - defaultIsmInterchainGasPaymaster: new OverheadIgp__factory(), - storageGasOracle: new StorageGasOracle__factory(), multisigIsm: new LegacyMultisigIsm__factory(), mailbox: new Mailbox__factory(), }; diff --git a/typescript/sdk/src/core/types.ts b/typescript/sdk/src/core/types.ts index bf5394152..0112fc137 100644 --- a/typescript/sdk/src/core/types.ts +++ b/typescript/sdk/src/core/types.ts @@ -1,34 +1,17 @@ -import { BigNumber } from 'ethers'; - -import { - InterchainGasPaymaster, - LegacyMultisigIsm, - Mailbox, - OverheadIgp, -} from '@hyperlane-xyz/core'; +import { LegacyMultisigIsm, Mailbox } from '@hyperlane-xyz/core'; import type { types } from '@hyperlane-xyz/utils'; import type { CheckerViolation } from '../deploy/types'; import { ChainMap, ChainName } from '../types'; -export enum GasOracleContractType { - StorageGasOracle = 'StorageGasOracle', -} - -export type InterchainGasPaymasterConfig = { - beneficiary: types.Address; - gasOracles: ChainMap; -}; - export type MultisigIsmConfig = { validators: Array; threshold: number; }; export type CoreConfig = { - multisigIsm: MultisigIsmConfig; + multisigIsm: ChainMap; owner: types.Address; - igp: InterchainGasPaymasterConfig; remove?: boolean; }; @@ -37,8 +20,6 @@ export enum CoreViolationType { Mailbox = 'Mailbox', ConnectionManager = 'ConnectionManager', ValidatorAnnounce = 'ValidatorAnnounce', - InterchainGasPaymaster = 'InterchainGasPaymaster', - DefaultIsmInterchainGasPaymaster = 'DefaultIsmInterchainGasPaymaster', } export enum MultisigIsmViolationType { @@ -50,15 +31,6 @@ export enum MailboxViolationType { DefaultIsm = 'DefaultIsm', } -export enum DefaultIsmIgpViolationType { - DestinationGasOverheads = 'DestinationGasOverheads', -} - -export enum IgpViolationType { - Beneficiary = 'Beneficiary', - GasOracles = 'GasOracles', -} - export interface MailboxViolation extends CheckerViolation { type: CoreViolationType.Mailbox; contract: Mailbox; @@ -96,34 +68,3 @@ export interface ValidatorAnnounceViolation extends CheckerViolation { actual: boolean; expected: boolean; } - -export interface IgpViolation extends CheckerViolation { - type: CoreViolationType.InterchainGasPaymaster; - contract: InterchainGasPaymaster; - subType: IgpViolationType; -} - -export interface IgpBeneficiaryViolation extends IgpViolation { - subType: IgpViolationType.Beneficiary; - actual: types.Address; - expected: types.Address; -} - -export interface IgpGasOraclesViolation extends IgpViolation { - subType: IgpViolationType.GasOracles; - actual: ChainMap; - expected: ChainMap; -} - -export interface DefaultIsmIgpViolation extends CheckerViolation { - type: CoreViolationType.DefaultIsmInterchainGasPaymaster; - contract: OverheadIgp; - subType: DefaultIsmIgpViolationType; -} - -export interface DefaultIsmIgpDestinationGasOverheadsViolation - extends DefaultIsmIgpViolation { - subType: DefaultIsmIgpViolationType.DestinationGasOverheads; - actual: ChainMap; - expected: ChainMap; -} diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index 009208df2..9999cf65e 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -10,7 +10,7 @@ import { TransparentUpgradeableProxy, TransparentUpgradeableProxy__factory, } from '@hyperlane-xyz/core'; -import { types } from '@hyperlane-xyz/utils'; +import { types, utils } from '@hyperlane-xyz/utils'; import { HyperlaneContract, @@ -510,4 +510,29 @@ export abstract class HyperlaneDeployer< } return ret; } + + protected async transferOwnershipOfContracts( + chain: ChainName, + owner: types.Address, + ownables: Ownable[], + ): Promise { + const receipts: ethers.ContractReceipt[] = []; + for (const ownable of ownables) { + const currentOwner = await ownable.owner(); + if (!utils.eqAddress(currentOwner, owner)) { + const receipt = await this.runIfOwner(chain, ownable, () => + this.multiProvider.handleTx( + chain, + ownable.transferOwnership( + owner, + this.multiProvider.getTransactionOverrides(chain), + ), + ), + ); + if (receipt) receipts.push(receipt); + } + } + + return receipts.filter((x) => !!x) as ethers.ContractReceipt[]; + } } diff --git a/typescript/sdk/src/deploy/utils.ts b/typescript/sdk/src/deploy/utils.ts deleted file mode 100644 index e31ee57aa..000000000 --- a/typescript/sdk/src/deploy/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { types } from '@hyperlane-xyz/utils'; - -import { ChainMap } from '../types'; -import { objMap } from '../utils/objects'; - -export function getChainToOwnerMap( - configMap: ChainMap, - owner: types.Address, -): ChainMap<{ owner: string }> { - return objMap(configMap, () => { - return { - owner, - }; - }); -} diff --git a/typescript/sdk/src/gas/HyperlaneIgp.ts b/typescript/sdk/src/gas/HyperlaneIgp.ts new file mode 100644 index 000000000..c3c265895 --- /dev/null +++ b/typescript/sdk/src/gas/HyperlaneIgp.ts @@ -0,0 +1,129 @@ +import { BigNumber } from 'ethers'; + +import { InterchainGasPaymaster__factory } from '@hyperlane-xyz/core'; +import { types } from '@hyperlane-xyz/utils'; + +import { HyperlaneApp } from '../HyperlaneApp'; +import { + HyperlaneEnvironment, + hyperlaneEnvironments, +} from '../consts/environments'; +import { HyperlaneAddresses } from '../contracts'; +import { MultiProvider } from '../providers/MultiProvider'; +import { ChainMap, ChainName } from '../types'; + +import { IgpContracts, igpFactories } from './contracts'; + +export type IgpContractsMap = ChainMap; + +export class HyperlaneIgp extends HyperlaneApp { + constructor(contractsMap: IgpContractsMap, multiProvider: MultiProvider) { + super(contractsMap, multiProvider); + } + + static fromAddresses( + addresses: ChainMap, + multiProvider: MultiProvider, + ): HyperlaneIgp { + const { contracts, intersectionProvider } = + this.buildContracts(addresses, igpFactories, multiProvider); + return new HyperlaneIgp(contracts, intersectionProvider); + } + + static fromEnvironment( + env: Env, + multiProvider: MultiProvider, + ): HyperlaneIgp { + const envAddresses = hyperlaneEnvironments[env]; + if (!envAddresses) { + throw new Error(`No addresses found for ${env}`); + } + return HyperlaneIgp.fromAddresses(envAddresses, multiProvider); + } + + getContracts(chain: ChainName): IgpContracts { + return super.getContracts(chain); + } + + /** + * Calls the default ISM IGP's `quoteGasPayment` function to get the amount of native tokens + * required to pay for interchain gas. + * The default ISM IGP will add any gas overhead amounts related to the Mailbox + * and default ISM on the destination to the provided gasAmount. + * @param origin The name of the origin chain. + * @param destination The name of the destination chain. + * @param gasAmount The amount of gas to use when calling `quoteGasPayment`. + * The default IGP is expected to add any gas overhead related to the Mailbox + * or ISM, so this gas amount is only required to cover the usage of the `handle` + * function. + * @returns The amount of native tokens required to pay for interchain gas. + */ + quoteGasPayment( + origin: ChainName, + destination: ChainName, + gasAmount: BigNumber, + ): Promise { + const igp = this.getContracts(origin).interchainGasPaymaster; + return this.quoteGasPaymentForIgp( + origin, + destination, + gasAmount, + igp.address, + ); + } + + /** + * Calls the default ISM IGP's `quoteGasPayment` function to get the amount of native tokens + * required to pay for interchain gas. + * The default ISM IGP will add any gas overhead amounts related to the Mailbox + * and default ISM on the destination to the provided gasAmount. + * @param origin The name of the origin chain. + * @param destination The name of the destination chain. + * @param gasAmount The amount of gas to use when calling `quoteGasPayment`. + * The default IGP is expected to add any gas overhead related to the Mailbox + * or ISM, so this gas amount is only required to cover the usage of the `handle` + * function. + * @returns The amount of native tokens required to pay for interchain gas. + */ + quoteGasPaymentForDefaultIsmIgp( + origin: ChainName, + destination: ChainName, + gasAmount: BigNumber, + ): Promise { + const igp = this.getContracts(origin).defaultIsmInterchainGasPaymaster; + return this.quoteGasPaymentForIgp( + origin, + destination, + gasAmount, + igp.address, + ); + } + + /** + * Calls the origin's default IGP's `quoteGasPayment` function to get the + * amount of native tokens required to pay for interchain gas. + * The default IGP is expected to add any gas overhead related to the Mailbox + * and ISM to the provided gasAmount. + * @param origin The name of the origin chain. + * @param destination The name of the destination chain. + * @param gasAmount The amount of gas to use when calling `quoteGasPayment`. + * The default IGP is expected to add any gas overhead related to the Mailbox + * or ISM, so this gas amount is only required to cover the usage of the `handle` + * function. + * @returns The amount of native tokens required to pay for interchain gas. + */ + protected quoteGasPaymentForIgp( + origin: ChainName, + destination: ChainName, + gasAmount: BigNumber, + interchainGasPaymasterAddress: types.Address, + ): Promise { + const originProvider = this.multiProvider.getProvider(origin); + const igp = InterchainGasPaymaster__factory.connect( + interchainGasPaymasterAddress, + originProvider, + ); + const domainId = this.multiProvider.getDomainId(destination); + return igp.quoteGasPayment(domainId, gasAmount); + } +} diff --git a/typescript/sdk/src/gas/HyperlaneIgpChecker.ts b/typescript/sdk/src/gas/HyperlaneIgpChecker.ts new file mode 100644 index 000000000..19dadb77a --- /dev/null +++ b/typescript/sdk/src/gas/HyperlaneIgpChecker.ts @@ -0,0 +1,194 @@ +import { BigNumber, utils as ethersUtils } from 'ethers'; + +import { types, utils } from '@hyperlane-xyz/utils'; + +import { BytecodeHash } from '../consts/bytecode'; +import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker'; +import { ChainName } from '../types'; + +import { HyperlaneIgp } from './HyperlaneIgp'; +import { + GasOracleContractType, + IgpBeneficiaryViolation, + IgpGasOraclesViolation, + IgpOverheadViolation, + IgpViolationType, + OverheadIgpConfig, +} from './types'; + +export class HyperlaneIgpChecker extends HyperlaneAppChecker< + HyperlaneIgp, + OverheadIgpConfig +> { + async checkChain(chain: ChainName): Promise { + await this.checkDomainOwnership(chain); + await this.checkProxiedContracts(chain); + await this.checkBytecodes(chain); + await this.checkOverheadInterchainGasPaymaster(chain); + await this.checkInterchainGasPaymaster(chain); + } + + async checkDomainOwnership(chain: ChainName): Promise { + const config = this.configMap[chain]; + if (config.owner) { + const contracts = this.app.getContracts(chain); + const ownables = [ + contracts.proxyAdmin, + contracts.interchainGasPaymaster.contract, + contracts.defaultIsmInterchainGasPaymaster, + ]; + return this.checkOwnership(chain, config.owner, ownables); + } + } + + async checkBytecodes(chain: ChainName): Promise { + const contracts = this.app.getContracts(chain); + await this.checkBytecode( + chain, + 'InterchainGasPaymaster proxy', + contracts.interchainGasPaymaster.address, + [BytecodeHash.TRANSPARENT_PROXY_BYTECODE_HASH], + ); + await this.checkBytecode( + chain, + 'InterchainGasPaymaster implementation', + contracts.interchainGasPaymaster.addresses.implementation, + [ + BytecodeHash.INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH, + BytecodeHash.OWNER_INITIALIZABLE_INTERCHAIN_GAS_PAYMASTER_BYTECODE_HASH, + ], + ); + + await this.checkBytecode( + chain, + 'OverheadIGP', + contracts.defaultIsmInterchainGasPaymaster.address, + [BytecodeHash.OVERHEAD_IGP_BYTECODE_HASH], + (bytecode) => + // Remove the address of the wrapped IGP from the bytecode + bytecode.replaceAll( + ethersUtils.defaultAbiCoder + .encode( + ['address'], + [contracts.interchainGasPaymaster.addresses.proxy], + ) + .slice(2), + '', + ), + ); + } + + async checkProxiedContracts(chain: ChainName): Promise { + const contracts = this.app.getContracts(chain); + await this.checkProxiedContract( + chain, + 'InterchainGasPaymaster', + contracts.interchainGasPaymaster.addresses, + contracts.proxyAdmin.address, + ); + } + + async checkOverheadInterchainGasPaymaster(local: ChainName): Promise { + const coreContracts = this.app.getContracts(local); + const defaultIsmIgp = coreContracts.defaultIsmInterchainGasPaymaster; + + // Construct the violation, updating the actual & expected + // objects as violations are found. + // A single violation is used so that only a single `setDestinationGasOverheads` + // call is generated to set multiple gas overheads. + const overheadViolation: IgpOverheadViolation = { + type: 'InterchainGasPaymaster', + subType: IgpViolationType.Overhead, + contract: defaultIsmIgp, + chain: local, + actual: {}, + expected: {}, + }; + + const remotes = this.app.remoteChains(local); + for (const remote of remotes) { + const expectedOverhead = this.configMap[local].overhead[remote]; + + const remoteId = this.multiProvider.getDomainId(remote); + const existingOverhead = await defaultIsmIgp.destinationGasOverhead( + remoteId, + ); + if (!existingOverhead.eq(expectedOverhead)) { + const remoteChain = remote as ChainName; + overheadViolation.actual[remoteChain] = existingOverhead; + overheadViolation.expected[remoteChain] = + BigNumber.from(expectedOverhead); + } + } + + if (Object.keys(overheadViolation.actual).length > 0) { + this.addViolation(overheadViolation); + } + } + + async checkInterchainGasPaymaster(local: ChainName): Promise { + const coreContracts = this.app.getContracts(local); + const igp = coreContracts.interchainGasPaymaster.contract; + + // Construct the violation, updating the actual & expected + // objects as violations are found. + // A single violation is used so that only a single `setGasOracles` + // call is generated to set multiple gas oracles. + const gasOraclesViolation: IgpGasOraclesViolation = { + type: 'InterchainGasPaymaster', + subType: IgpViolationType.GasOracles, + contract: igp, + chain: local, + actual: {}, + expected: {}, + }; + + const remotes = this.app.remoteChains(local); + for (const remote of remotes) { + const remoteId = this.multiProvider.getDomainId(remote); + const actualGasOracle = await igp.gasOracles(remoteId); + const expectedGasOracle = this.getGasOracleAddress(local, remote); + + if (!utils.eqAddress(actualGasOracle, expectedGasOracle)) { + const remoteChain = remote as ChainName; + gasOraclesViolation.actual[remoteChain] = actualGasOracle; + gasOraclesViolation.expected[remoteChain] = expectedGasOracle; + } + } + // Add the violation only if it's been populated with gas oracle inconsistencies + if (Object.keys(gasOraclesViolation.actual).length > 0) { + this.addViolation(gasOraclesViolation); + } + + const actualBeneficiary = await igp.beneficiary(); + const expectedBeneficiary = this.configMap[local].beneficiary; + if (!utils.eqAddress(actualBeneficiary, expectedBeneficiary)) { + const violation: IgpBeneficiaryViolation = { + type: 'InterchainGasPaymaster', + subType: IgpViolationType.Beneficiary, + contract: igp, + chain: local, + actual: actualBeneficiary, + expected: expectedBeneficiary, + }; + this.addViolation(violation); + } + } + + getGasOracleAddress(local: ChainName, remote: ChainName): types.Address { + const config = this.configMap[local]; + const gasOracleType = config.gasOracleType[remote]; + if (!gasOracleType) { + throw Error( + `Expected gas oracle type for local ${local} and remote ${remote}`, + ); + } + const coreContracts = this.app.getContracts(local); + switch (gasOracleType) { + case GasOracleContractType.StorageGasOracle: + return coreContracts.storageGasOracle.address; + default: + throw Error(`Unsupported gas oracle type ${gasOracleType}`); + } + } +} diff --git a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts new file mode 100644 index 000000000..8b6ff6653 --- /dev/null +++ b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts @@ -0,0 +1,184 @@ +import debug from 'debug'; + +import { + InterchainGasPaymaster, + OverheadIgp, + Ownable, + Ownable__factory, + ProxyAdmin, + StorageGasOracle, +} from '@hyperlane-xyz/core'; +import { types, utils } from '@hyperlane-xyz/utils'; + +import { DeployOptions, HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; +import { MultiProvider } from '../providers/MultiProvider'; +import { ProxiedContract, TransparentProxyAddresses } from '../proxy'; +import { ChainMap, ChainName } from '../types'; +import { objMap } from '../utils/objects'; + +import { IgpContracts, igpFactories } from './contracts'; +import { OverheadIgpConfig } from './types'; + +export class HyperlaneIgpDeployer extends HyperlaneDeployer< + OverheadIgpConfig, + IgpContracts, + typeof igpFactories +> { + startingBlockNumbers: ChainMap; + + constructor( + multiProvider: MultiProvider, + configMap: ChainMap, + factoriesOverride = igpFactories, + ) { + super(multiProvider, configMap, factoriesOverride, { + logger: debug('hyperlane:IgpDeployer'), + }); + this.startingBlockNumbers = objMap(configMap, () => undefined); + } + + async deployInterchainGasPaymaster( + chain: ChainName, + proxyAdmin: ProxyAdmin, + storageGasOracle: StorageGasOracle, + deployOpts?: DeployOptions, + ): Promise< + ProxiedContract + > { + const owner = this.configMap[chain].owner; + const beneficiary = this.configMap[chain].beneficiary; + const igp = await this.deployProxiedContract( + chain, + 'interchainGasPaymaster', + [beneficiary], + proxyAdmin, + [owner, beneficiary], + deployOpts, + ); + + // Set the gas oracles + const configChains = Object.keys(this.configMap); + const remotes = this.multiProvider + .intersect(configChains, false) + .multiProvider.getRemoteChains(chain); + + const gasOracleConfigsToSet: InterchainGasPaymaster.GasOracleConfigStruct[] = + []; + + for (const remote of remotes) { + const remoteId = this.multiProvider.getDomainId(remote); + const currentGasOracle = await igp.contract.gasOracles(remoteId); + if (!utils.eqAddress(currentGasOracle, storageGasOracle.address)) { + gasOracleConfigsToSet.push({ + remoteDomain: remoteId, + gasOracle: storageGasOracle.address, + }); + } + } + + if (gasOracleConfigsToSet.length > 0) { + await this.runIfOwner(chain, igp.contract, async () => + this.multiProvider.handleTx( + chain, + igp.contract.setGasOracles(gasOracleConfigsToSet), + ), + ); + } + return igp; + } + + async deployOverheadInterchainGasPaymaster( + chain: ChainName, + interchainGasPaymasterAddress: types.Address, + deployOpts?: DeployOptions, + ): Promise { + const deployer = await this.multiProvider.getSignerAddress(chain); + // Transfer ownership to the deployer so the destination gas overheads can be set + const initCalldata = Ownable__factory.createInterface().encodeFunctionData( + 'transferOwnership', + [deployer], + ); + const overheadInterchainGasPaymaster = await this.deployContract( + chain, + 'defaultIsmInterchainGasPaymaster', + [interchainGasPaymasterAddress], + { + ...deployOpts, + initCalldata, + }, + ); + + const configChains = Object.keys(this.configMap); + const remotes = this.multiProvider + .intersect(configChains, false) + .multiProvider.getRemoteChains(chain); + + // Only set gas overhead configs if they differ from what's on chain + const configs: OverheadIgp.DomainConfigStruct[] = []; + for (const remote of remotes) { + const remoteDomain = this.multiProvider.getDomainId(remote); + const gasOverhead = this.configMap[chain].overhead[remote]; + const existingOverhead = + await overheadInterchainGasPaymaster.destinationGasOverhead( + remoteDomain, + ); + if (!existingOverhead.eq(gasOverhead)) { + configs.push({ domain: remoteDomain, gasOverhead }); + } + } + + if (configs.length > 0) { + await this.runIfOwner(chain, overheadInterchainGasPaymaster, () => + this.multiProvider.handleTx( + chain, + overheadInterchainGasPaymaster.setDestinationGasOverheads( + configs, + this.multiProvider.getTransactionOverrides(chain), + ), + ), + ); + } + + return overheadInterchainGasPaymaster; + } + + async deployStorageGasOracle( + chain: ChainName, + deployOpts?: DeployOptions, + ): Promise { + return this.deployContract(chain, 'storageGasOracle', [], deployOpts); + } + + async deployContracts( + chain: ChainName, + config: OverheadIgpConfig, + ): Promise { + const provider = this.multiProvider.getProvider(chain); + const startingBlockNumber = await provider.getBlockNumber(); + this.startingBlockNumbers[chain] = startingBlockNumber; + // NB: To share ProxyAdmins with HyperlaneCore, ensure the ProxyAdmin + // is loaded into the contract cache. + const proxyAdmin = await this.deployContract(chain, 'proxyAdmin', []); + const storageGasOracle = await this.deployStorageGasOracle(chain); + const interchainGasPaymaster = await this.deployInterchainGasPaymaster( + chain, + proxyAdmin, + storageGasOracle, + ); + const overheadInterchainGasPaymaster = + await this.deployOverheadInterchainGasPaymaster( + chain, + interchainGasPaymaster.address, + ); + // Ownership of the Mailbox and the interchainGasPaymaster is transferred upon initialization. + const ownables: Ownable[] = [overheadInterchainGasPaymaster]; + await this.transferOwnershipOfContracts(chain, config.owner, ownables); + + return { + proxyAdmin, + storageGasOracle, + interchainGasPaymaster, + defaultIsmInterchainGasPaymaster: overheadInterchainGasPaymaster, + }; + } +} diff --git a/typescript/sdk/src/gas/calculator.hardhat-test.ts b/typescript/sdk/src/gas/calculator.hardhat-test.ts deleted file mode 100644 index e1cee2a56..000000000 --- a/typescript/sdk/src/gas/calculator.hardhat-test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { expect } from 'chai'; -import { BigNumber } from 'ethers'; -import { ethers } from 'hardhat'; - -import { types } from '@hyperlane-xyz/utils'; - -import { Chains } from '../consts/chains'; -import { HyperlaneCore } from '../core/HyperlaneCore'; -import { TestCoreDeployer } from '../core/TestCoreDeployer'; -import { MultiProvider } from '../providers/MultiProvider'; - -import { InterchainGasCalculator } from './calculator'; - -describe('InterchainGasCalculator', async () => { - const localChain = Chains.test1; - const remoteChain = Chains.test2; - - const testGasAmount = BigNumber.from('100000'); - - let signer: SignerWithAddress; - let multiProvider: MultiProvider; - - let calculator: InterchainGasCalculator; - let igp: types.Address; - - before(async () => { - [signer] = await ethers.getSigners(); - - multiProvider = MultiProvider.createTestMultiProvider({ signer }); - - const coreDeployer = new TestCoreDeployer(multiProvider); - const coreContractsMaps = await coreDeployer.deploy(); - const core = new HyperlaneCore(coreContractsMaps, multiProvider); - calculator = new InterchainGasCalculator(multiProvider, core); - igp = coreContractsMaps[localChain].interchainGasPaymaster.address; - }); - - describe('quoteGasPaymentForDefaultIsmIgp', () => { - it("calls the default ISM IGP's quoteGasPayment function", async () => { - const quote = await calculator.quoteGasPaymentForDefaultIsmIgp( - localChain, - remoteChain, - testGasAmount, - ); - - // (100,000 gas amount + 151,966 overhead) * 10 gas price - expect(quote).to.equal(BigNumber.from('2519660')); - }); - }); - - describe('quoteGasPayment', () => { - it("calls the IGP's quoteGasPayment function", async () => { - const quote = await calculator.quoteGasPayment( - localChain, - remoteChain, - testGasAmount, - ); - - // 100,000 gas amount * 10 gas price - expect(quote).to.equal(BigNumber.from('1000000')); - }); - }); - - describe('quoteGasPaymentForIGP', () => { - it("calls the provided IGP's quoteGasPayment", async () => { - const quote = await calculator.quoteGasPaymentForIGP( - localChain, - remoteChain, - testGasAmount, - igp, - ); - - // 100,000 gas amount * 10 gas price - expect(quote).to.equal(BigNumber.from('1000000')); - }); - }); -}); diff --git a/typescript/sdk/src/gas/calculator.test.ts b/typescript/sdk/src/gas/calculator.test.ts deleted file mode 100644 index 3e48a04d4..000000000 --- a/typescript/sdk/src/gas/calculator.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { expect } from 'chai'; -import { BigNumber } from 'ethers'; -import sinon from 'sinon'; - -import { Chains } from '../consts/chains'; -import { HyperlaneCore } from '../core/HyperlaneCore'; -import { CoreContracts } from '../core/contracts'; -import { MultiProvider } from '../providers/MultiProvider'; -import { MockProvider, MockTokenPriceGetter } from '../test/testUtils'; -import { ChainName, TestChainNames } from '../types'; - -import { InterchainGasCalculator, ParsedMessage } from './calculator'; - -const HANDLE_GAS = 100_000; -const SUGGESTED_GAS_PRICE = 10; -const INBOX_PROCESS_OVERHEAD_GAS = 100_000; - -// Exposes protected methods so they can be stubbed. -class TestInterchainGasCalculator extends InterchainGasCalculator { - estimateGasForProcess( - origin: ChainName, - destination: ChainName, - ): Promise { - return super.estimateGasForProcess(origin, destination); - } - estimateGasForHandle(message: ParsedMessage): Promise { - return super.estimateGasForHandle(message); - } - convertBetweenTokens( - fromChain: ChainName, - toChain: ChainName, - fromAmount: BigNumber, - ): Promise { - return super.convertBetweenTokens(fromChain, toChain, fromAmount); - } - tokenDecimals(chain: ChainName): number { - return super.tokenDecimals(chain); - } - getGasPrice(chain: ChainName): Promise { - return super.getGasPrice(chain); - } -} - -describe('InterchainGasCalculator', () => { - const provider = new MockProvider(); - - const multiProvider = MultiProvider.createTestMultiProvider({ provider }); - const core: HyperlaneCore = HyperlaneCore.fromEnvironment( - 'test', - multiProvider, - ); - const origin = Chains.test1; - const destination = Chains.test2; - - let tokenPriceGetter: MockTokenPriceGetter; - let calculator: TestInterchainGasCalculator; - - beforeEach(() => { - tokenPriceGetter = new MockTokenPriceGetter(); - tokenPriceGetter.setTokenPrice(origin, 9.0909); - tokenPriceGetter.setTokenPrice(destination, 5.5); - calculator = new TestInterchainGasCalculator(multiProvider, core, { - tokenPriceGetter, - // A multiplier of 1 makes testing easier to reason about - paymentEstimateMultiplier: '1', - }); - }); - - afterEach(() => { - sinon.restore(); - provider.clearMethodResolveValues(); - }); - - describe('estimatePaymentForGas', () => { - it('estimates origin token payment from a specified destination gas amount', async () => { - // Set destination gas price to 10 wei - provider.setMethodResolveValue( - 'getGasPrice', - BigNumber.from(SUGGESTED_GAS_PRICE), - ); - - const estimatedPayment = await calculator.estimatePaymentForGas( - origin, - destination, - BigNumber.from(HANDLE_GAS), - ); - - // 100k gas * 10 gas price * ($5.5 per destination token / $9.0909 per origin token) - expect(estimatedPayment.toNumber()).to.equal(605_000); - }); - }); - - describe('estimatePaymentForHandleGas', () => { - it('estimates origin token payment from a specified destination handle gas amount', async () => { - // Set destination gas price to 10 wei - provider.setMethodResolveValue( - 'getGasPrice', - BigNumber.from(SUGGESTED_GAS_PRICE), - ); - - // Stub the inbox process overhead gas - sinon - .stub(calculator, 'estimateGasForProcess') - .returns(Promise.resolve(BigNumber.from(INBOX_PROCESS_OVERHEAD_GAS))); - - const estimatedPayment = await calculator.estimatePaymentForHandleGas( - origin, - destination, - BigNumber.from(HANDLE_GAS), - ); - - // (100_000 dest handler gas + 100_000 process overhead gas) - // * 10 gas price * ($5.5 per destination token / $9.0909 per origin token) - expect(estimatedPayment.toNumber()).to.equal(1_210_000); - }); - }); - - /* - describe('estimatePaymentForMessage', () => { - it('estimates origin token payment from a specified message', async () => { - // Set destination gas price to 10 wei - provider.setMethodResolveValue( - 'getGasPrice', - BigNumber.from(SUGGESTED_GAS_PRICE), - ); - // Set the estimated handle gas - sinon - .stub(calculator, 'estimateGasForHandle') - .returns(Promise.resolve(BigNumber.from(HANDLE_GAS))); - // Stub the inbox process overhead gas - sinon - .stub(calculator, 'estimateGasForProcess') - .returns(Promise.resolve(BigNumber.from(INBOX_PROCESS_OVERHEAD_GAS))); - - const zeroAddressBytes32 = utils.addressToBytes32( - ethers.constants.AddressZero, - ); - const message: ParsedMessage = { - origin: origin, - sender: zeroAddressBytes32, - destination: destination, - recipient: zeroAddressBytes32, - body: '0x12345678', - }; - - const estimatedPayment = await calculator.estimatePaymentForMessage( - message, - ); - - // (100_000 dest handler gas + 100_000 process overhead gas) - // * 10 gas price * ($5.5 per destination token / $9.0909 per origin token) - expect(estimatedPayment.toNumber()).to.equal(1_210_000); - }); - }); - */ - - describe('convertBetweenTokens', () => { - const destinationWei = BigNumber.from('1000'); - - it('converts using the USD value of origin and destination native tokens', async () => { - const originWei = await calculator.convertBetweenTokens( - destination, - origin, - destinationWei, - ); - - // 1000 * (5.5 / 9.0909) - expect(originWei.toNumber()).to.equal(605); - }); - - it('considers when the origin token decimals > the destination token decimals', async () => { - calculator.tokenDecimals = (chain: TestChainNames) => { - if (chain === origin) { - return 20; - } - return 18; - }; - - const originWei = await calculator.convertBetweenTokens( - destination, - origin, - destinationWei, - ); - - // 1000 * (5.5 / 9.0909) * 100 - expect(originWei.toNumber()).to.equal(60500); - }); - - it('considers when the origin token decimals < the destination token decimals', async () => { - sinon.stub(calculator, 'tokenDecimals').callsFake((chain: ChainName) => { - if (chain === origin) { - return 16; - } - return 18; - }); - - const originWei = await calculator.convertBetweenTokens( - destination, - origin, - destinationWei, - ); - - // 1000 * (5.5 / 9.0909) / 100 - expect(originWei.toNumber()).to.equal(6); - }); - }); - - describe('getGasPrice', () => { - it('gets the gas price from the provider', async () => { - provider.setMethodResolveValue( - 'getGasPrice', - BigNumber.from(SUGGESTED_GAS_PRICE), - ); - - expect((await calculator.getGasPrice(destination)).toNumber()).to.equal( - SUGGESTED_GAS_PRICE, - ); - }); - }); - - describe('estimateGasForProcess', () => { - let threshold: number; - // Mock the return value of MultisigIsm.threshold - // to return `threshold`. Because the mocking involves a closure, - // changing `threshold` will change the return value of MultisigIsm.threshold. - before(() => { - const getContractsStub = sinon.stub(core, 'getContracts'); - let thresholdStub: sinon.SinonStub | undefined; - getContractsStub.callsFake((chain) => { - // Get the "real" return value of getContracts. - const contracts: CoreContracts = - getContractsStub.wrappedMethod.bind(core)(chain); - - // Ethers contracts are frozen using Object.freeze, so we make a copy - // of the object so we can stub `threshold`. - const multisigIsm = Object.assign({}, contracts.multisigIsm); - - // Because we are stubbing vaidatorManager.threshold when core.getContracts gets called, - // we must ensure we don't try to stub more than once or sinon will complain. - if (!thresholdStub) { - thresholdStub = sinon - .stub(multisigIsm, 'threshold') - .callsFake(() => Promise.resolve(threshold)); - - contracts.multisigIsm = multisigIsm; - } - return contracts; - }); - }); - - it('scales the gas cost with the quorum threshold', async () => { - threshold = 2; - const gasWithThresholdLow = await calculator.estimateGasForProcess( - origin, - destination, - ); - - threshold = 3; - const gasWithThresholdHigh = await calculator.estimateGasForProcess( - origin, - destination, - ); - - expect(gasWithThresholdHigh.gt(gasWithThresholdLow)).to.be.true; - }); - }); -}); diff --git a/typescript/sdk/src/gas/calculator.ts b/typescript/sdk/src/gas/calculator.ts deleted file mode 100644 index 2936f813f..000000000 --- a/typescript/sdk/src/gas/calculator.ts +++ /dev/null @@ -1,416 +0,0 @@ -import CoinGecko from 'coingecko-api'; -import { BigNumber, FixedNumber, ethers } from 'ethers'; - -import { InterchainGasPaymaster__factory } from '@hyperlane-xyz/core'; -import { types, utils } from '@hyperlane-xyz/utils'; - -import { CoreEnvironment, HyperlaneCore } from '../core/HyperlaneCore'; -import { MultiProvider } from '../providers/MultiProvider'; -import { ChainName } from '../types'; -import { convertDecimalValue, mulBigAndFixed } from '../utils/number'; - -import { CoinGeckoTokenPriceGetter, TokenPriceGetter } from './token-prices'; - -/** - * A note on arithmetic: - * The ethers.BigNumber implementation behaves very similar to Solidity's - * number handling by not supporting decimals. To avoid adding another big - * number implementation as a dependency, we use ethers.FixedNumber, a - * fixed point implementation intended to model how Solidity's half-supported - * fixed point numbers work, see https://docs.soliditylang.org/en/v0.8.13/types.html#fixed-point-numbers). - * - * Generally, ceiling is used rather than floor here to err on the side of over- - * estimating amounts. - */ - -// If a chain doesn't specify how many decimals their native token has, 18 is used. -const DEFAULT_TOKEN_DECIMALS = 18; - -// Intrinsic gas for a transaction. Does not consider calldata costs or differences in -// intrinsic gas for different chains. -const GAS_INTRINSIC = 21_000; - -// The gas used to process a message when the quorum size is zero. -// Includes intrinsic gas, mailbox overhead gas, all other gas that does not scale with the -// quorum size. Excludes the cost of the recipient's handle function. -const GAS_OVERHEAD_BASE = 155_000; - -// The amount of gas used for each signature when a signed checkpoint -// is submitted for verification. -// Really observed to be about 6500, but rounding up for safety. -const GAS_OVERHEAD_PER_SIGNATURE = 7_500; - -export interface InterchainGasCalculatorConfig { - /** - * A multiplier applied to the estimated origin token payment amount. - * This should be high enough to account for movements in token exchange - * rates and gas prices. - * Only used for gas payment estimates that are not quoted on-chain. - * @defaultValue 1.25 - */ - paymentEstimateMultiplier?: string; - /** - * An amount of additional gas to add to the estimated gas of processing a message. - * Only used when estimating a payment from a message. - * Only used for gas payment estimates that are not quoted on-chain. - * @defaultValue 50,000 - */ - messageGasEstimateBuffer?: string; - /** - * Used to get the native token prices of the origin and destination chains. - * Only used for gas payment estimates that are not quoted on-chain. - * @defaultValue An instance of DefaultTokenPriceGetter. - */ - tokenPriceGetter?: TokenPriceGetter; -} - -export interface ParsedMessage { - origin: ChainName; - sender: string; - destination: ChainName; - recipient: string; - body: string; -} - -/** - * Calculates interchain gas payments. - */ -export class InterchainGasCalculator { - private core: HyperlaneCore; - private multiProvider: MultiProvider; - - private tokenPriceGetter: TokenPriceGetter; - - private paymentEstimateMultiplier: ethers.FixedNumber; - private messageGasEstimateBuffer: ethers.BigNumber; - - static fromEnvironment( - env: Env, - multiProvider: MultiProvider, - config?: InterchainGasCalculatorConfig, - ): InterchainGasCalculator { - const core = HyperlaneCore.fromEnvironment(env, multiProvider); - return new InterchainGasCalculator(multiProvider, core, config); - } - - constructor( - multiProvider: MultiProvider, - core: HyperlaneCore, - config?: InterchainGasCalculatorConfig, - ) { - this.multiProvider = multiProvider; - this.core = core; - - if (config?.tokenPriceGetter) { - this.tokenPriceGetter = config.tokenPriceGetter; - } else { - const coinGecko = new CoinGecko(); - this.tokenPriceGetter = new CoinGeckoTokenPriceGetter(coinGecko); - } - - this.paymentEstimateMultiplier = FixedNumber.from( - config?.paymentEstimateMultiplier ?? '1.25', - ); - this.messageGasEstimateBuffer = BigNumber.from( - config?.messageGasEstimateBuffer ?? 50_000, - ); - } - - /** - * Only intended for IGPs that quote gas payments on-chain. - * Calls the default ISM IGP's `quoteGasPayment` function to get the amount of native tokens - * required to pay for interchain gas. - * The default ISM IGP will add any gas overhead amounts related to the Mailbox - * and default ISM on the destination to the provided gasAmount. - * @param origin The name of the origin chain. - * @param destination The name of the destination chain. - * @param gasAmount The amount of gas to use when calling `quoteGasPayment`. - * The default IGP is expected to add any gas overhead related to the Mailbox - * or ISM, so this gas amount is only required to cover the usage of the `handle` - * function. - * @returns The amount of native tokens required to pay for interchain gas. - */ - async quoteGasPaymentForDefaultIsmIgp( - origin: ChainName, - destination: ChainName, - gasAmount: BigNumber, - ): Promise { - const igpAddress = - this.core.getContracts(origin).defaultIsmInterchainGasPaymaster; - return this.quoteGasPaymentForIGP( - origin, - destination, - gasAmount, - igpAddress.address, - ); - } - - /** - * Only intended for IGPs that quote gas payments on-chain. - * Calls the "base" IGP's `quoteGasPayment` function to get the amount of native tokens - * required to pay for interchain gas. - * This IGP will not apply any overhead gas to the provided gasAmount. - * @param origin The name of the origin chain. - * @param destination The name of the destination chain. - * @param gasAmount The amount of gas to use when calling `quoteGasPayment`. - * This is expected to be the total amount of gas that a transaction would use - * on the destination chain. This should consider intrinsic transaction gas, - * Mailbox overhead gas costs, ISM gas costs, and the recipient's handle function - * gas cost. - * @returns The amount of native tokens required to pay for interchain gas. - */ - async quoteGasPayment( - origin: ChainName, - destination: ChainName, - gasAmount: BigNumber, - ): Promise { - const igpAddress = this.core.getContracts(origin).interchainGasPaymaster; - return this.quoteGasPaymentForIGP( - origin, - destination, - gasAmount, - igpAddress.address, - ); - } - - /** - * Only intended for IGPs that quote gas payments on-chain. - * Calls the origin's default IGP's `quoteGasPayment` function to get the - * amount of native tokens required to pay for interchain gas. - * The default IGP is expected to add any gas overhead related to the Mailbox - * and ISM to the provided gasAmount. - * @param origin The name of the origin chain. - * @param destination The name of the destination chain. - * @param gasAmount The amount of gas to use when calling `quoteGasPayment`. - * The default IGP is expected to add any gas overhead related to the Mailbox - * or ISM, so this gas amount is only required to cover the usage of the `handle` - * function. - * @returns The amount of native tokens required to pay for interchain gas. - */ - async quoteGasPaymentForIGP( - origin: ChainName, - destination: ChainName, - gasAmount: BigNumber, - interchainGasPaymasterAddress: types.Address, - ): Promise { - const originProvider = this.multiProvider.getProvider(origin); - const igp = InterchainGasPaymaster__factory.connect( - interchainGasPaymasterAddress, - originProvider, - ); - const domainId = this.multiProvider.getDomainId(destination); - return igp.quoteGasPayment(domainId, gasAmount); - } - - /** - * Only intended for IGPs that do *not* quote gas payments on-chain. - * Given an amount of gas to consume on the destination chain, calculates the - * estimated payment denominated in the native token of the origin chain. - * Considers the exchange rate between the native tokens of the origin and - * destination chains and the suggested gas price of the destination chain. - * @param origin The name of the origin chain. - * @param destination The name of the destination chain. - * @param gas The amount of gas to pay for on the destination chain. - * @returns An estimated amount of origin chain tokens to cover gas costs on the - * destination chain. - */ - async estimatePaymentForGas( - origin: ChainName, - destination: ChainName, - gas: BigNumber, - ): Promise { - const destinationGasPrice = await this.getGasPrice(destination); - const destinationGasCost = gas.mul(destinationGasPrice); - const originGasCost = await this.convertBetweenTokens( - destination, - origin, - destinationGasCost, - ); - // Applies a multiplier - return mulBigAndFixed( - originGasCost, - this.paymentEstimateMultiplier, - true, // ceil - ); - } - - /** - * Only intended for IGPs that do *not* quote gas payments on-chain. - * Given an amount of gas the message's recipient `handle` function is expected - * to use, calculates the estimated payment denominated in the native - * token of the origin chain. Considers the exchange rate between the native - * tokens of the origin and destination chains, the suggested gas price on - * the destination chain, gas costs incurred by a relayer when submitting a signed - * checkpoint to the destination chain, and the overhead gas cost in Inbox of processing - * a message. - * @param origin The name of the origin chain. - * @param destination The name of the destination chain. - * @param handleGas The amount of gas the recipient `handle` function - * is estimated to use. - * @returns An estimated amount of origin chain tokens to cover gas costs of the - * message on the destination chain. - */ - async estimatePaymentForHandleGas( - origin: ChainName, - destination: ChainName, - handleGas: BigNumber, - ): Promise { - const destinationGas = handleGas.add( - await this.estimateGasForProcess(origin, destination), - ); - return this.estimatePaymentForGas(origin, destination, destinationGas); - } - - /** - * Only intended for IGPs that do *not* quote gas payments on-chain. - * Calculates the estimated payment to process the message on its destination chain, - * denominated in the native token of the origin chain. The gas used by the message's - * recipient handler function is estimated in an eth_estimateGas call to the - * destination chain, and is then used to calculate the payment using - * Currently made private as it does not work properly for Arbitrum. - * {@link estimatePaymentForHandleGasAmount}. - * @param message The parsed message to estimate payment for. - * @returns An estimated amount of origin chain tokens to cover gas costs of the - * message on the destination chain. - */ - protected async estimatePaymentForMessage( - message: ParsedMessage, - ): Promise { - const handleGas = await this.estimateGasForHandle(message); - return this.estimatePaymentForHandleGas( - message.origin, - message.destination, - handleGas, - ); - } - - /** - * Using the exchange rates provided by tokenPriceGetter, returns the amount of - * `toChain` native tokens equivalent in value to the provided `fromAmount` of - * `fromChain` native tokens. Accounts for differences in the decimals of the tokens. - * @param fromChain The chain whose native token is being converted from. - * @param toChain The chain whose native token is being converted into. - * @param fromAmount The amount of `fromChain` native tokens to convert from. - * @returns The amount of `toChain` native tokens whose value is equivalent to - * `fromAmount` of `fromChain` native tokens. - */ - protected async convertBetweenTokens( - fromChain: ChainName, - toChain: ChainName, - value: BigNumber, - ): Promise { - // Does not factor in differing token decimals. - const exchangeRate = await this.tokenPriceGetter.getTokenExchangeRate( - fromChain, - toChain, - ); - - // 1/100th of a cent - const PRECISION = 1000; - - return convertDecimalValue( - value.mul(Math.round(exchangeRate * PRECISION)).div(PRECISION), - this.tokenDecimals(fromChain), - this.tokenDecimals(toChain), - ); - } - - /** - * Gets a suggested gas price for a chain. - * @param chainName The name of the chain to get the gas price for - * @returns The suggested gas price in wei on the destination chain. - */ - protected async getGasPrice(chain: ChainName): Promise { - const provider = this.multiProvider.getProvider(chain); - if (provider == undefined) { - throw new Error(`Missing provider for ${chain}`); - } - return provider.getGasPrice(); - } - - /** - * Gets the number of decimals of the provided chain's native token. - * @param chain The chain. - * @returns The number of decimals of `chain`'s native token. - */ - protected tokenDecimals(chain: ChainName): number { - return ( - this.multiProvider.tryGetChainMetadata(chain)?.nativeToken?.decimals ?? - DEFAULT_TOKEN_DECIMALS - ); - } - - /** - * Estimates the amount of gas used by message's recipient `handle` function - * on its destination chain. This does not assume the Inbox of the destination - * chain has a checkpoint that the message is included in, and does not - * consider intrinsic gas or any "overhead" gas incurred by Inbox.process. - * The estimated gas returned is the sum of: - * 1. The estimated gas consumption of a direct call to the `handle` - * function of the recipient address using the correct parameters and - * setting the `from` address of the transaction to the address of the inbox. - * 2. A buffer to account for inaccuracies in the above estimation. - * @param message The message to estimate recipient `handle` gas usage for. - * @returns The estimated gas required by the message's recipient handle function - * on the destination chain. - */ - protected async estimateGasForHandle( - message: ParsedMessage, - ): Promise { - const provider = this.multiProvider.getProvider(message.destination); - - const mailbox = this.core.getContracts(message.destination).mailbox - .contract; - - const handlerInterface = new ethers.utils.Interface([ - 'function handle(uint32,bytes32,bytes)', - ]); - // Estimates a direct call to the `handle` function of the recipient - // with the `from` address set to the inbox. - // This includes intrinsic gas. - const directHandleCallGas = await provider.estimateGas({ - to: utils.bytes32ToAddress(message.recipient), - from: mailbox.address, - data: handlerInterface.encodeFunctionData('handle', [ - this.multiProvider.getChainId(message.origin), - utils.addressToBytes32(message.sender), - message.body, - ]), - }); - - // Subtract intrinsic gas, which is included in directHandleCallGas. - // Note the "real" intrinsic gas will always be higher than this.intrinsicGas - // due to calldata costs, but this is desired because subtracting the lower bound - // this.intrinsicGas will result in a more generous final estimate. - return directHandleCallGas - .add(this.messageGasEstimateBuffer) - .sub(this.intrinsicGas()); - } - - /** - * @returns A generous estimation of the gas consumption of all process - * operations within Inbox.sol, including intrinsic gas. Does not include any gas - * consumed within a message's recipient `handle` function. - */ - protected async estimateGasForProcess( - origin: ChainName, - destination: ChainName, - ): Promise { - // TODO: Check the recipient module - const module = this.core.getContracts(destination).multisigIsm; - const threshold = await module.threshold( - this.multiProvider.getDomainId(origin), - ); - return BigNumber.from(threshold) - .mul(GAS_OVERHEAD_PER_SIGNATURE) - .add(GAS_OVERHEAD_BASE); - } - - /** - * @returns The intrinsic gas of a basic transaction. Note this does not consider calldata - * costs or potentially different intrinsic gas costs for different chains. - */ - protected intrinsicGas(): BigNumber { - return BigNumber.from(GAS_INTRINSIC); - } -} diff --git a/typescript/sdk/src/gas/contracts.ts b/typescript/sdk/src/gas/contracts.ts new file mode 100644 index 000000000..3f9d1bba8 --- /dev/null +++ b/typescript/sdk/src/gas/contracts.ts @@ -0,0 +1,37 @@ +import { + InterchainGasPaymaster, + InterchainGasPaymaster__factory, + OverheadIgp, + OverheadIgp__factory, + ProxyAdmin, + ProxyAdmin__factory, + StorageGasOracle, + StorageGasOracle__factory, +} from '@hyperlane-xyz/core'; +import { types } from '@hyperlane-xyz/utils'; + +import { ProxiedContract, TransparentProxyAddresses } from '../proxy'; + +export type IgpAddresses = { + proxyAdmin: types.Address; + interchainGasPaymaster: types.Address | TransparentProxyAddresses; + defaultIsmInterchainGasPaymaster: types.Address; + storageGasOracle: types.Address; +}; + +export type IgpContracts = { + proxyAdmin: ProxyAdmin; + interchainGasPaymaster: ProxiedContract< + InterchainGasPaymaster, + TransparentProxyAddresses + >; + defaultIsmInterchainGasPaymaster: OverheadIgp; + storageGasOracle: StorageGasOracle; +}; + +export const igpFactories = { + proxyAdmin: new ProxyAdmin__factory(), + interchainGasPaymaster: new InterchainGasPaymaster__factory(), + defaultIsmInterchainGasPaymaster: new OverheadIgp__factory(), + storageGasOracle: new StorageGasOracle__factory(), +}; diff --git a/typescript/sdk/src/gas/types.ts b/typescript/sdk/src/gas/types.ts index 8bc733ea2..de51e6c6a 100644 --- a/typescript/sdk/src/gas/types.ts +++ b/typescript/sdk/src/gas/types.ts @@ -1,70 +1,53 @@ -import { LegacyMultisigIsm, Mailbox } from '@hyperlane-xyz/core'; +import { BigNumber } from 'ethers'; + +import { InterchainGasPaymaster, OverheadIgp } from '@hyperlane-xyz/core'; import type { types } from '@hyperlane-xyz/utils'; import type { CheckerViolation } from '../deploy/types'; -import { ChainName } from '../types'; +import { ChainMap } from '../types'; -export type MultisigIsmConfig = { - validators: Array; - threshold: number; -}; +export enum GasOracleContractType { + StorageGasOracle = 'StorageGasOracle', +} -export type CoreConfig = { - multisigIsm: MultisigIsmConfig; +export type IgpConfig = { owner: types.Address; - remove?: boolean; + beneficiary: types.Address; + gasOracleType: ChainMap; }; -export enum CoreViolationType { - MultisigIsm = 'MultisigIsm', - Mailbox = 'Mailbox', - ConnectionManager = 'ConnectionManager', - ValidatorAnnounce = 'ValidatorAnnounce', -} - -export enum MultisigIsmViolationType { - EnrolledValidators = 'EnrolledValidators', - Threshold = 'Threshold', -} +export type OverheadIgpConfig = IgpConfig & { + overhead: ChainMap; +}; -export enum MailboxViolationType { - DefaultIsm = 'DefaultIsm', +export enum IgpViolationType { + Beneficiary = 'Beneficiary', + GasOracles = 'GasOracles', + Overhead = 'Overhead', } -export interface MailboxViolation extends CheckerViolation { - type: CoreViolationType.Mailbox; - contract: Mailbox; - mailboxType: MailboxViolationType; +export interface IgpViolation extends CheckerViolation { + type: 'InterchainGasPaymaster'; + subType: IgpViolationType; } -export interface MailboxMultisigIsmViolation extends MailboxViolation { +export interface IgpBeneficiaryViolation extends IgpViolation { + subType: IgpViolationType.Beneficiary; + contract: InterchainGasPaymaster; actual: types.Address; expected: types.Address; } -export interface MultisigIsmViolation extends CheckerViolation { - type: CoreViolationType.MultisigIsm; - contract: LegacyMultisigIsm; - subType: MultisigIsmViolationType; - remote: ChainName; -} - -export interface EnrolledValidatorsViolation extends MultisigIsmViolation { - subType: MultisigIsmViolationType.EnrolledValidators; - actual: Set; - expected: Set; -} - -export interface ThresholdViolation extends MultisigIsmViolation { - subType: MultisigIsmViolationType.Threshold; - actual: number; - expected: number; +export interface IgpGasOraclesViolation extends IgpViolation { + subType: IgpViolationType.GasOracles; + contract: InterchainGasPaymaster; + actual: ChainMap; + expected: ChainMap; } -export interface ValidatorAnnounceViolation extends CheckerViolation { - type: CoreViolationType.ValidatorAnnounce; - chain: ChainName; - validator: types.Address; - actual: boolean; - expected: boolean; +export interface IgpOverheadViolation extends IgpViolation { + subType: IgpViolationType.Overhead; + contract: OverheadIgp; + actual: ChainMap; + expected: ChainMap; } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index ced9899a7..de450bdf3 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -1,3 +1,12 @@ +export { + AgentChainSetup, + AgentConfig, + AgentConnection, + AgentConnectionType, + AgentSigner, + buildAgentConfig, + HyperlaneAgentAddresses, +} from './agents/types'; export { chainIdToMetadata, ChainMetadata, @@ -21,24 +30,24 @@ export { Testnets, } from './consts/chains'; export { - environments as coreEnvironments, + hyperlaneAgentAddresses, + HyperlaneContractAddresses, + hyperlaneContractAddresses, hyperlaneCoreAddresses, + hyperlaneEnvironments, } from './consts/environments'; +export { defaultMultisigIsmConfigs } from './consts/multisigIsm'; export { buildContracts, connectContracts, connectContractsMap, + filterAddresses, HyperlaneAddresses, HyperlaneContracts, HyperlaneFactories, serializeContracts, } from './contracts'; -export { - ConnectionClientContracts, - CoreContracts, - coreFactories, - GasOracleContracts, -} from './core/contracts'; +export { CoreContracts, coreFactories } from './core/contracts'; export { AnnotatedDispatch, AnnotatedLifecycleEvent, @@ -49,49 +58,26 @@ export { DispatchedMessage, HyperlaneCore, } from './core/HyperlaneCore'; -export { TestCoreApp, TestCoreContracts } from './core/TestCoreApp'; -export { TestCoreDeployer } from './core/TestCoreDeployer'; export { HyperlaneCoreChecker } from './core/HyperlaneCoreChecker'; export { HyperlaneCoreDeployer } from './core/HyperlaneCoreDeployer'; +export { TestCoreApp, TestCoreContracts } from './core/TestCoreApp'; +export { TestCoreDeployer } from './core/TestCoreDeployer'; export { CoreConfig, CoreViolationType, - DefaultIsmIgpViolation, - DefaultIsmIgpViolationType, EnrolledValidatorsViolation, - GasOracleContractType, - IgpBeneficiaryViolation, - IgpGasOraclesViolation, - IgpViolation, - IgpViolationType, + MultisigIsmConfig, MultisigIsmViolation, MultisigIsmViolationType, } from './core/types'; export { HyperlaneAppChecker } from './deploy/HyperlaneAppChecker'; export { HyperlaneDeployer } from './deploy/HyperlaneDeployer'; -export { - InterchainAccountDeployer, - InterchainQueryDeployer, -} from './middleware/deploy'; -export { LiquidityLayerApp } from './middleware/liquidity-layer/LiquidityLayerApp'; -export { - BridgeAdapterConfig, - BridgeAdapterType, - CircleBridgeAdapterConfig, - LiquidityLayerDeployer, - PortalAdapterConfig, -} from './middleware/liquidity-layer/LiquidityLayerRouterDeployer'; export { ProxyViolation } from './deploy/proxy'; -export { GasRouterDeployer } from './router/GasRouterDeployer'; -export { HyperlaneRouterChecker } from './router/HyperlaneRouterChecker'; -export { HyperlaneRouterDeployer } from './router/HyperlaneRouterDeployer'; -export { GasRouterConfig, RouterConfig } from './router/types'; export { CheckerViolation, OwnerViolation, ViolationType, } from './deploy/types'; -export { getChainToOwnerMap } from './deploy/utils'; export { ContractVerifier } from './deploy/verify/ContractVerifier'; export { CompilerOptions, @@ -105,30 +91,61 @@ export { queryAnnotatedEvents, TSContract, } from './events'; -export { InterchainGasCalculator, ParsedMessage } from './gas/calculator'; +export { HyperlaneIgp } from './gas/HyperlaneIgp'; +export { HyperlaneIgpChecker } from './gas/HyperlaneIgpChecker'; +export { HyperlaneIgpDeployer } from './gas/HyperlaneIgpDeployer'; +export { CoinGeckoTokenPriceGetter } from './gas/token-prices'; export { - CoinGeckoTokenPriceGetter, - TokenPriceGetter, -} from './gas/token-prices'; + GasOracleContractType, + IgpBeneficiaryViolation, + IgpConfig, + IgpGasOraclesViolation, + IgpOverheadViolation, + IgpViolation, + IgpViolationType, + OverheadIgpConfig, +} from './gas/types'; export { HyperlaneApp } from './HyperlaneApp'; export { + InterchainAccountDeployer, interchainAccountFactories, + InterchainQueryDeployer, interchainQueryFactories, } from './middleware/deploy'; export { LiquidityLayerContracts, liquidityLayerFactories, } from './middleware/liquidity-layer/contracts'; +export { LiquidityLayerApp } from './middleware/liquidity-layer/LiquidityLayerApp'; +export { + BridgeAdapterConfig, + BridgeAdapterType, + CircleBridgeAdapterConfig, + LiquidityLayerDeployer, + PortalAdapterConfig, +} from './middleware/liquidity-layer/LiquidityLayerRouterDeployer'; export { MultiProvider, providerBuilder } from './providers/MultiProvider'; export { RetryJsonRpcProvider, RetryProvider } from './providers/RetryProvider'; export { ProxiedContract, ProxyAddresses, + ProxyKind, TransparentProxyAddresses, } from './proxy'; +export { GasRouterDeployer } from './router/GasRouterDeployer'; +export { HyperlaneRouterChecker } from './router/HyperlaneRouterChecker'; +export { HyperlaneRouterDeployer } from './router/HyperlaneRouterDeployer'; export { GasRouterApp, Router, RouterApp } from './router/RouterApps'; -export { RouterContracts, RouterFactories } from './router/types'; -export { getTestOwnerConfig } from './test/testUtils'; +export { + GasRouterConfig, + RouterConfig, + RouterContracts, + RouterFactories, +} from './router/types'; +export { + createRouterConfigMap, + deployTestIgpsAndGetRouterConfig, +} from './test/testUtils'; export { ChainMap, ChainName, @@ -137,6 +154,7 @@ export { TestChainNames, } from './types'; export { canonizeId, evmId } from './utils/ids'; +export { multisigIsmVerificationCost } from './utils/ism'; export { MultiGeneric } from './utils/MultiGeneric'; export { bigToFixed, @@ -144,6 +162,13 @@ export { fixedToBig, mulBigAndFixed, } from './utils/number'; -export { objMap, objMapEntries, pick, promiseObjAll } from './utils/objects'; +export { + objFilter, + objMap, + objMapEntries, + objMerge, + pick, + promiseObjAll, +} from './utils/objects'; export { delay } from './utils/time'; export { chainMetadataToWagmiChain } from './utils/wagmi'; diff --git a/typescript/sdk/src/middleware/accounts.hardhat-test.ts b/typescript/sdk/src/middleware/accounts.hardhat-test.ts index 2ce946671..0d43caa0d 100644 --- a/typescript/sdk/src/middleware/accounts.hardhat-test.ts +++ b/typescript/sdk/src/middleware/accounts.hardhat-test.ts @@ -12,7 +12,7 @@ import { TestCoreApp } from '../core/TestCoreApp'; import { TestCoreDeployer } from '../core/TestCoreDeployer'; import { MultiProvider } from '../providers/MultiProvider'; import { RouterConfig } from '../router/types'; -import { getTestOwnerConfig } from '../test/testUtils'; +import { deployTestIgpsAndGetRouterConfig } from '../test/testUtils'; import { ChainMap } from '../types'; import { objMap, promiseObjAll } from '../utils/objects'; @@ -41,8 +41,10 @@ describe('InterchainAccounts', async () => { const coreDeployer = new TestCoreDeployer(multiProvider); const coreContractsMaps = await coreDeployer.deploy(); coreApp = new TestCoreApp(coreContractsMaps, multiProvider); - config = coreApp.extendWithConnectionClientConfig( - getTestOwnerConfig(signer.address), + config = await deployTestIgpsAndGetRouterConfig( + multiProvider, + signer.address, + coreContractsMaps, ); config.test1.interchainSecurityModule = diff --git a/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts b/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts index 60b4452b8..67fe0c5c8 100644 --- a/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts +++ b/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts @@ -76,7 +76,7 @@ export class LiquidityLayerDeployer extends MiddlewareRouterDeployer< objMap(contractsMap, (_chain, contracts) => ({ router: contracts.circleBridgeAdapter, })), - (_): _ is { router: CircleBridgeAdapter } => !!_.router, + (chain, _): _ is { router: CircleBridgeAdapter } => !!_.router, ), ); @@ -86,7 +86,7 @@ export class LiquidityLayerDeployer extends MiddlewareRouterDeployer< objMap(contractsMap, (_chain, contracts) => ({ router: contracts.portalAdapter, })), - (_): _ is { router: PortalAdapter } => !!_.router, + (chain, _): _ is { router: PortalAdapter } => !!_.router, ), ); } diff --git a/typescript/sdk/src/middleware/liquidity-layer/liquidity-layer.hardhat-test.ts b/typescript/sdk/src/middleware/liquidity-layer/liquidity-layer.hardhat-test.ts index 76bdebc3f..41112683d 100644 --- a/typescript/sdk/src/middleware/liquidity-layer/liquidity-layer.hardhat-test.ts +++ b/typescript/sdk/src/middleware/liquidity-layer/liquidity-layer.hardhat-test.ts @@ -21,7 +21,7 @@ import { Chains } from '../../consts/chains'; import { TestCoreApp } from '../../core/TestCoreApp'; import { TestCoreDeployer } from '../../core/TestCoreDeployer'; import { MultiProvider } from '../../providers/MultiProvider'; -import { getTestOwnerConfig } from '../../test/testUtils'; +import { deployTestIgpsAndGetRouterConfig } from '../../test/testUtils'; import { ChainMap } from '../../types'; import { objMap } from '../../utils/objects'; @@ -73,10 +73,14 @@ describe('LiquidityLayerRouter', async () => { signer, ); messageTransmitter = await messageTransmitterF.deploy(mockToken.address); - - config = coreApp.extendWithConnectionClientConfig( - objMap(getTestOwnerConfig(signer.address), (_chain, conf) => ({ - ...conf, + const routerConfig = await deployTestIgpsAndGetRouterConfig( + multiProvider, + signer.address, + coreContractsMaps, + ); + config = objMap(routerConfig, (chain, config) => { + return { + ...config, circle: { type: BridgeAdapterType.Circle, tokenMessengerAddress: circleTokenMessenger.address, @@ -107,8 +111,8 @@ describe('LiquidityLayerRouter', async () => { }, ], } as PortalAdapterConfig, - })), - ); + }; + }); }); beforeEach(async () => { diff --git a/typescript/sdk/src/middleware/queries.hardhat-test.ts b/typescript/sdk/src/middleware/queries.hardhat-test.ts index 367eb67b7..9daf50ccb 100644 --- a/typescript/sdk/src/middleware/queries.hardhat-test.ts +++ b/typescript/sdk/src/middleware/queries.hardhat-test.ts @@ -15,7 +15,7 @@ import { TestCoreApp } from '../core/TestCoreApp'; import { TestCoreDeployer } from '../core/TestCoreDeployer'; import { MultiProvider } from '../providers/MultiProvider'; import { RouterConfig } from '../router/types'; -import { getTestOwnerConfig } from '../test/testUtils'; +import { deployTestIgpsAndGetRouterConfig } from '../test/testUtils'; import { ChainMap } from '../types'; import { InterchainQueryDeployer } from './deploy'; @@ -42,8 +42,10 @@ describe('InterchainQueryRouter', async () => { const coreDeployer = new TestCoreDeployer(multiProvider); const coreContractsMaps = await coreDeployer.deploy(); coreApp = new TestCoreApp(coreContractsMaps, multiProvider); - config = coreApp.extendWithConnectionClientConfig( - getTestOwnerConfig(signer.address), + config = await deployTestIgpsAndGetRouterConfig( + multiProvider, + signer.address, + coreContractsMaps, ); }); diff --git a/typescript/sdk/src/providers/MultiProvider.ts b/typescript/sdk/src/providers/MultiProvider.ts index 115f5bf9a..bd0189953 100644 --- a/typescript/sdk/src/providers/MultiProvider.ts +++ b/typescript/sdk/src/providers/MultiProvider.ts @@ -8,7 +8,7 @@ import { providers, } from 'ethers'; -import { types } from '@hyperlane-xyz/utils'; +import { types, utils } from '@hyperlane-xyz/utils'; import { ChainMetadata, @@ -57,37 +57,45 @@ export class MultiProvider { options: MultiProviderOptions = {}, ) { Object.entries(chainMetadata).forEach(([key, cm]) => { - if (!isValidChainMetadata(cm)) - throw new Error(`Invalid chain metadata for ${cm.chainId}`); if (key !== cm.name) throw new Error( `Chain name mismatch: Key was ${key}, but name is ${cm.name}`, ); + this.addChain(cm); }); + this.logger = debug(options?.loggerName || 'hyperlane:MultiProvider'); + } - this.metadata = chainMetadata; + /** + * Add a chain to the multiprovider + * @throws if chain's name or domain/chain ID collide + */ + addChain(metadata: ChainMetadata): void { + if (!isValidChainMetadata(metadata)) + throw new Error(`Invalid chain metadata for ${metadata.name}`); // Ensure no two chains have overlapping names/domainIds/chainIds - const chainNames = new Set(); - const chainIds = new Set(); - const domainIds = new Set(); - for (const chain of Object.values(chainMetadata)) { - const { name, chainId, domainId } = chain; - if (chainNames.has(name)) + for (const chainMetadata of Object.values(this.metadata)) { + const { name, chainId, domainId } = chainMetadata; + if (name == metadata.name) throw new Error(`Duplicate chain name: ${name}`); - if (chainIds.has(chainId)) - throw new Error(`Duplicate chain id: ${chainId}`); - if (domainIds.has(chainId)) - throw new Error(`Overlapping chain/domain id: ${chainId}`); - if (domainId && domainIds.has(domainId)) - throw new Error(`Duplicate domain id: ${domainId}`); - if (domainId && chainIds.has(domainId)) - throw new Error(`Overlapping chain/domain id: ${domainId}`); - chainNames.add(name); - chainIds.add(chainId); - if (domainId) domainIds.add(domainId); + // Chain and Domain Ids should be globally unique + const idCollision = + chainId == metadata.chainId || + domainId == metadata.chainId || + (metadata.domainId && + (chainId == metadata.domainId || domainId === metadata.domainId)); + if (idCollision) + throw new Error( + `Chain/Domain id collision: ${name} and ${metadata.name}`, + ); + } + this.metadata[metadata.name] = metadata; + if (this.useSharedSigner) { + const signers = Object.values(this.signers); + if (signers.length > 0) { + this.setSharedSigner(signers[0]); + } } - - this.logger = debug(options?.loggerName || 'hyperlane:MultiProvider'); } /** @@ -414,7 +422,7 @@ export class MultiProvider { * Get chain names excluding given chain name */ getRemoteChains(name: ChainName): ChainName[] { - return this.getKnownChainNames().filter((n) => n !== name); + return utils.exclude(name, this.getKnownChainNames()); } /** @@ -624,9 +632,10 @@ export class MultiProvider { */ static createTestMultiProvider( params: { signer?: Signer; provider?: Provider } = {}, + chains: ChainName[] = TestChains, ): MultiProvider { const { signer, provider } = params; - const chainMetadata = pick(defaultChainMetadata, TestChains); + const chainMetadata = pick(defaultChainMetadata, chains); const mp = new MultiProvider(chainMetadata); if (signer) { mp.setSharedSigner(signer); @@ -634,7 +643,7 @@ export class MultiProvider { const _provider = provider || signer?.provider; if (_provider) { const providerMap: ChainMap = {}; - TestChains.forEach((t) => (providerMap[t] = _provider)); + chains.forEach((t) => (providerMap[t] = _provider)); mp.setProviders(providerMap); } return mp; diff --git a/typescript/sdk/src/proxy.ts b/typescript/sdk/src/proxy.ts index 568c61f9e..9d72b42b8 100644 --- a/typescript/sdk/src/proxy.ts +++ b/typescript/sdk/src/proxy.ts @@ -29,6 +29,12 @@ export function isProxyAddresses( ); } +export function getProxyAddress( + address: types.Address | ProxyAddresses, +): string { + return isProxyAddresses(address) ? address.proxy : address; +} + export type TransparentProxyAddresses = ProxyAddresses; export class ProxiedContract< diff --git a/typescript/sdk/src/router/app.ts b/typescript/sdk/src/router/app.ts new file mode 100644 index 000000000..450ecdd46 --- /dev/null +++ b/typescript/sdk/src/router/app.ts @@ -0,0 +1,39 @@ +import { BigNumber } from 'ethers'; + +import { GasRouter } from '@hyperlane-xyz/core'; +import { types } from '@hyperlane-xyz/utils'; + +import { HyperlaneApp } from '../HyperlaneApp'; +import { ChainMap, ChainName } from '../types'; +import { objMap, promiseObjAll } from '../utils/objects'; + +import { RouterContracts } from './types'; + +export class RouterApp< + Contracts extends RouterContracts, +> extends HyperlaneApp { + getSecurityModules = (): Promise> => + promiseObjAll( + objMap(this.contractsMap, (_, contracts) => + contracts.router.interchainSecurityModule(), + ), + ); + + getOwners = (): Promise> => + promiseObjAll( + objMap(this.contractsMap, (_, contracts) => contracts.router.owner()), + ); +} + +export class GasRouterApp< + Contracts extends RouterContracts, +> extends RouterApp { + async quoteGasPayment( + origin: ChainName, + destination: ChainName, + ): Promise { + return this.getContracts(origin).router.quoteGasPayment( + this.multiProvider.getDomainId(destination), + ); + } +} diff --git a/typescript/sdk/src/test/envSubsetDeployer/app.ts b/typescript/sdk/src/test/envSubsetDeployer/app.ts index 036cf8b68..9d8f07d26 100644 --- a/typescript/sdk/src/test/envSubsetDeployer/app.ts +++ b/typescript/sdk/src/test/envSubsetDeployer/app.ts @@ -2,7 +2,7 @@ import { TestRouter__factory } from '@hyperlane-xyz/core'; import { HyperlaneApp } from '../../HyperlaneApp'; import { chainMetadata } from '../../consts/chainMetadata'; -import { Chains, TestChains } from '../../consts/chains'; +import { Chains } from '../../consts/chains'; import { HyperlaneCore } from '../../core/HyperlaneCore'; import { HyperlaneDeployer } from '../../deploy/HyperlaneDeployer'; import { MultiProvider } from '../../providers/MultiProvider'; @@ -16,13 +16,6 @@ import { import { ChainMap, ChainName } from '../../types'; import { objMap, pick, promiseObjAll } from '../../utils/objects'; -export const fullTestEnvConfigs = pick(chainMetadata, TestChains); - -export const subsetTestConfigs = pick(chainMetadata, [ - Chains.test1, - Chains.test2, -]); - export const alfajoresChainConfig = pick(chainMetadata, [Chains.alfajores]); export class EnvSubsetApp extends HyperlaneApp {} diff --git a/typescript/sdk/src/test/envSubsetDeployer/check-single-chain.ts b/typescript/sdk/src/test/envSubsetDeployer/check-single-chain.ts index 8632ee869..6d18374ed 100644 --- a/typescript/sdk/src/test/envSubsetDeployer/check-single-chain.ts +++ b/typescript/sdk/src/test/envSubsetDeployer/check-single-chain.ts @@ -1,16 +1,12 @@ import { buildContracts } from '../../contracts'; import { HyperlaneCore } from '../../core/HyperlaneCore'; -import { getChainToOwnerMap } from '../../deploy/utils'; +import { HyperlaneIgp } from '../../gas/HyperlaneIgp'; import { MultiProvider } from '../../providers/MultiProvider'; import { RouterContracts } from '../../router/types'; import { ChainMap } from '../../types'; +import { createRouterConfigMap } from '../testUtils'; -import { - EnvSubsetApp, - EnvSubsetChecker, - alfajoresChainConfig, - envSubsetFactories, -} from './app'; +import { EnvSubsetApp, EnvSubsetChecker, envSubsetFactories } from './app'; // Copied from output of deploy-single-chain.ts script const deploymentAddresses = { @@ -31,9 +27,13 @@ async function check() { ) as ChainMap; const app = new EnvSubsetApp(contractsMap, multiProvider); const core = HyperlaneCore.fromEnvironment('testnet', multiProvider); - const config = core.extendWithConnectionClientConfig( - getChainToOwnerMap(alfajoresChainConfig, ownerAddress), + const igp = HyperlaneIgp.fromEnvironment('testnet', multiProvider); + const config = createRouterConfigMap( + ownerAddress, + core.contractsMap, + igp.contractsMap, ); + const envSubsetChecker = new EnvSubsetChecker(multiProvider, app, config); console.info('Starting check'); diff --git a/typescript/sdk/src/test/envSubsetDeployer/deploy-single-chain.ts b/typescript/sdk/src/test/envSubsetDeployer/deploy-single-chain.ts index ba0fa2bc4..375bb79d0 100644 --- a/typescript/sdk/src/test/envSubsetDeployer/deploy-single-chain.ts +++ b/typescript/sdk/src/test/envSubsetDeployer/deploy-single-chain.ts @@ -1,10 +1,11 @@ import { Chains } from '../../consts/chains'; import { serializeContracts } from '../../contracts'; import { HyperlaneCore } from '../../core/HyperlaneCore'; -import { getChainToOwnerMap } from '../../deploy/utils'; +import { HyperlaneIgp } from '../../gas/HyperlaneIgp'; import { MultiProvider } from '../../providers/MultiProvider'; +import { createRouterConfigMap } from '../testUtils'; -import { EnvSubsetDeployer, alfajoresChainConfig } from './app'; +import { EnvSubsetDeployer } from './app'; import { getAlfajoresSigner } from './utils'; async function main() { @@ -15,8 +16,11 @@ async function main() { multiProvider.setSigner(Chains.alfajores, signer); const core = HyperlaneCore.fromEnvironment('testnet', multiProvider); - const config = core.extendWithConnectionClientConfig( - getChainToOwnerMap(alfajoresChainConfig, signer.address), + const igp = HyperlaneIgp.fromEnvironment('testnet', multiProvider); + const config = createRouterConfigMap( + signer.address, + core.contractsMap, + igp.contractsMap, ); console.info('Starting deployment'); diff --git a/typescript/sdk/src/test/envSubsetDeployer/deploy.hardhat-test.ts b/typescript/sdk/src/test/envSubsetDeployer/deploy.hardhat-test.ts index 0c9c48ca9..ac6ad94b2 100644 --- a/typescript/sdk/src/test/envSubsetDeployer/deploy.hardhat-test.ts +++ b/typescript/sdk/src/test/envSubsetDeployer/deploy.hardhat-test.ts @@ -1,21 +1,15 @@ import '@nomiclabs/hardhat-waffle'; import { ethers } from 'hardhat'; -import { ChainMetadata } from '../../consts/chainMetadata'; +import { TestChains } from '../../consts/chains'; import { TestCoreApp } from '../../core/TestCoreApp'; import { TestCoreDeployer } from '../../core/TestCoreDeployer'; -import { getChainToOwnerMap } from '../../deploy/utils'; import { MultiProvider } from '../../providers/MultiProvider'; import { RouterConfig, RouterContracts } from '../../router/types'; -import { ChainMap } from '../../types'; +import { ChainMap, ChainName } from '../../types'; +import { deployTestIgpsAndGetRouterConfig, testCoreConfig } from '../testUtils'; -import { - EnvSubsetApp, - EnvSubsetChecker, - EnvSubsetDeployer, - fullTestEnvConfigs, - subsetTestConfigs, -} from './app'; +import { EnvSubsetApp, EnvSubsetChecker, EnvSubsetDeployer } from './app'; // Tests deploying the basic EnvSubsetApp to a local hardhat-based test env describe('deploy app for full test env', async () => { @@ -26,7 +20,7 @@ describe('deploy app for full test env', async () => { let app: EnvSubsetApp; before(async () => { - const testEnv = await initTestEnv(fullTestEnvConfigs); + const testEnv = await initTestEnv(TestChains); multiProvider = testEnv.multiProvider; config = testEnv.config; deployer = testEnv.deployer; @@ -56,13 +50,10 @@ describe('deploy app to test env subset', async () => { let app: EnvSubsetApp; before(async () => { - const testEnv = await initTestEnv(subsetTestConfigs); - multiProvider = testEnv.multiProvider; - config = { - test1: testEnv.config.test1, - test2: testEnv.config.test2, - }; - deployer = testEnv.deployer; + ({ multiProvider, config, deployer } = await initTestEnv([ + 'test1', + 'test2', + ])); }); it('deploys', async () => { @@ -80,15 +71,20 @@ describe('deploy app to test env subset', async () => { }); }); -async function initTestEnv(environmentConfig: ChainMap) { +async function initTestEnv(chains: ChainName[]) { const [signer] = await ethers.getSigners(); - const multiProvider = MultiProvider.createTestMultiProvider({ signer }); - - const coreDeployer = new TestCoreDeployer(multiProvider); + const multiProvider = MultiProvider.createTestMultiProvider( + { signer }, + chains, + ); + const coreConfig = testCoreConfig(chains); + const coreDeployer = new TestCoreDeployer(multiProvider, coreConfig); const coreContractsMaps = await coreDeployer.deploy(); const core = new TestCoreApp(coreContractsMaps, multiProvider); - const config = core.extendWithConnectionClientConfig( - getChainToOwnerMap(environmentConfig, signer.address), + const config = await deployTestIgpsAndGetRouterConfig( + multiProvider, + signer.address, + coreContractsMaps, ); const deployer = new EnvSubsetDeployer(multiProvider, config, core); return { multiProvider, config, deployer }; diff --git a/typescript/sdk/src/test/testUtils.ts b/typescript/sdk/src/test/testUtils.ts index 7afbb0466..db56b5172 100644 --- a/typescript/sdk/src/test/testUtils.ts +++ b/typescript/sdk/src/test/testUtils.ts @@ -1,60 +1,83 @@ import { ethers } from 'ethers'; +import { + TestInterchainGasPaymaster, + TestInterchainGasPaymaster__factory, +} from '@hyperlane-xyz/core'; import { types } from '@hyperlane-xyz/utils'; import { chainMetadata } from '../consts/chainMetadata'; -import { TestChains } from '../consts/chains'; +import { CoreContracts } from '../core/contracts'; +import { CoreConfig } from '../core/types'; +import { IgpContracts } from '../gas/contracts'; import { CoinGeckoInterface, CoinGeckoResponse, CoinGeckoSimpleInterface, CoinGeckoSimplePriceParams, - TokenPriceGetter, } from '../gas/token-prices'; +import { MultiProvider } from '../providers/MultiProvider'; +import { RouterConfig } from '../router/types'; import { ChainMap, ChainName } from '../types'; - -export function getTestOwnerConfig(owner: types.Address) { - const config: ChainMap<{ owner: types.Address }> = {}; - TestChains.forEach((t) => (config[t] = { owner })); - return config; +import { objMap } from '../utils/objects'; + +export function createRouterConfigMap( + owner: types.Address, + coreContracts: ChainMap, + igpContracts: ChainMap, +): ChainMap { + return objMap(coreContracts, (chain, contracts) => { + return { + owner, + mailbox: contracts.mailbox.address, + interchainGasPaymaster: + igpContracts[chain].interchainGasPaymaster.address, + }; + }); } -const MOCK_NETWORK = { - name: 'MockNetwork', - chainId: 1337, -}; - -// A mock ethers Provider used for testing with mocked provider functionality -export class MockProvider extends ethers.providers.BaseProvider { - private methodResolveValues: { [key: string]: any }; - - constructor() { - super(MOCK_NETWORK); - - this.methodResolveValues = {}; - } - - // Required to be implemented or the BaseProvider throws - async detectNetwork() { - return Promise.resolve(MOCK_NETWORK); - } - - perform(method: string, params: any): Promise { - const value = this.methodResolveValues[method]; - if (value) { - return Promise.resolve(value); - } - - return super.perform(method, params); - } - - setMethodResolveValue(method: string, value: any) { - this.methodResolveValues[method] = value; +export async function deployTestIgpsAndGetRouterConfig( + multiProvider: MultiProvider, + owner: types.Address, + coreContracts: ChainMap, +): Promise> { + const igps: ChainMap = {}; + for (const chain of multiProvider.getKnownChainNames()) { + const factory = new TestInterchainGasPaymaster__factory( + multiProvider.getSigner(chain), + ); + igps[chain] = await factory.deploy(owner); } + return objMap(coreContracts, (chain, contracts) => { + return { + owner, + mailbox: contracts.mailbox.address, + interchainGasPaymaster: igps[chain].address, + }; + }); +} - clearMethodResolveValues() { - this.methodResolveValues = {}; - } +const nonZeroAddress = ethers.constants.AddressZero.replace('00', '01'); + +// dummy config as TestInbox and TestOutbox do not use deployed ISM +export function testCoreConfig(chains: ChainName[]): ChainMap { + const multisigIsm = { + validators: [nonZeroAddress], + threshold: 1, + }; + return Object.fromEntries( + chains.map((local) => [ + local, + { + owner: nonZeroAddress, + multisigIsm: Object.fromEntries( + chains + .filter((c) => c !== local) + .map((remote) => [remote, multisigIsm]), + ), + }, + ]), + ); } // A mock CoinGecko intended to be used by tests @@ -101,33 +124,3 @@ export class MockCoinGecko implements CoinGeckoInterface { this.fail[id] = fail; } } - -// A mock TokenPriceGetter intended to be used by tests when mocking token prices -export class MockTokenPriceGetter implements TokenPriceGetter { - private tokenPrices: Partial>; - - constructor() { - this.tokenPrices = {}; - } - - async getTokenExchangeRate( - base: ChainName, - quote: ChainName, - ): Promise { - const basePrice = await this.getTokenPrice(base); - const quotePrice = await this.getTokenPrice(quote); - return basePrice / quotePrice; - } - - getTokenPrice(chain: ChainName): Promise { - const price = this.tokenPrices[chain]; - if (price) { - return Promise.resolve(price); - } - throw Error(`No price for chain ${chain}`); - } - - setTokenPrice(chain: ChainName, price: number) { - this.tokenPrices[chain] = price; - } -} diff --git a/typescript/sdk/src/utils/ism.ts b/typescript/sdk/src/utils/ism.ts new file mode 100644 index 000000000..59a4b2ed3 --- /dev/null +++ b/typescript/sdk/src/utils/ism.ts @@ -0,0 +1,13 @@ +import multisigIsmVerifyCosts from '../consts/multisigIsmVerifyCosts.json'; + +export function multisigIsmVerificationCost(m: number, n: number): number { + if ( + !(`${n}` in multisigIsmVerifyCosts) || + // @ts-ignore + !(`${m}` in multisigIsmVerifyCosts[`${n}`]) + ) { + throw new Error(`No multisigIsmVerificationCost found for ${m}-of-${n}`); + } + // @ts-ignore + return multisigIsmVerifyCosts[`${n}`][`${m}`]; +} diff --git a/typescript/sdk/src/utils/objects.ts b/typescript/sdk/src/utils/objects.ts index cea10df1b..d8bacd920 100644 --- a/typescript/sdk/src/utils/objects.ts +++ b/typescript/sdk/src/utils/objects.ts @@ -20,10 +20,10 @@ export function objMap( export function objFilter( obj: Record, - func: (v: I) => v is O, + func: (k: K, v: I) => v is O, ): Record { return Object.fromEntries( - Object.entries(obj).filter(([, v]) => func(v)), + Object.entries(obj).filter(([k, v]) => func(k as K, v)), ) as Record; } @@ -45,3 +45,37 @@ export function pick(obj: Record, keys: K[]) { } return ret as Record; } + +export function isObject(item: any) { + return item && typeof item === 'object' && !Array.isArray(item); +} + +// Recursively merges b into a +// Where there are conflicts, b takes priority over a +export function objMerge( + a: Record, + b: Record, + max_depth = 10, +): any { + if (max_depth === 0) { + throw new Error('objMerge tried to go too deep'); + } + if (isObject(a) && isObject(b)) { + const ret: Record = {}; + const aKeys = new Set(Object.keys(a)); + const bKeys = new Set(Object.keys(b)); + const allKeys = new Set([...aKeys, ...bKeys]); + for (const key of allKeys.values()) { + if (aKeys.has(key) && bKeys.has(key)) { + ret[key] = objMerge(a[key], b[key], max_depth - 1); + } else if (aKeys.has(key)) { + ret[key] = a[key]; + } else { + ret[key] = b[key]; + } + } + return ret; + } else { + return b ? b : a; + } +} diff --git a/typescript/utils/src/utils.ts b/typescript/utils/src/utils.ts index 9d61ac780..9cfae6599 100644 --- a/typescript/utils/src/utils.ts +++ b/typescript/utils/src/utils.ts @@ -9,6 +9,10 @@ import { ParsedMessage, } from './types'; +export function exclude(item: T, list: T[]) { + return list.filter((i) => i !== item); +} + export function assert(predicate: any, errorMessage?: string) { if (!predicate) { throw new Error(errorMessage ?? 'Error');