diff --git a/rust/agents/relayer/src/msg/metadata/ccip_read.rs b/rust/agents/relayer/src/msg/metadata/ccip_read.rs index 497839a7b0..1ca3fbe1b6 100644 --- a/rust/agents/relayer/src/msg/metadata/ccip_read.rs +++ b/rust/agents/relayer/src/msg/metadata/ccip_read.rs @@ -54,16 +54,18 @@ impl MetadataBuilder for CcipReadIsmMetadataBuilder { }; for url in info.urls.iter() { + // Need to explicitly convert the sender H160 the hex because the `ToString` implementation + // for `H160` truncates the output. (e.g. `0xc66a…7b6f` instead of returning + // the full address) + let sender_as_bytes = &bytes_to_hex(info.sender.as_bytes()); + let data_as_bytes = &info.call_data.to_string(); let interpolated_url = url - // Need to explicitly convert the sender H160 the hex because the `ToString` implementation - // for `H160` truncates the output. (e.g. `0xc66a…7b6f` instead of returning - // the full address) - .replace("{sender}", &bytes_to_hex(info.sender.as_bytes())) - .replace("{data}", &info.call_data.to_string()); + .replace("{sender}", sender_as_bytes) + .replace("{data}", data_as_bytes); let res = if !url.contains("{data}") { let body = json!({ - "data": info.call_data.to_string(), - "sender": info.sender.to_string(), + "sender": sender_as_bytes, + "data": data_as_bytes }); Client::new() .post(interpolated_url) diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index 60a30dbefe..15a53a4010 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -52,7 +52,7 @@ export const ethereumMainnetConfigs: ChainMap = { export const nonEthereumMainnetConfigs: ChainMap = { // solana: chainMetadata.solana, // neutron: chainMetadata.neutron, - injective: chainMetadata.injective, + // injective: chainMetadata.injective, }; export const mainnetConfigs: ChainMap = { diff --git a/typescript/infra/config/environments/mainnet3/funding.ts b/typescript/infra/config/environments/mainnet3/funding.ts index 9207a10d27..a2b8ec6a5f 100644 --- a/typescript/infra/config/environments/mainnet3/funding.ts +++ b/typescript/infra/config/environments/mainnet3/funding.ts @@ -9,7 +9,7 @@ import { environment } from './chains'; export const keyFunderConfig: KeyFunderConfig = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '0e3f73f-20240206-160718', + tag: 'af21f03-20240212-175700', }, // We're currently using the same deployer key as mainnet. // To minimize nonce clobbering we offset the key funder cron diff --git a/typescript/infra/config/environments/mainnet3/gas-oracle.ts b/typescript/infra/config/environments/mainnet3/gas-oracle.ts index 0775a7abc1..2b33470058 100644 --- a/typescript/infra/config/environments/mainnet3/gas-oracle.ts +++ b/typescript/infra/config/environments/mainnet3/gas-oracle.ts @@ -55,51 +55,51 @@ const gasPrices: ChainMap = { viction: ethers.utils.parseUnits('0.25', 'gwei'), }; -// Accurate from coingecko as of Mar 9, 2023. +// Accurate from coingecko as of Feb 9, 2024. // These aren't overestimates because the exchange rates between // tokens are what matters. These generally have high beta const tokenUsdPrices: ChainMap = { // https://www.coingecko.com/en/coins/bnb - bsc: ethers.utils.parseUnits('230.55', TOKEN_EXCHANGE_RATE_DECIMALS), + bsc: ethers.utils.parseUnits('323.61', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/avalanche - avalanche: ethers.utils.parseUnits('20.25', TOKEN_EXCHANGE_RATE_DECIMALS), + avalanche: ethers.utils.parseUnits('38.27', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/polygon - polygon: ethers.utils.parseUnits('0.75', TOKEN_EXCHANGE_RATE_DECIMALS), + polygon: ethers.utils.parseUnits('0.85', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/celo - celo: ethers.utils.parseUnits('0.52', TOKEN_EXCHANGE_RATE_DECIMALS), + celo: ethers.utils.parseUnits('0.73', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - arbitrum: ethers.utils.parseUnits('2000.00', TOKEN_EXCHANGE_RATE_DECIMALS), + arbitrum: ethers.utils.parseUnits('2495.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - optimism: ethers.utils.parseUnits('2000.00', TOKEN_EXCHANGE_RATE_DECIMALS), + optimism: ethers.utils.parseUnits('2495.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - ethereum: ethers.utils.parseUnits('2000.00', TOKEN_EXCHANGE_RATE_DECIMALS), - // https://www.coingecko.com/en/coins/moonbeam - moonbeam: ethers.utils.parseUnits('0.266', TOKEN_EXCHANGE_RATE_DECIMALS), - // xDAI - gnosis: ethers.utils.parseUnits('1.00', TOKEN_EXCHANGE_RATE_DECIMALS), - // https://www.coingecko.com/en/coins/solana - solana: ethers.utils.parseUnits('58.85', TOKEN_EXCHANGE_RATE_DECIMALS), + ethereum: ethers.utils.parseUnits('2495.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - base: ethers.utils.parseUnits('2000.00', TOKEN_EXCHANGE_RATE_DECIMALS), + base: ethers.utils.parseUnits('2495.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum - scroll: ethers.utils.parseUnits('2000.00', TOKEN_EXCHANGE_RATE_DECIMALS), + scroll: ethers.utils.parseUnits('2495.00', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum polygonzkevm: ethers.utils.parseUnits( - '2000.00', + '2495.00', TOKEN_EXCHANGE_RATE_DECIMALS, ), - // https://www.coingecko.com/en/coins/neutron - neutron: ethers.utils.parseUnits('0.304396', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/ethereum mantapacific: ethers.utils.parseUnits( - '1619.00', + '2495.00', TOKEN_EXCHANGE_RATE_DECIMALS, ), + // https://www.coingecko.com/en/coins/moonbeam + moonbeam: ethers.utils.parseUnits('0.387', TOKEN_EXCHANGE_RATE_DECIMALS), + // xDAI + gnosis: ethers.utils.parseUnits('1.00', TOKEN_EXCHANGE_RATE_DECIMALS), + // https://www.coingecko.com/en/coins/solana + solana: ethers.utils.parseUnits('108.01', TOKEN_EXCHANGE_RATE_DECIMALS), + // https://www.coingecko.com/en/coins/neutron + neutron: ethers.utils.parseUnits('1.14', TOKEN_EXCHANGE_RATE_DECIMALS), // https://www.coingecko.com/en/coins/injective - injective: ethers.utils.parseUnits('32.78', TOKEN_EXCHANGE_RATE_DECIMALS), - inevm: ethers.utils.parseUnits('32.78', TOKEN_EXCHANGE_RATE_DECIMALS), // 1:1 injective + injective: ethers.utils.parseUnits('35.07', TOKEN_EXCHANGE_RATE_DECIMALS), + inevm: ethers.utils.parseUnits('35.07', TOKEN_EXCHANGE_RATE_DECIMALS), // 1:1 injective // https://www.coingecko.com/en/coins/viction - viction: ethers.utils.parseUnits('0.881', TOKEN_EXCHANGE_RATE_DECIMALS), + viction: ethers.utils.parseUnits('0.726', TOKEN_EXCHANGE_RATE_DECIMALS), }; // Gets the exchange rate of the remote quoted in local tokens diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts index d5e999abfa..f883c2f183 100644 --- a/typescript/infra/config/environments/mainnet3/igp.ts +++ b/typescript/infra/config/environments/mainnet3/igp.ts @@ -1,6 +1,5 @@ import { ChainMap, - GasOracleContractType, IgpConfig, defaultMultisigConfigs, multisigIsmVerificationCost, @@ -12,6 +11,7 @@ import { ethereumChainNames, supportedChainNames, } from './chains'; +import { storageGasOracleConfig } from './gas-oracle'; import { owners } from './owners'; // TODO: make this generic @@ -20,15 +20,6 @@ const DEPLOYER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; const FOREIGN_DEFAULT_OVERHEAD = 600_000; // cosmwasm warp route somewhat arbitrarily chosen -function getGasOracles(local: MainnetChains) { - return Object.fromEntries( - exclude(local, supportedChainNames).map((name) => [ - name, - GasOracleContractType.StorageGasOracle, - ]), - ); -} - const remoteOverhead = (remote: MainnetChains) => ethereumChainNames.includes(remote) ? multisigIsmVerificationCost( @@ -41,11 +32,11 @@ export const igp: ChainMap = objMap(owners, (local, owner) => ({ ...owner, oracleKey: DEPLOYER_ADDRESS, beneficiary: KEY_FUNDER_ADDRESS, - gasOracleType: getGasOracles(local), overhead: Object.fromEntries( exclude(local, supportedChainNames).map((remote) => [ remote, remoteOverhead(remote), ]), ), + oracleConfig: storageGasOracleConfig[local], })); diff --git a/typescript/infra/config/environments/mainnet3/igp/verification.json b/typescript/infra/config/environments/mainnet3/igp/verification.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/igp/verification.json @@ -0,0 +1 @@ +{} diff --git a/typescript/infra/config/environments/mainnet3/index.ts b/typescript/infra/config/environments/mainnet3/index.ts index 29282e3839..127af12260 100644 --- a/typescript/infra/config/environments/mainnet3/index.ts +++ b/typescript/infra/config/environments/mainnet3/index.ts @@ -13,7 +13,6 @@ import { agents } from './agent'; import { environment as environmentName, mainnetConfigs } from './chains'; import { core } from './core'; import { keyFunderConfig } from './funding'; -import { storageGasOracleConfig } from './gas-oracle'; import { helloWorld } from './helloworld'; import { igp } from './igp'; import { infrastructure } from './infrastructure'; @@ -54,7 +53,6 @@ export const environment: EnvironmentConfig = { infra: infrastructure, helloWorld, keyFunderConfig, - storageGasOracleConfig, liquidityLayerConfig: { bridgeAdapters: bridgeAdapterConfigs, relayer: relayerConfig, diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index 9794189456..1a1a87a7a2 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -8,6 +8,7 @@ export const timelocks: ChainMap
= { }; export const safes: ChainMap
= { + mantapacific: '0x03ed2D65f2742193CeD99D48EbF1F1D6F12345B6', // does not have a UI celo: '0x1DE69322B55AC7E0999F8e7738a1428C8b130E4d', ethereum: '0x12C5AB61Fe17dF9c65739DBa73dF294708f78d23', avalanche: '0xDF9B28B76877f1b1B4B8a11526Eb7D8D7C49f4f3', diff --git a/typescript/infra/config/environments/test/index.ts b/typescript/infra/config/environments/test/index.ts index a458f47a08..c11ab549f0 100644 --- a/typescript/infra/config/environments/test/index.ts +++ b/typescript/infra/config/environments/test/index.ts @@ -7,7 +7,6 @@ import { EnvironmentConfig } from '../../../src/config'; import { agents } from './agent'; import { testConfigs } from './chains'; import { core } from './core'; -import { storageGasOracleConfig } from './gas-oracle'; import { igp } from './igp'; import { infra } from './infra'; import { owners } from './owners'; @@ -31,5 +30,4 @@ export const environment: EnvironmentConfig = { getKeys: async () => { throw new Error('Not implemented'); }, - storageGasOracleConfig, }; diff --git a/typescript/infra/config/environments/testnet4/funding.ts b/typescript/infra/config/environments/testnet4/funding.ts index 78b39dcc49..ae2bdfdec3 100644 --- a/typescript/infra/config/environments/testnet4/funding.ts +++ b/typescript/infra/config/environments/testnet4/funding.ts @@ -9,7 +9,7 @@ import { environment } from './chains'; export const keyFunderConfig: KeyFunderConfig = { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '3a165e4-20240131-141310', + tag: 'b09cc90-20240208-194409', }, // We're currently using the same deployer key as testnet2. // To minimize nonce clobbering we offset the key funder cron diff --git a/typescript/infra/config/environments/testnet4/igp.ts b/typescript/infra/config/environments/testnet4/igp.ts index e0b44bcae3..2294bff2da 100644 --- a/typescript/infra/config/environments/testnet4/igp.ts +++ b/typescript/infra/config/environments/testnet4/igp.ts @@ -1,7 +1,5 @@ import { ChainMap, - ChainName, - GasOracleContractType, IgpConfig, defaultMultisigConfigs, multisigIsmVerificationCost, @@ -9,23 +7,15 @@ import { import { exclude, objMap } from '@hyperlane-xyz/utils'; import { supportedChainNames } from './chains'; +import { storageGasOracleConfig } from './gas-oracle'; import { owners } from './owners'; -function getGasOracles(local: ChainName) { - return Object.fromEntries( - exclude(local, supportedChainNames).map((name) => [ - name, - GasOracleContractType.StorageGasOracle, - ]), - ); -} - export const igp: ChainMap = objMap(owners, (chain, ownerConfig) => { return { ...ownerConfig, oracleKey: ownerConfig.owner, beneficiary: ownerConfig.owner, - gasOracleType: getGasOracles(chain), + oracleConfig: storageGasOracleConfig[chain], overhead: Object.fromEntries( exclude(chain, supportedChainNames).map((remote) => [ remote, diff --git a/typescript/infra/config/environments/testnet4/index.ts b/typescript/infra/config/environments/testnet4/index.ts index 8dd7c68ec3..b522e7eef3 100644 --- a/typescript/infra/config/environments/testnet4/index.ts +++ b/typescript/infra/config/environments/testnet4/index.ts @@ -12,7 +12,6 @@ import { agents } from './agent'; import { environment as environmentName, testnetConfigs } from './chains'; import { core } from './core'; import { keyFunderConfig } from './funding'; -import { storageGasOracleConfig } from './gas-oracle'; import { helloWorld } from './helloworld'; import { igp } from './igp'; import { infrastructure } from './infrastructure'; @@ -51,5 +50,4 @@ export const environment: EnvironmentConfig = { bridgeAdapters: bridgeAdapterConfigs, relayer: liquidityLayerRelayerConfig, }, - storageGasOracleConfig, }; diff --git a/typescript/infra/config/kathy.json b/typescript/infra/config/kathy.json index 432476055b..e31ba0cfc4 100644 --- a/typescript/infra/config/kathy.json +++ b/typescript/infra/config/kathy.json @@ -2,7 +2,7 @@ "mainnet3": { "hyperlane": "0x5fb02f40f56d15f0442a39d11a23f73747095b20", "neutron": "", - "rc": "" + "rc": "0x7691f88dccc1554788ba8f226a4a31e5f3ead7c3" }, "testnet4": { "hyperlane": "0x1e8834ff0669b13cf5d37685c5327b82dbae1144", diff --git a/typescript/infra/helm/key-funder/templates/cron-job.yaml b/typescript/infra/helm/key-funder/templates/cron-job.yaml index ed2ce3dce3..a1ac085f85 100644 --- a/typescript/infra/helm/key-funder/templates/cron-job.yaml +++ b/typescript/infra/helm/key-funder/templates/cron-job.yaml @@ -28,8 +28,6 @@ spec: {{- range $context, $roles := .Values.hyperlane.contextsAndRolesToFund }} - --contexts-and-roles - {{ $context }}={{ join "," $roles }} - - -f - - /addresses-secret/{{ $context }}-addresses.json {{- end }} {{- if .Values.hyperlane.connectionType }} - --connection-type @@ -48,4 +46,4 @@ spec: - name: key-funder-addresses-secret secret: secretName: key-funder-addresses-secret - defaultMode: 0400 + defaultMode: 0400 \ No newline at end of file diff --git a/typescript/infra/scripts/deploy.ts b/typescript/infra/scripts/deploy.ts index 057946d6d6..1fdf1ff9fd 100644 --- a/typescript/infra/scripts/deploy.ts +++ b/typescript/infra/scripts/deploy.ts @@ -73,10 +73,7 @@ async function main() { } else if (module === Modules.WARP) { throw new Error('Warp is not supported. Use CLI instead.'); } else if (module === Modules.INTERCHAIN_GAS_PAYMASTER) { - config = { - ...envConfig.igp, - oracleConfig: envConfig.storageGasOracleConfig, - }; + config = envConfig.igp; deployer = new HyperlaneIgpDeployer(multiProvider); } else if (module === Modules.INTERCHAIN_ACCOUNTS) { const core = HyperlaneCore.fromEnvironment(env, multiProvider); diff --git a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts index d80fa088da..7fa542f325 100644 --- a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts +++ b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts @@ -17,23 +17,28 @@ import { log, objFilter, objMap, - promiseObjAll, warn, } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; -import { KeyAsAddress, getRoleKeysPerChain } from '../../src/agents/key-utils'; import { - BaseCloudAgentKey, + KeyAsAddress, + fetchLocalKeyAddresses, + getRoleKeysPerChain, +} from '../../src/agents/key-utils'; +import { + BaseAgentKey, + LocalAgentKey, ReadOnlyCloudAgentKey, } from '../../src/agents/keys'; import { DeployEnvironment } from '../../src/config'; import { deployEnvToSdkEnv } from '../../src/config/environment'; import { ContextAndRoles, ContextAndRolesMap } from '../../src/config/funding'; -import { Role } from '../../src/roles'; +import { FundableRole, Role } from '../../src/roles'; import { submitMetrics } from '../../src/utils/metrics'; import { assertContext, + assertFundableRole, assertRole, isEthereumProtocolChain, readJSONAtPath, @@ -275,7 +280,7 @@ async function main() { const contexts = Object.keys(argv.contextsAndRoles) as Contexts[]; contextFunders = await Promise.all( contexts.map((context) => - ContextFunder.fromContext( + ContextFunder.fromLocal( environment, multiProvider, context, @@ -303,20 +308,20 @@ async function main() { class ContextFunder { igp: HyperlaneIgp; - keysToFundPerChain: ChainMap; + keysToFundPerChain: ChainMap; constructor( public readonly environment: DeployEnvironment, public readonly multiProvider: MultiProvider, - roleKeysPerChain: ChainMap>, + roleKeysPerChain: ChainMap>, public readonly context: Contexts, - public readonly rolesToFund: Role[], + public readonly rolesToFund: FundableRole[], public readonly skipIgpClaim: boolean, ) { // At the moment, only blessed EVM chains are supported roleKeysPerChain = objFilter( roleKeysPerChain, - (chain, _roleKeys): _roleKeys is Record => { + (chain, _roleKeys): _roleKeys is Record => { const valid = isEthereumProtocolChain(chain) && multiProvider.tryGetChainName(chain) !== null; @@ -335,12 +340,12 @@ class ContextFunder { ); this.keysToFundPerChain = objMap(roleKeysPerChain, (_chain, roleKeys) => { return Object.keys(roleKeys).reduce((agg, roleStr) => { - const role = roleStr as Role; + const role = roleStr as FundableRole; if (this.rolesToFund.includes(role)) { return [...agg, ...roleKeys[role]]; } return agg; - }, [] as BaseCloudAgentKey[]); + }, [] as BaseAgentKey[]); }); } @@ -417,29 +422,49 @@ class ContextFunder { ); } - // The keys here are not ReadOnlyCloudAgentKeys, instead they are AgentGCPKey or AgentAWSKeys, - // which require credentials to fetch. If you want to avoid requiring credentials, use - // fromSerializedAddressFile instead. - static async fromContext( + // the keys are retrieved from the local artifacts in the infra/config/relayer.json or infra/config/kathy.json + static async fromLocal( environment: DeployEnvironment, multiProvider: MultiProvider, context: Contexts, - rolesToFund: Role[], + rolesToFund: FundableRole[], skipIgpClaim: boolean, ) { - const agentConfig = getAgentConfig(context, environment); - const roleKeysPerChain = getRoleKeysPerChain(agentConfig); - // Fetch all the keys - await promiseObjAll( - objMap(roleKeysPerChain, (_chain, roleKeys) => { - return promiseObjAll( - objMap(roleKeys, (_role, keys) => { - return Promise.all(keys.map((key) => key.fetch())); - }), + // only roles that are fundable keys ie. relayer and kathy + const fundableRoleKeys: Record = { + [Role.Relayer]: '', + [Role.Kathy]: '', + }; + const roleKeysPerChain: ChainMap> = {}; + const chains = getEnvironmentConfig(environment).chainMetadataConfigs; + for (const role of rolesToFund) { + assertFundableRole(role); // only the relayer and kathy are fundable keys + const roleAddress = fetchLocalKeyAddresses(role)[environment][context]; + if (!roleAddress) { + throw Error( + `Could not find address for ${role} in ${environment} ${context}`, ); - }), - ); - + } + fundableRoleKeys[role] = roleAddress; + + for (const chain of Object.keys(chains)) { + if (!roleKeysPerChain[chain as ChainName]) { + roleKeysPerChain[chain as ChainName] = { + [Role.Relayer]: [], + [Role.Kathy]: [], + }; + } + roleKeysPerChain[chain][role] = [ + new LocalAgentKey( + environment, + context, + role, + fundableRoleKeys[role as FundableRole], + chain, + ), + ]; + } + } return new ContextFunder( environment, multiProvider, @@ -499,7 +524,7 @@ class ContextFunder { } private async attemptToFundKey( - key: BaseCloudAgentKey, + key: BaseAgentKey, chain: ChainName, ): Promise { const provider = this.multiProvider.tryGetProvider(chain); @@ -620,7 +645,7 @@ class ContextFunder { // is lower than the desired balance by the min delta private async fundKeyIfRequired( chain: ChainName, - key: BaseCloudAgentKey, + key: BaseAgentKey, desiredBalance: BigNumber, ) { const fundingAmount = await this.getFundingAmount( @@ -835,13 +860,13 @@ async function getAddressInfo( } async function getKeyInfo( - key: BaseCloudAgentKey, + key: BaseAgentKey, chain: ChainName, provider: ethers.providers.Provider, ) { return { ...(await getAddressInfo(key.address, chain, provider)), - context: key.context, + context: (key as LocalAgentKey).context, originChain: key.chainName, role: key.role, }; @@ -876,7 +901,9 @@ function parseContextAndRoles(str: string): ContextAndRoles { for (const role of roles) { if (!validRoles.has(role)) { throw Error( - `Invalid role ${role}, must be one of ${Array.from(validRoles)}`, + `Invalid fundable role ${role}, must be one of ${Array.from( + validRoles, + )}`, ); } } diff --git a/typescript/infra/scripts/gas-oracle/compare-token-exchange-rates.ts b/typescript/infra/scripts/gas-oracle/compare-token-exchange-rates.ts deleted file mode 100644 index def3e1260f..0000000000 --- a/typescript/infra/scripts/gas-oracle/compare-token-exchange-rates.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ethers } from 'ethers'; - -import { - ChainName, - CoinGeckoTokenPriceGetter, - HyperlaneCore, - prettyTokenExchangeRate, -} from '@hyperlane-xyz/sdk'; - -import { StorageGasOracleConfig } from '../../src/config'; -import { deployEnvToSdkEnv } from '../../src/config/environment'; -import { - TOKEN_EXCHANGE_RATE_DECIMALS, - TOKEN_EXCHANGE_RATE_SCALE, -} from '../../src/config/gas-oracle'; -import { getArgs } from '../agent-utils'; -import { getEnvironmentConfig } from '../core-utils'; - -// Compares the token exchange rate between chains according to the config -// to the exchange rates using current Coingecko prices. The config exchange -// rates apply the 30% spread / fee, so we expect config prices to be ~30% higher. -async function main() { - const tokenPriceGetter = CoinGeckoTokenPriceGetter.withDefaultCoinGecko(); - - const { environment } = await getArgs().argv; - const coreEnvConfig = getEnvironmentConfig(environment); - const multiProvider = await coreEnvConfig.getMultiProvider(); - - const storageGasOracleConfig = coreEnvConfig.storageGasOracleConfig; - if (!storageGasOracleConfig) { - throw Error(`No storage gas oracle config for environment ${environment}`); - } - - const core = HyperlaneCore.fromEnvironment( - deployEnvToSdkEnv[environment], - multiProvider, - ); - - for (const chain of core.chains()) { - await compare(tokenPriceGetter, storageGasOracleConfig[chain], chain); - console.log('\n==========='); - } -} - -async function compare( - tokenPriceGetter: CoinGeckoTokenPriceGetter, - localStorageGasOracleConfig: StorageGasOracleConfig, - local: ChainName, -) { - for (const remoteStr of Object.keys(localStorageGasOracleConfig)) { - const remote = remoteStr as ChainName; - const configGasData = localStorageGasOracleConfig[remote]!; - const currentTokenExchangeRateNum = - await tokenPriceGetter.getTokenExchangeRate(remote, local); - const currentTokenExchangeRate = ethers.utils.parseUnits( - currentTokenExchangeRateNum.toFixed(TOKEN_EXCHANGE_RATE_DECIMALS), - TOKEN_EXCHANGE_RATE_DECIMALS, - ); - - const diff = configGasData.tokenExchangeRate.sub(currentTokenExchangeRate); - const percentDiff = diff - .mul(TOKEN_EXCHANGE_RATE_SCALE) - .div(currentTokenExchangeRate) - .mul(100); - - console.log(`${local} -> ${remote}`); - console.log( - `\tConfig token exchange rate:\n\t\t${prettyTokenExchangeRate( - configGasData.tokenExchangeRate, - )}`, - ); - console.log( - `\tCurrent token exchange rate:\n\t\t${prettyTokenExchangeRate( - currentTokenExchangeRate, - )}`, - ); - console.log( - `Config tokenExchangeRate is ${ethers.utils.formatUnits( - percentDiff, - TOKEN_EXCHANGE_RATE_DECIMALS, - )}% different from the current value`, - ); - console.log('------'); - } -} - -main().catch((err: any) => console.error('Error:', err)); diff --git a/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts b/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts deleted file mode 100644 index 3d36b1aabd..0000000000 --- a/typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - ChainName, - HyperlaneIgp, - MultiProvider, - prettyRemoteGasData, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; - -import { RemoteGasData, StorageGasOracleConfig } from '../../src/config'; -import { deployEnvToSdkEnv } from '../../src/config/environment'; -import { RemoteGasDataConfig } from '../../src/config/gas-oracle'; -import { getArgs, withNetwork } from '../agent-utils'; -import { getEnvironmentConfig } from '../core-utils'; - -import { eqRemoteGasData, prettyRemoteGasDataConfig } from './utils'; - -/** - * Idempotent. Use `--dry-run` to not send any transactions. - * Updates the currently stored gas data on the StorageGasOracle contract - * if the configured data differs from the on-chain data. - * Expects the deployer key to be the owner of the StorageGasOracle contract. - */ -async function main() { - const args = await withNetwork(getArgs()) - .boolean('dry-run') - .describe('dry-run', 'If true, will not submit any transactions') - .default('dry-run', false).argv; - - const { environment, network } = args; - const coreEnvConfig = getEnvironmentConfig(environment); - const multiProvider = await coreEnvConfig.getMultiProvider(); - - const storageGasOracleConfig = coreEnvConfig.storageGasOracleConfig; - if (!storageGasOracleConfig) { - throw Error(`No storage gas oracle config for environment ${environment}`); - } - - const igp = HyperlaneIgp.fromEnvironment( - deployEnvToSdkEnv[environment], - multiProvider, - ); - - const targetChains = network ? [network] : igp.chains(); - - for (const chain of targetChains) { - if ( - multiProvider.getChainMetadata(chain).protocol !== ProtocolType.Ethereum - ) { - console.log(`Skipping ${chain} because it is not an Ethereum chain`); - continue; - } - - await setStorageGasOracleValues( - igp, - multiProvider, - storageGasOracleConfig[chain], - chain, - args.dryRun, - ); - console.log('\n==========='); - } -} - -async function setStorageGasOracleValues( - igp: HyperlaneIgp, - // This multiProvider is used instead of the one on the IGP because the IGP's - // multiprovider will have filtered out non-Ethereum chains. - multiProvider: MultiProvider, - localStorageGasOracleConfig: StorageGasOracleConfig, - local: ChainName, - dryRun: boolean, -) { - console.log(`Setting remote gas data on local chain ${local}...`); - const storageGasOracle = igp.getContracts(local).storageGasOracle; - - const configsToSet: RemoteGasDataConfig[] = []; - - for (const remote of Object.keys(localStorageGasOracleConfig)) { - const desiredGasData = localStorageGasOracleConfig[remote]!; - const remoteId = multiProvider.getDomainId(remote); - - const existingGasData: RemoteGasData = await storageGasOracle.remoteGasData( - remoteId, - ); - - console.log( - `${local} -> ${remote} existing gas data:\n`, - prettyRemoteGasData(existingGasData), - ); - console.log( - `${local} -> ${remote} desired gas data:\n`, - prettyRemoteGasData(desiredGasData), - ); - - if (eqRemoteGasData(existingGasData, desiredGasData)) { - console.log('Existing and desired gas data are the same, doing nothing'); - } else { - console.log('Existing and desired gas data differ, will update'); - configsToSet.push({ - remoteDomain: remoteId, - ...desiredGasData, - }); - } - console.log('---'); - } - - if (configsToSet.length > 0) { - console.log(`Updating ${configsToSet.length} configs on local ${local}:`); - console.log( - configsToSet - .map((config) => prettyRemoteGasDataConfig(multiProvider, config)) - .join('\n\t--\n'), - ); - - if (dryRun) { - console.log('Running in dry run mode, not sending tx'); - } else { - await igp.multiProvider.sendTransaction( - local, - await storageGasOracle.populateTransaction.setRemoteGasDataConfigs( - configsToSet, - ), - ); - } - } -} - -main().catch((err) => console.error('Error', err)); diff --git a/typescript/infra/scripts/gas-oracle/utils.ts b/typescript/infra/scripts/gas-oracle/utils.ts deleted file mode 100644 index 1c85a968a9..0000000000 --- a/typescript/infra/scripts/gas-oracle/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MultiProvider, prettyRemoteGasData } from '@hyperlane-xyz/sdk'; - -import { RemoteGasData } from '../../src/config'; -import { RemoteGasDataConfig } from '../../src/config/gas-oracle'; - -export function prettyRemoteGasDataConfig( - multiProvider: MultiProvider, - config: RemoteGasDataConfig, -) { - return `\tRemote: ${config.remoteDomain} (${multiProvider.getChainName( - config.remoteDomain, - )})\n${prettyRemoteGasData(config)}`; -} - -export function eqRemoteGasData(a: RemoteGasData, b: RemoteGasData): boolean { - return ( - a.tokenExchangeRate.eq(b.tokenExchangeRate) && a.gasPrice.eq(b.gasPrice) - ); -} diff --git a/typescript/infra/src/agents/key-utils.ts b/typescript/infra/src/agents/key-utils.ts index 47c647bd4b..65245f6b72 100644 --- a/typescript/infra/src/agents/key-utils.ts +++ b/typescript/infra/src/agents/key-utils.ts @@ -18,7 +18,12 @@ import { RootAgentConfig, } from '../config'; import { Role } from '../roles'; -import { execCmd, isEthereumProtocolChain } from '../utils/utils'; +import { + execCmd, + isEthereumProtocolChain, + readJSON, + writeJSON, +} from '../utils/utils'; import { AgentAwsKey } from './aws/key'; import { AgentGCPKey } from './gcp'; @@ -42,6 +47,8 @@ export interface KeyAsAddress { address: string; } +const CONFIG_DIRECTORY_PATH = path.join(__dirname, '../../config'); + // ================== // Functions for getting keys // ================== @@ -438,26 +445,22 @@ export async function persistValidatorAddressesToLocalArtifacts( validators: fetchedValidatorAddresses[chain].validators, // fresh from aws }; } - // Resolve the relative path - const filePath = path.resolve(__dirname, '../../config/aw-multisig.json'); // Write the updated object back to the file - fs.writeFileSync(filePath, JSON.stringify(awMultisigAddresses, null, 2)); + writeJSON(CONFIG_DIRECTORY_PATH, 'aw-multisig.json', awMultisigAddresses); } -export function fetchLocalKeyAddresses( - role: Role, - environment: DeployEnvironment, - context: Contexts, -): Address { - // Resolve the relative path - const filePath = path.resolve(__dirname, `../../config/${role}.json`); - const data = fs.readFileSync(filePath, 'utf8'); - const addresses: LocalRoleAddresses = JSON.parse(data); +export function fetchLocalKeyAddresses(role: Role): LocalRoleAddresses { + try { + const addresses: LocalRoleAddresses = readJSON( + CONFIG_DIRECTORY_PATH, + `${role}.json`, + ); - debugLog( - `Fetching addresses from GCP for ${context} context in ${environment} environment`, - ); - return addresses[environment][context]; + debugLog(`Fetching addresses from GCP for ${role} role ...`); + return addresses; + } catch (e) { + throw new Error(`Error fetching addresses locally for ${role} role: ${e}`); + } } function addressesIdentifier( diff --git a/typescript/infra/src/agents/keys.ts b/typescript/infra/src/agents/keys.ts index 98e4df12e9..a6d817d891 100644 --- a/typescript/infra/src/agents/keys.ts +++ b/typescript/infra/src/agents/keys.ts @@ -73,6 +73,18 @@ export abstract class CloudAgentKey extends BaseCloudAgentKey { } } +export class LocalAgentKey extends BaseAgentKey { + constructor( + public readonly environment: DeployEnvironment, + public readonly context: Contexts, + public readonly role: Role, + public readonly address: string, + public readonly chainName?: ChainName, + ) { + super(environment, role, chainName); + } +} + // A read-only representation of a key managed internally. export class ReadOnlyCloudAgentKey extends BaseCloudAgentKey { constructor( diff --git a/typescript/infra/src/config/environment.ts b/typescript/infra/src/config/environment.ts index d4f301bbd8..3163178592 100644 --- a/typescript/infra/src/config/environment.ts +++ b/typescript/infra/src/config/environment.ts @@ -18,7 +18,6 @@ import { Role } from '../roles'; import { RootAgentConfig } from './agent'; import { KeyFunderConfig } from './funding'; -import { AllStorageGasOracleConfigs } from './gas-oracle'; import { HelloWorldConfig } from './helloworld/types'; import { InfrastructureConfig } from './infrastructure'; import { LiquidityLayerRelayerConfig } from './middleware'; @@ -55,7 +54,6 @@ export type EnvironmentConfig = { bridgeAdapters: ChainMap; relayer: LiquidityLayerRelayerConfig; }; - storageGasOracleConfig?: AllStorageGasOracleConfigs; }; export const deployEnvToSdkEnv: Record< diff --git a/typescript/infra/src/config/funding.ts b/typescript/infra/src/config/funding.ts index 3ad6f48aa0..89d35f54a6 100644 --- a/typescript/infra/src/config/funding.ts +++ b/typescript/infra/src/config/funding.ts @@ -1,7 +1,7 @@ import { RpcConsensusType } from '@hyperlane-xyz/sdk'; import { Contexts } from '../../config/contexts'; -import { Role } from '../roles'; +import { FundableRole, Role } from '../roles'; import { DockerConfig } from './agent'; @@ -10,7 +10,7 @@ export interface ContextAndRoles { roles: Role[]; } -export type ContextAndRolesMap = Partial>; +export type ContextAndRolesMap = Partial>; export interface KeyFunderConfig { docker: DockerConfig; diff --git a/typescript/infra/src/config/gas-oracle.ts b/typescript/infra/src/config/gas-oracle.ts index 812314220a..a7e91e44be 100644 --- a/typescript/infra/src/config/gas-oracle.ts +++ b/typescript/infra/src/config/gas-oracle.ts @@ -1,22 +1,17 @@ import { BigNumber, ethers } from 'ethers'; -import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; +import { + ChainMap, + ChainName, + StorageGasOracleConfig as DestinationOracleConfig, +} from '@hyperlane-xyz/sdk'; import { convertDecimals } from '@hyperlane-xyz/utils'; import { mustGetChainNativeTokenDecimals } from '../utils/utils'; -export type RemoteGasData = { - tokenExchangeRate: BigNumber; - gasPrice: BigNumber; -}; - -export type RemoteGasDataConfig = RemoteGasData & { - remoteDomain: number; -}; - -// Gas data to configure on a single local chain. Includes RemoteGasData +// Gas data to configure on a single local chain. Includes DestinationOracleConfig // for each remote chain. -export type StorageGasOracleConfig = ChainMap; +export type StorageGasOracleConfig = ChainMap; // StorageGasOracleConfigs for each local chain export type AllStorageGasOracleConfigs = ChainMap; @@ -27,11 +22,8 @@ export const TOKEN_EXCHANGE_RATE_SCALE = ethers.utils.parseUnits( TOKEN_EXCHANGE_RATE_DECIMALS, ); -// Overcharge by 30% to account for market making risk -const TOKEN_EXCHANGE_RATE_MULTIPLIER = ethers.utils.parseUnits( - '1.30', - TOKEN_EXCHANGE_RATE_DECIMALS, -); +// Overcharge by 20% to account for market making risk (when assets are unequal) +const EXCHANGE_RATE_MARGIN_PCT = 20; // Gets the StorageGasOracleConfig for a particular local chain function getLocalStorageGasOracleConfig( @@ -79,9 +71,11 @@ export function getTokenExchangeRateFromValues( remoteValue: BigNumber, ): BigNumber { // This does not yet account for decimals! - const exchangeRate = remoteValue - .mul(TOKEN_EXCHANGE_RATE_MULTIPLIER) - .div(localValue); + let exchangeRate = remoteValue.mul(TOKEN_EXCHANGE_RATE_SCALE).div(localValue); + // use margin if exchange rate is not 1 + if (!exchangeRate.eq(TOKEN_EXCHANGE_RATE_SCALE)) { + exchangeRate = exchangeRate.mul(100 + EXCHANGE_RATE_MARGIN_PCT).div(100); + } return BigNumber.from( convertDecimals( diff --git a/typescript/infra/src/config/index.ts b/typescript/infra/src/config/index.ts index f98fa4b3be..97a3b0c81a 100644 --- a/typescript/infra/src/config/index.ts +++ b/typescript/infra/src/config/index.ts @@ -1,10 +1,9 @@ -export { EnvironmentConfig, DeployEnvironment } from './environment'; +export * from './agent'; +export { DeployEnvironment, EnvironmentConfig } from './environment'; export { AllStorageGasOracleConfigs, - RemoteGasData, StorageGasOracleConfig, getAllStorageGasOracleConfigs, } from './gas-oracle'; export { HelloWorldConfig } from './helloworld/types'; export { InfrastructureConfig } from './infrastructure'; -export * from './agent'; diff --git a/typescript/infra/src/roles.ts b/typescript/infra/src/roles.ts index f9b1f466e5..4953118f37 100644 --- a/typescript/infra/src/roles.ts +++ b/typescript/infra/src/roles.ts @@ -6,6 +6,8 @@ export enum Role { Kathy = 'kathy', } +export type FundableRole = Role.Relayer | Role.Kathy; + export const ALL_KEY_ROLES = [ Role.Validator, Role.Relayer, diff --git a/typescript/infra/src/utils/utils.ts b/typescript/infra/src/utils/utils.ts index 0b6d8a410e..43f59118c7 100644 --- a/typescript/infra/src/utils/utils.ts +++ b/typescript/infra/src/utils/utils.ts @@ -15,7 +15,7 @@ import { import { ProtocolType, objMerge } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; -import { Role } from '../roles'; +import { FundableRole, Role } from '../roles'; export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -213,6 +213,14 @@ export function assertRole(roleStr: string) { return role; } +export function assertFundableRole(roleStr: string): FundableRole { + const role = roleStr as Role; + if (role !== Role.Relayer && role !== Role.Kathy) { + throw Error(`Invalid fundable role ${role}`); + } + return role; +} + export function assertChain(chainStr: string) { const chain = chainStr as ChainName; if (!AllChains.includes(chain as CoreChainName)) { diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts index 8cea8c47d3..3ebc53ca82 100644 --- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts +++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts @@ -217,23 +217,25 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< mailbox.address, ); - let proxyOwner: string; if (config.upgrade) { const timelockController = await this.deployTimelock( chain, config.upgrade.timelock, ); - proxyOwner = timelockController.address; - } else { - proxyOwner = config.owner; + config.ownerOverrides = { + ...config.ownerOverrides, + proxyAdmin: timelockController.address, + }; } - await this.transferOwnershipOfContracts(chain, proxyOwner, { proxyAdmin }); - - return { + const contracts = { mailbox, proxyAdmin, validatorAnnounce, }; + + await this.transferOwnershipOfContracts(chain, config, contracts); + + return contracts; } } diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index a9632fa684..379bc2e93b 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -42,6 +42,7 @@ import { proxyConstructorArgs, proxyImplementation, } from './proxy'; +import { OwnableConfig } from './types'; import { ContractVerificationInput } from './verify/types'; import { buildVerificationInput, @@ -618,16 +619,21 @@ export abstract class HyperlaneDeployer< return ret; } - protected async transferOwnershipOfContracts( + protected async transferOwnershipOfContracts( chain: ChainName, - owner: Address, - ownables: { [key: string]: Ownable }, + config: OwnableConfig, + ownables: Partial>, ): Promise { const receipts: ethers.ContractReceipt[] = []; - for (const contractName of Object.keys(ownables)) { - const ownable = ownables[contractName]; - const currentOwner = await ownable.owner(); - if (!eqAddress(currentOwner, owner)) { + for (const [contractName, ownable] of Object.entries( + ownables, + )) { + if (!ownable) { + continue; + } + const current = await ownable.owner(); + const owner = config.ownerOverrides?.[contractName as K] ?? config.owner; + if (!eqAddress(current, owner)) { this.logger( `Transferring ownership of ${contractName} to ${owner} on ${chain}`, ); diff --git a/typescript/sdk/src/deploy/types.ts b/typescript/sdk/src/deploy/types.ts index 8ce65d8358..3d2389858f 100644 --- a/typescript/sdk/src/deploy/types.ts +++ b/typescript/sdk/src/deploy/types.ts @@ -9,7 +9,7 @@ import { Address } from '@hyperlane-xyz/utils'; import type { ChainName } from '../types'; -export type OwnableConfig = { +export type OwnableConfig = { owner: Address; ownerOverrides?: Partial>; }; diff --git a/typescript/sdk/src/gas/HyperlaneIgpChecker.ts b/typescript/sdk/src/gas/HyperlaneIgpChecker.ts index b6d1301f6f..3ecc11a698 100644 --- a/typescript/sdk/src/gas/HyperlaneIgpChecker.ts +++ b/typescript/sdk/src/gas/HyperlaneIgpChecker.ts @@ -1,6 +1,6 @@ -import { BigNumber, ethers } from 'ethers'; +import { BigNumber } from 'ethers'; -import { Address, eqAddress } from '@hyperlane-xyz/utils'; +import { eqAddress } from '@hyperlane-xyz/utils'; import { BytecodeHash } from '../consts/bytecode'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker'; @@ -9,7 +9,6 @@ import { ChainName } from '../types'; import { HyperlaneIgp } from './HyperlaneIgp'; import { - GasOracleContractType, IgpBeneficiaryViolation, IgpConfig, IgpGasOraclesViolation, @@ -31,12 +30,7 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< async checkDomainOwnership(chain: ChainName): Promise { const config = this.configMap[chain]; - - const ownableOverrides: Record = { - ...config.ownerOverrides, - storageGasOracle: config.oracleKey, - }; - await super.checkOwnership(chain, config.owner, ownableOverrides); + await super.checkOwnership(chain, config.owner, config.ownerOverrides); } async checkBytecodes(chain: ChainName): Promise { @@ -130,17 +124,14 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< expected: {}, }; - // In addition to all remote chains on the app, which are just Ethereum chains, - // also consider what the config says about non-Ethereum chains. - const remotes = new Set([ - ...this.app.remoteChains(local), - ...Object.keys(this.configMap[local].gasOracleType), - ]); + const remotes = new Set( + Object.keys(this.configMap[local].oracleConfig ?? {}), + ); for (const remote of remotes) { const remoteId = this.multiProvider.getDomainId(remote); const destinationGasConfigs = await igp.destinationGasConfigs(remoteId); const actualGasOracle = destinationGasConfigs.gasOracle; - const expectedGasOracle = this.getGasOracleAddress(local, remote); + const expectedGasOracle = coreContracts.storageGasOracle.address; if (!eqAddress(actualGasOracle, expectedGasOracle)) { const remoteChain = remote as ChainName; @@ -167,22 +158,4 @@ export class HyperlaneIgpChecker extends HyperlaneAppChecker< this.addViolation(violation); } } - - getGasOracleAddress(local: ChainName, remote: ChainName): Address { - const config = this.configMap[local]; - const gasOracleType = config.gasOracleType[remote]; - if (!gasOracleType) { - this.app.logger( - `No gas oracle for local ${local} and remote ${remote}, defaulting to zero address`, - ); - return ethers.constants.AddressZero; - } - const coreContracts = this.app.getContracts(local); - switch (gasOracleType) { - case GasOracleContractType.StorageGasOracle: - return coreContracts.storageGasOracle.address; - default: - throw Error(`Unsupported gas oracle type ${gasOracleType}`); - } - } } diff --git a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts index bd03a43ec6..fd82c90464 100644 --- a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts +++ b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts @@ -1,26 +1,23 @@ import debug from 'debug'; -import { ethers } from 'ethers'; import { InterchainGasPaymaster, ProxyAdmin, StorageGasOracle, - StorageGasOracle__factory, } from '@hyperlane-xyz/core'; -import { Address, eqAddress, warn } from '@hyperlane-xyz/utils'; +import { eqAddress } from '@hyperlane-xyz/utils'; import { HyperlaneContracts } from '../contracts/types'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; import { MultiProvider } from '../providers/MultiProvider'; -import { ChainMap, ChainName } from '../types'; +import { ChainName } from '../types'; import { IgpFactories, igpFactories } from './contracts'; -import { prettyRemoteGasData } from './oracle/logging'; -import { OracleConfig, StorageGasOracleConfig } from './oracle/types'; +import { serializeDifference } from './oracle/types'; import { IgpConfig } from './types'; export class HyperlaneIgpDeployer extends HyperlaneDeployer< - IgpConfig & Partial, + IgpConfig, IgpFactories > { constructor(multiProvider: MultiProvider) { @@ -45,10 +42,8 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer< ); const gasParamsToSet: InterchainGasPaymaster.GasParamStruct[] = []; - const remotes = Object.keys(config.gasOracleType); - for (const remote of remotes) { + for (const [remote, newGasOverhead] of Object.entries(config.overhead)) { const remoteId = this.multiProvider.getDomainId(remote); - const newGasOverhead = config.overhead[remote]; const currentGasConfig = await igp.destinationGasConfigs(remoteId); if ( @@ -83,97 +78,62 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer< return igp; } - async deployStorageGasOracle(chain: ChainName): Promise { - return this.deployContract(chain, 'storageGasOracle', []); - } - - async configureStorageGasOracle( + async deployStorageGasOracle( chain: ChainName, - igp: InterchainGasPaymaster, - gasOracleConfig: ChainMap, - ): Promise { - this.logger(`Configuring gas oracles for ${chain}...`); - const remotes = Object.keys(gasOracleConfig); - const configsToSet: Record< - Address, - StorageGasOracle.RemoteGasDataConfigStruct[] - > = {}; + config: IgpConfig, + ): Promise { + const gasOracle = await this.deployContract(chain, 'storageGasOracle', []); - // For each remote, check if the gas oracle has the correct data - for (const remote of remotes) { - const desiredGasData = gasOracleConfig[remote]; - const remoteId = this.multiProvider.getDomainId(remote); - // each destination can have a different gas oracle - const gasOracleAddress = (await igp.destinationGasConfigs(remoteId)) - .gasOracle; + if (!config.oracleConfig) { + this.logger('No oracle config provided, skipping...'); + return gasOracle; + } - if (eqAddress(gasOracleAddress, ethers.constants.AddressZero)) { - warn(`No gas oracle set for ${chain} -> ${remote}, cannot configure`); - continue; - } - const gasOracle = StorageGasOracle__factory.connect( - gasOracleAddress, - this.multiProvider.getSigner(chain), - ); - configsToSet[gasOracleAddress] ||= []; + this.logger(`Configuring gas oracle from ${chain}...`); + const configsToSet: Array = []; + + // For each remote, check if the gas oracle has the correct data + for (const [remote, desired] of Object.entries(config.oracleConfig)) { + const remoteDomain = this.multiProvider.getDomainId(remote); - this.logger(`Checking gas oracle ${gasOracleAddress} for ${remote}...`); - const remoteGasDataConfig = await gasOracle.remoteGasData(remoteId); + const actual = await gasOracle.remoteGasData(remoteDomain); if ( - !remoteGasDataConfig.gasPrice.eq(desiredGasData.gasPrice) || - !remoteGasDataConfig.tokenExchangeRate.eq( - desiredGasData.tokenExchangeRate, - ) + !actual.gasPrice.eq(desired.gasPrice) || + !actual.tokenExchangeRate.eq(desired.tokenExchangeRate) ) { - this.logger( - `${chain} -> ${remote} existing gas data:\n`, - prettyRemoteGasData(remoteGasDataConfig), - ); - this.logger( - `${chain} -> ${remote} desired gas data:\n`, - prettyRemoteGasData(desiredGasData), - ); - configsToSet[gasOracleAddress].push({ - remoteDomain: this.multiProvider.getDomainId(remote), - ...desiredGasData, + this.logger(`-> ${remote} ${serializeDifference(actual, desired)}`); + configsToSet.push({ + remoteDomain, + ...desired, }); } } - // loop through each gas oracle and batch set the remote gas data - for (const gasOracle of Object.keys(configsToSet)) { - const gasOracleContract = StorageGasOracle__factory.connect( - gasOracle, - this.multiProvider.getSigner(chain), + + if (configsToSet.length > 0) { + await this.runIfOwner(chain, gasOracle, async () => + this.multiProvider.handleTx( + chain, + gasOracle.setRemoteGasDataConfigs( + configsToSet, + this.multiProvider.getTransactionOverrides(chain), + ), + ), ); - if (configsToSet[gasOracle].length > 0) { - await this.runIfOwner(chain, gasOracleContract, async () => { - this.logger( - `Setting gas oracle on ${gasOracle} for ${configsToSet[ - gasOracle - ].map((config) => config.remoteDomain)}`, - ); - return this.multiProvider.handleTx( - chain, - gasOracleContract.setRemoteGasDataConfigs( - configsToSet[gasOracle], - this.multiProvider.getTransactionOverrides(chain), - ), - ); - }); - } } + + return gasOracle; } async deployContracts( chain: ChainName, - config: IgpConfig & Partial, + config: IgpConfig, ): Promise> { // 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 storageGasOracle = await this.deployStorageGasOracle(chain, config); const interchainGasPaymaster = await this.deployInterchainGasPaymaster( chain, proxyAdmin, @@ -181,29 +141,22 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer< config, ); - // Configure storage gas oracle with remote gas data if provided - if (config.oracleConfig) { - await this.configureStorageGasOracle( - chain, - interchainGasPaymaster, - config.oracleConfig, - ); - } - - await this.transferOwnershipOfContracts(chain, config.owner, { - interchainGasPaymaster, - }); - - // Configure oracle key for StorageGasOracle separately to keep 'hot' - // for updating exchange rates regularly - await this.transferOwnershipOfContracts(chain, config.oracleKey, { - storageGasOracle, - }); - - return { + const contracts = { proxyAdmin, storageGasOracle, interchainGasPaymaster, }; + + const ownerConfig = { + ...config, + ownerOverrides: { + ...config.ownerOverrides, + storageGasOracle: config.oracleKey, + }, + }; + + await this.transferOwnershipOfContracts(chain, ownerConfig, contracts); + + return contracts; } } diff --git a/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts b/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts index e3adc16cfe..3cf0dcc1e0 100644 --- a/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts +++ b/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts @@ -9,8 +9,6 @@ import { ChainMap } from '../../types'; import { HyperlaneIgpDeployer } from '../HyperlaneIgpDeployer'; import { IgpConfig } from '../types'; -import { OracleConfig } from './types'; - describe('HyperlaneIgpDeployer', () => { const local = 'test1'; const remote = 'test2'; @@ -18,7 +16,7 @@ describe('HyperlaneIgpDeployer', () => { let deployer: HyperlaneIgpDeployer; let igp: InterchainGasPaymaster; let multiProvider: MultiProvider; - let testConfig: ChainMap>; + let testConfig: ChainMap; before(async () => { const [signer] = await ethers.getSigners(); @@ -26,57 +24,35 @@ describe('HyperlaneIgpDeployer', () => { remoteId = multiProvider.getDomainId(remote); deployer = new HyperlaneIgpDeployer(multiProvider); testConfig = testIgpConfig([local, remote], signer.address); + const contracts = await deployer.deploy(testConfig); + igp = contracts[local].interchainGasPaymaster; }); it('should deploy storage gas oracle with config given', async () => { - // Act - igp = (await deployer.deploy(testConfig))[local].interchainGasPaymaster; // Assert const deployedConfig = await igp.getExchangeRateAndGasPrice(remoteId); - if (testConfig[local].oracleConfig) { - expect(deployedConfig.tokenExchangeRate).to.equal( - testConfig[local].oracleConfig[remote].tokenExchangeRate, - ); - expect(deployedConfig.gasPrice).to.equal( - testConfig[local].oracleConfig[remote].gasPrice, - ); - } + expect({ + gasPrice: deployedConfig.gasPrice, + tokenExchangeRate: deployedConfig.tokenExchangeRate, + }).to.deep.equal(testConfig[local].oracleConfig![remote]); }); it('should configure new oracle config', async () => { - // Assert - const deployedConfig = await igp.getExchangeRateAndGasPrice(remoteId); - if (testConfig[local].oracleConfig) { - expect(deployedConfig.tokenExchangeRate).to.equal( - testConfig[local].oracleConfig[remote].tokenExchangeRate, - ); - expect(deployedConfig.gasPrice).to.equal( - testConfig[local].oracleConfig[remote].gasPrice, - ); - - // Arrange - testConfig[local].oracleConfig[remote].tokenExchangeRate = - ethers.utils.parseUnits('2', 'gwei'); - testConfig[local].oracleConfig[remote].gasPrice = ethers.utils.parseUnits( - '3', - 'gwei', - ); - - // Act - await deployer.configureStorageGasOracle( - local, - igp, - testConfig[local].oracleConfig, - ); - - // Assert - const modifiedConfig = await igp.getExchangeRateAndGasPrice(remoteId); - expect(modifiedConfig.tokenExchangeRate).to.equal( - testConfig[local].oracleConfig[remote].tokenExchangeRate, - ); - expect(modifiedConfig.gasPrice).to.equal( - testConfig[local].oracleConfig[remote].gasPrice, - ); - } + testConfig[local].oracleConfig![remote] = { + tokenExchangeRate: ethers.utils.parseUnits('2', 'gwei'), + gasPrice: ethers.utils.parseUnits('3', 'gwei'), + }; + + const localContracts = await deployer.deployContracts( + local, + testConfig[local], + ); + igp = localContracts.interchainGasPaymaster; + + const modifiedConfig = await igp.getExchangeRateAndGasPrice(remoteId); + expect({ + gasPrice: modifiedConfig.gasPrice, + tokenExchangeRate: modifiedConfig.tokenExchangeRate, + }).to.deep.equal(testConfig[local].oracleConfig![remote]); }); }); diff --git a/typescript/sdk/src/gas/oracle/logging.ts b/typescript/sdk/src/gas/oracle/logging.ts deleted file mode 100644 index 69274e84e7..0000000000 --- a/typescript/sdk/src/gas/oracle/logging.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BigNumber, ethers } from 'ethers'; - -import { TOKEN_EXCHANGE_RATE_EXPONENT } from '../../consts/igp'; -import { ChainName } from '../../types'; - -import { StorageGasOracleConfig } from './types'; - -export function prettyRemoteGasDataConfig( - chain: ChainName, - config: StorageGasOracleConfig, -): string { - return `\tRemote: (${chain})\n${prettyRemoteGasData(config)}`; -} - -export function prettyRemoteGasData(data: StorageGasOracleConfig): string { - return `\tToken exchange rate: ${prettyTokenExchangeRate( - data.tokenExchangeRate, - )}\n\tGas price: ${data.gasPrice.toString()}`; -} - -export function prettyTokenExchangeRate(tokenExchangeRate: BigNumber): string { - return `${tokenExchangeRate.toString()} (${ethers.utils.formatUnits( - tokenExchangeRate, - TOKEN_EXCHANGE_RATE_EXPONENT, - )})`; -} diff --git a/typescript/sdk/src/gas/oracle/types.ts b/typescript/sdk/src/gas/oracle/types.ts index 2185f5cd97..a3cd3b9d1c 100644 --- a/typescript/sdk/src/gas/oracle/types.ts +++ b/typescript/sdk/src/gas/oracle/types.ts @@ -1,18 +1,55 @@ -import { BigNumber } from 'ethers'; +import { ethers } from 'ethers'; -import { ChainMap } from '../../types'; +import { StorageGasOracle } from '@hyperlane-xyz/core'; + +import { TOKEN_EXCHANGE_RATE_EXPONENT } from '../../consts/igp'; export enum GasOracleContractType { StorageGasOracle = 'StorageGasOracle', } + // Gas data to configure on a single destination chain. -export type StorageGasOracleConfig = { - tokenExchangeRate: BigNumber; - gasPrice: BigNumber; +export type StorageGasOracleConfig = Pick< + StorageGasOracle.RemoteGasDataConfigStructOutput, + 'gasPrice' | 'tokenExchangeRate' +>; + +export const formatGasOracleConfig = (config: StorageGasOracleConfig) => ({ + tokenExchangeRate: ethers.utils.formatUnits( + config.tokenExchangeRate, + TOKEN_EXCHANGE_RATE_EXPONENT, + ), + gasPrice: ethers.utils.formatUnits(config.gasPrice, 'gwei'), +}); + +const percentDifference = ( + actual: ethers.BigNumber, + expected: ethers.BigNumber, +): ethers.BigNumber => expected.sub(actual).mul(100).div(actual); + +const serializePercentDifference = ( + actual: ethers.BigNumber, + expected: ethers.BigNumber, +): string => { + if (actual.isZero()) { + return 'new'; + } + const diff = percentDifference(actual, expected); + return diff.isNegative() ? `${diff.toString()}%` : `+${diff.toString()}%`; }; -// StorageGasOracleConfig for each local chain -export type StorageGasOraclesConfig = ChainMap; -export type OracleConfig = { - oracleConfig: StorageGasOraclesConfig; +export const serializeDifference = ( + actual: StorageGasOracleConfig, + expected: StorageGasOracleConfig, +): string => { + const gasPriceDiff = serializePercentDifference( + actual.gasPrice, + expected.gasPrice, + ); + const tokenExchangeRateDiff = serializePercentDifference( + actual.tokenExchangeRate, + expected.tokenExchangeRate, + ); + const formatted = formatGasOracleConfig(expected); + return `$ ${formatted.tokenExchangeRate} (${tokenExchangeRateDiff}), ${formatted.gasPrice} gwei (${gasPriceDiff})`; }; diff --git a/typescript/sdk/src/gas/types.ts b/typescript/sdk/src/gas/types.ts index c4f76c05aa..38c6c49707 100644 --- a/typescript/sdk/src/gas/types.ts +++ b/typescript/sdk/src/gas/types.ts @@ -7,16 +7,16 @@ import type { CheckerViolation, OwnableConfig } from '../deploy/types'; import { ChainMap } from '../types'; import { IgpFactories } from './contracts'; - -export enum GasOracleContractType { - StorageGasOracle = 'StorageGasOracle', -} +import { GasOracleContractType, StorageGasOracleConfig } from './oracle/types'; export type IgpConfig = OwnableConfig & { beneficiary: Address; - gasOracleType: ChainMap; oracleKey: Address; overhead: ChainMap; + // TODO: require this + oracleConfig?: ChainMap; + // DEPRECATED + gasOracleType?: ChainMap; }; export enum IgpViolationType { diff --git a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts index bfc4b74828..8a92ae66bf 100644 --- a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts +++ b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts @@ -87,7 +87,11 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< hook = await this.deployRouting(chain, config, coreAddresses); } else if (config.type === HookType.PAUSABLE) { hook = await this.deployContract(chain, config.type, []); - await this.transferOwnershipOfContracts(chain, config.owner, { hook }); + await this.transferOwnershipOfContracts( + chain, + config, + { [HookType.PAUSABLE]: hook }, + ); } else { throw new Error(`Unsupported hook config: ${config}`); } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 0130351de1..e9db2dac12 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -106,12 +106,11 @@ export { } from './gas/adapters/serialization'; export { IgpFactories, igpFactories } from './gas/contracts'; export { - prettyRemoteGasData, - prettyTokenExchangeRate, -} from './gas/oracle/logging'; + GasOracleContractType, + StorageGasOracleConfig, +} from './gas/oracle/types'; export { CoinGeckoTokenPriceGetter } from './gas/token-prices'; export { - GasOracleContractType, IgpBeneficiaryViolation, IgpConfig, IgpGasOraclesViolation, diff --git a/typescript/sdk/src/router/HyperlaneRouterDeployer.ts b/typescript/sdk/src/router/HyperlaneRouterDeployer.ts index e9d501c43c..1878ec2665 100644 --- a/typescript/sdk/src/router/HyperlaneRouterDeployer.ts +++ b/typescript/sdk/src/router/HyperlaneRouterDeployer.ts @@ -1,4 +1,4 @@ -import { Router } from '@hyperlane-xyz/core'; +import { Ownable, Router } from '@hyperlane-xyz/core'; import { Address, addressToBytes32, @@ -102,9 +102,14 @@ export abstract class HyperlaneRouterDeployer< this.logger(`Transferring ownership of ownables...`); for (const chain of Object.keys(contractsMap)) { const contracts = contractsMap[chain]; - const owner = configMap[chain].owner; - const ownables = await filterOwnableContracts(contracts); - await this.transferOwnershipOfContracts(chain, owner, ownables); + const ownables = (await filterOwnableContracts(contracts)) as Partial< + Record + >; + await this.transferOwnershipOfContracts( + chain, + configMap[chain], + ownables, + ); } } diff --git a/typescript/sdk/src/test/testUtils.ts b/typescript/sdk/src/test/testUtils.ts index fa62ea7834..020017adf4 100644 --- a/typescript/sdk/src/test/testUtils.ts +++ b/typescript/sdk/src/test/testUtils.ts @@ -7,14 +7,13 @@ import { HyperlaneContractsMap } from '../contracts/types'; import { CoreFactories } from '../core/contracts'; import { CoreConfig } from '../core/types'; import { IgpFactories } from '../gas/contracts'; -import { OracleConfig, StorageGasOraclesConfig } from '../gas/oracle/types'; import { CoinGeckoInterface, CoinGeckoResponse, CoinGeckoSimpleInterface, CoinGeckoSimplePriceParams, } from '../gas/token-prices'; -import { GasOracleContractType, IgpConfig } from '../gas/types'; +import { IgpConfig } from '../gas/types'; import { HookType } from '../hook/types'; import { IsmType } from '../ism/types'; import { RouterConfig } from '../router/types'; @@ -70,51 +69,29 @@ export function testCoreConfig( return Object.fromEntries(chains.map((local) => [local, chainConfig])); } -function testOracleConfigs( - chains: ChainName[], -): ChainMap { - return Object.fromEntries( - chains.map((local) => [ - local, - Object.fromEntries( - exclude(local, chains).map((remote) => [ - remote, - { - gasPrice: ethers.utils.parseUnits('1', 'gwei'), - tokenExchangeRate: ethers.utils.parseUnits('1', 10), - }, - ]), - ), - ]), - ); -} - -function getGasOracleTypes(chains: ChainName[], local: ChainName) { - return Object.fromEntries( - exclude(local, chains).map((remote) => [ - remote, - GasOracleContractType.StorageGasOracle, - ]), - ); -} +const TEST_ORACLE_CONFIG = { + gasPrice: ethers.utils.parseUnits('1', 'gwei'), + tokenExchangeRate: ethers.utils.parseUnits('1', 10), +}; export function testIgpConfig( chains: ChainName[], owner = nonZeroAddress, -): ChainMap> { - const oracleConfig = testOracleConfigs(chains); +): ChainMap { return Object.fromEntries( chains.map((local) => [ local, { owner, - beneficiary: owner, oracleKey: owner, - gasOracleType: getGasOracleTypes(chains, local), + beneficiary: owner, + // TODO: these should be one map overhead: Object.fromEntries( exclude(local, chains).map((remote) => [remote, 60000]), ), - oracleConfig: oracleConfig[local], + oracleConfig: Object.fromEntries( + exclude(local, chains).map((remote) => [remote, TEST_ORACLE_CONFIG]), + ), }, ]), );