Refactor IGP related code out of HyperlaneCore (#1907)
### Description A number of changes intended to make the SDK more easily consumed by hyperlane-deploy - Splits the IGP related code out of HyperlaneCore* into HyperlaneIgp* - Moves agent config building from infra to SDK so that it can be accessed by hyperlane-deploy - Modifies CoreConfig to specify a ChainMap<MultisigIsmConfig> (i.e. specify everything that you need to know to deploy to a chain in that chain's CoreConfig) - Removes core.extendWithConnectionClientConfig - Moves common logic into HyperlaneAppGovernor to be shared between HyperlaneCoreGovernor and HyperlaneIgpGovernor - Adds TestRecipient contract addresses to the SDK addresses for consumption by hyperlane-deploy - Allow buildContracts to build when address keys are a superset of factory keys (via filterContracts). This is useful as it allows us to just throw all the addresses at a HyperlaneApp constructor without needing to remove the ones that aren't relevant - Removes InterchainGasCalculator, the logic we want to keep now lives in HyperlaneIgp - Allows chains to be added to a MultiProvider ### Drive-by changes - Merges the infra govern script into the check script (to dedupe) - Some minor renaming where "Core" was being used liberally - Default to using whatever is configured in `owners.ts` in the router configs for infra (for ICA, IQS, HelloWorld, and LL deployment) ### Related issues - Fixes #[issue number here] ### Backward compatibility _Are these changes backward compatible?_ No, existing consumers of the SDK may need to adjust. _Are there any infrastructure implications, e.g. changes that would prohibit deploying older commits using this infra tooling?_ None ### Testing _What kind of testing have these changes undergone?_ Manual / Unit Testsyorhodes-patch-1
parent
e8d90775f7
commit
97d17570cc
@ -1 +1 @@ |
||||
Subproject commit 99bcdfca24e8c9a5f2d3cca8eb6da891ff3c715e |
||||
Subproject commit 3b443c79bcb7671ef3d49ba7d6765c9adb139789 |
@ -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<CoreConfig> = { |
||||
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<CoreConfig> = objMap(owners, (local, owner) => { |
||||
return { |
||||
owner, |
||||
multisigIsm: Object.fromEntries( |
||||
Object.entries(defaultMultisigIsmConfigs).filter( |
||||
([chain]) => chain !== local && chainNames.includes(chain), |
||||
), |
||||
), |
||||
}; |
||||
}); |
||||
|
@ -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<OverheadIgpConfig> = 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, |
||||
), |
||||
]), |
||||
), |
||||
}; |
||||
}, |
||||
); |
@ -0,0 +1,13 @@ |
||||
import { ChainMap } from '@hyperlane-xyz/sdk'; |
||||
|
||||
export const owners: ChainMap<string> = { |
||||
celo: '0x1DE69322B55AC7E0999F8e7738a1428C8b130E4d', |
||||
ethereum: '0x12C5AB61Fe17dF9c65739DBa73dF294708f78d23', |
||||
avalanche: '0xDF9B28B76877f1b1B4B8a11526Eb7D8D7C49f4f3', |
||||
polygon: '0x0D195469f76146F6ae3De8fc887e0f0DFBA691e7', |
||||
bsc: '0xA0d3dcB9d61Fba32cc02Ad63983e101b29E2f28a', |
||||
arbitrum: '0xbA47E1b575980B7D1b1508cc48bE1Df4EE508111', |
||||
optimism: '0xb523CFAf45AACF472859f8B793CB0BFDB16bD257', |
||||
moonbeam: '0xF0cb1f968Df01fc789762fddBfA704AE0F952197', |
||||
gnosis: '0x36b0AA0e7d04e7b825D7E409FEa3c9A3d57E4C22', |
||||
}; |
@ -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<CoreConfig> = { |
||||
// 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<CoreConfig> = objMap(owners, (local, owner) => { |
||||
return { |
||||
owner, |
||||
multisigIsm: Object.fromEntries( |
||||
Object.entries(multisigIsm).filter(([chain]) => chain !== local), |
||||
), |
||||
}; |
||||
}); |
||||
|
@ -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<OverheadIgpConfig> = 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, |
||||
), |
||||
]), |
||||
), |
||||
}; |
||||
}, |
||||
); |
@ -0,0 +1,17 @@ |
||||
import { ChainMap, MultisigIsmConfig } from '@hyperlane-xyz/sdk'; |
||||
|
||||
export const multisigIsm: ChainMap<MultisigIsmConfig> = { |
||||
// Validators are hardhat accounts 1-3
|
||||
test1: { |
||||
validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'], |
||||
threshold: 1, |
||||
}, |
||||
test2: { |
||||
validators: ['0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'], |
||||
threshold: 1, |
||||
}, |
||||
test3: { |
||||
validators: ['0x90f79bf6eb2c4f870365e785982e1f101e93b906'], |
||||
threshold: 1, |
||||
}, |
||||
}; |
@ -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<types.Address> = Object.fromEntries( |
||||
chainNames.map((chain) => [chain, OWNER_ADDRESS]), |
||||
); |
@ -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<CoreConfig> = { |
||||
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<CoreConfig> = objMap(owners, (local, owner) => { |
||||
return { |
||||
owner, |
||||
multisigIsm: Object.fromEntries( |
||||
Object.entries(defaultMultisigIsmConfigs).filter( |
||||
([chain]) => chain !== local && chainNames.includes(chain), |
||||
), |
||||
), |
||||
}; |
||||
}); |
||||
|
@ -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<OverheadIgpConfig> = 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, |
||||
), |
||||
]), |
||||
), |
||||
}; |
||||
}, |
||||
); |
@ -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<types.Address> = Object.fromEntries( |
||||
chainNames.map((chain) => [chain, DEPLOYER_ADDRESS]), |
||||
); |
@ -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)); |
@ -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<any, any, any>; |
||||
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)); |
@ -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)); |
@ -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; |
||||
} |
||||
|
@ -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<OverheadIgpConfig>, |
||||
environment: DeployEnvironment, |
||||
) { |
||||
super(multiProvider, configMap); |
||||
this.environment = environment; |
||||
} |
||||
|
||||
async deployInterchainGasPaymaster( |
||||
chain: ChainName, |
||||
proxyAdmin: ProxyAdmin, |
||||
storageGasOracle: StorageGasOracle, |
||||
): Promise< |
||||
ProxiedContract<InterchainGasPaymaster, TransparentProxyAddresses> |
||||
> { |
||||
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<OverheadIgp> { |
||||
const deployOpts = { |
||||
create2Salt: ethers.utils.solidityKeccak256( |
||||
['string', 'string', 'uint8'], |
||||
[this.environment, 'defaultIsmInterchainGasPaymaster', 4], |
||||
), |
||||
}; |
||||
return super.deployOverheadInterchainGasPaymaster( |
||||
chain, |
||||
interchainGasPaymasterAddress, |
||||
deployOpts, |
||||
); |
||||
} |
||||
} |
@ -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<types.Address>) { |
||||
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}`, |
||||
); |
||||
} |
||||
} |
||||
} |
@ -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<any>, |
||||
Config, |
||||
> { |
||||
readonly checker: HyperlaneAppChecker<App, Config>; |
||||
private owners: ChainMap<types.Address>; |
||||
private calls: ChainMap<AnnotatedCallData[]>; |
||||
private canPropose: ChainMap<Map<string, boolean>>; |
||||
|
||||
constructor( |
||||
checker: HyperlaneAppChecker<App, Config>, |
||||
owners: ChainMap<types.Address>, |
||||
) { |
||||
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<boolean> => { |
||||
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<void>; |
||||
|
||||
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<SubmissionType> { |
||||
const multiProvider = this.checker.multiProvider; |
||||
const signer = multiProvider.getSigner(chain); |
||||
const signerAddress = await signer.getAddress(); |
||||
|
||||
const canUseSubmissionType = async ( |
||||
submitterAddress: types.Address, |
||||
): Promise<boolean> => { |
||||
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}`, |
||||
}); |
||||
} |
||||
} |
@ -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<ChainMap<AgentChainSetupBase>>; |
||||
tracing?: { |
||||
level?: string; |
||||
fmt?: 'json'; |
||||
}; |
||||
}; |
||||
|
||||
export function buildAgentConfig( |
||||
chains: ChainName[], |
||||
multiProvider: MultiProvider, |
||||
addresses: ChainMap<HyperlaneAgentAddresses>, |
||||
startBlocks: ChainMap<number>, |
||||
): 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; |
||||
} |
@ -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', |
||||
} |
@ -0,0 +1,173 @@ |
||||
import { MultisigIsmConfig } from '../core/types'; |
||||
import { ChainMap } from '../types'; |
||||
|
||||
export const defaultMultisigIsmConfigs: ChainMap<MultisigIsmConfig> = { |
||||
// ----------------- 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', |
||||
], |
||||
}, |
||||
}; |
@ -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<Mailbox, TransparentProxyAddresses>; |
||||
multisigIsm: LegacyMultisigIsm; |
||||
proxyAdmin: ProxyAdmin; |
||||
validatorAnnounce: ValidatorAnnounce; |
||||
}; |
||||
|
||||
export type CoreContracts = GasOracleContracts & |
||||
ConnectionClientContracts & { |
||||
mailbox: ProxiedContract<Mailbox, TransparentProxyAddresses>; |
||||
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(), |
||||
}; |
||||
|
@ -1,15 +0,0 @@ |
||||
import { types } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { ChainMap } from '../types'; |
||||
import { objMap } from '../utils/objects'; |
||||
|
||||
export function getChainToOwnerMap( |
||||
configMap: ChainMap<any>, |
||||
owner: types.Address, |
||||
): ChainMap<{ owner: string }> { |
||||
return objMap(configMap, () => { |
||||
return { |
||||
owner, |
||||
}; |
||||
}); |
||||
} |
@ -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<IgpContracts>; |
||||
|
||||
export class HyperlaneIgp extends HyperlaneApp<IgpContracts> { |
||||
constructor(contractsMap: IgpContractsMap, multiProvider: MultiProvider) { |
||||
super(contractsMap, multiProvider); |
||||
} |
||||
|
||||
static fromAddresses( |
||||
addresses: ChainMap<HyperlaneAddresses>, |
||||
multiProvider: MultiProvider, |
||||
): HyperlaneIgp { |
||||
const { contracts, intersectionProvider } = |
||||
this.buildContracts<IgpContracts>(addresses, igpFactories, multiProvider); |
||||
return new HyperlaneIgp(contracts, intersectionProvider); |
||||
} |
||||
|
||||
static fromEnvironment<Env extends HyperlaneEnvironment>( |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
const originProvider = this.multiProvider.getProvider(origin); |
||||
const igp = InterchainGasPaymaster__factory.connect( |
||||
interchainGasPaymasterAddress, |
||||
originProvider, |
||||
); |
||||
const domainId = this.multiProvider.getDomainId(destination); |
||||
return igp.quoteGasPayment(domainId, gasAmount); |
||||
} |
||||
} |
@ -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<void> { |
||||
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<void> { |
||||
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<void> { |
||||
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<void> { |
||||
const contracts = this.app.getContracts(chain); |
||||
await this.checkProxiedContract( |
||||
chain, |
||||
'InterchainGasPaymaster', |
||||
contracts.interchainGasPaymaster.addresses, |
||||
contracts.proxyAdmin.address, |
||||
); |
||||
} |
||||
|
||||
async checkOverheadInterchainGasPaymaster(local: ChainName): Promise<void> { |
||||
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<void> { |
||||
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}`); |
||||
} |
||||
} |
||||
} |
@ -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<number | undefined>; |
||||
|
||||
constructor( |
||||
multiProvider: MultiProvider, |
||||
configMap: ChainMap<OverheadIgpConfig>, |
||||
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<InterchainGasPaymaster, TransparentProxyAddresses> |
||||
> { |
||||
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<OverheadIgp> { |
||||
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<StorageGasOracle> { |
||||
return this.deployContract(chain, 'storageGasOracle', [], deployOpts); |
||||
} |
||||
|
||||
async deployContracts( |
||||
chain: ChainName, |
||||
config: OverheadIgpConfig, |
||||
): Promise<IgpContracts> { |
||||
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, |
||||
}; |
||||
} |
||||
} |
@ -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')); |
||||
}); |
||||
}); |
||||
}); |
@ -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<BigNumber> { |
||||
return super.estimateGasForProcess(origin, destination); |
||||
} |
||||
estimateGasForHandle(message: ParsedMessage): Promise<BigNumber> { |
||||
return super.estimateGasForHandle(message); |
||||
} |
||||
convertBetweenTokens( |
||||
fromChain: ChainName, |
||||
toChain: ChainName, |
||||
fromAmount: BigNumber, |
||||
): Promise<BigNumber> { |
||||
return super.convertBetweenTokens(fromChain, toChain, fromAmount); |
||||
} |
||||
tokenDecimals(chain: ChainName): number { |
||||
return super.tokenDecimals(chain); |
||||
} |
||||
getGasPrice(chain: ChainName): Promise<BigNumber> { |
||||
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<TestChainNames, Chains.test2> = { |
||||
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; |
||||
}); |
||||
}); |
||||
}); |
@ -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 extends CoreEnvironment>( |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
// 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<BigNumber> { |
||||
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<BigNumber> { |
||||
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<BigNumber> { |
||||
// 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); |
||||
} |
||||
} |
@ -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(), |
||||
}; |
@ -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<types.Address>; |
||||
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<GasOracleContractType>; |
||||
}; |
||||
|
||||
export enum CoreViolationType { |
||||
MultisigIsm = 'MultisigIsm', |
||||
Mailbox = 'Mailbox', |
||||
ConnectionManager = 'ConnectionManager', |
||||
ValidatorAnnounce = 'ValidatorAnnounce', |
||||
} |
||||
|
||||
export enum MultisigIsmViolationType { |
||||
EnrolledValidators = 'EnrolledValidators', |
||||
Threshold = 'Threshold', |
||||
} |
||||
export type OverheadIgpConfig = IgpConfig & { |
||||
overhead: ChainMap<number>; |
||||
}; |
||||
|
||||
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<types.Address>; |
||||
expected: Set<types.Address>; |
||||
} |
||||
|
||||
export interface ThresholdViolation extends MultisigIsmViolation { |
||||
subType: MultisigIsmViolationType.Threshold; |
||||
actual: number; |
||||
expected: number; |
||||
export interface IgpGasOraclesViolation extends IgpViolation { |
||||
subType: IgpViolationType.GasOracles; |
||||
contract: InterchainGasPaymaster; |
||||
actual: ChainMap<types.Address>; |
||||
expected: ChainMap<types.Address>; |
||||
} |
||||
|
||||
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<BigNumber>; |
||||
expected: ChainMap<BigNumber>; |
||||
} |
||||
|
@ -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<Contracts> { |
||||
getSecurityModules = (): Promise<ChainMap<types.Address>> => |
||||
promiseObjAll( |
||||
objMap(this.contractsMap, (_, contracts) => |
||||
contracts.router.interchainSecurityModule(), |
||||
), |
||||
); |
||||
|
||||
getOwners = (): Promise<ChainMap<types.Address>> => |
||||
promiseObjAll( |
||||
objMap(this.contractsMap, (_, contracts) => contracts.router.owner()), |
||||
); |
||||
} |
||||
|
||||
export class GasRouterApp< |
||||
Contracts extends RouterContracts<GasRouter>, |
||||
> extends RouterApp<Contracts> { |
||||
async quoteGasPayment( |
||||
origin: ChainName, |
||||
destination: ChainName, |
||||
): Promise<BigNumber> { |
||||
return this.getContracts(origin).router.quoteGasPayment( |
||||
this.multiProvider.getDomainId(destination), |
||||
); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue