feat: configure GasOracle in HyperlaneIgpDeployer (#3218)

### Description

HyperlaneIGPDeployer supports configuring storageGasOracle if provided.
HyperlaneIgpDeployer takes in `IgpConfig & Partial<OracleConfig>`

Note: storageGasOracle cannot be configured in CLI yet.
Note: this is a more minimal version of
https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3101 where more
breaking changes were made.


### Drive-by changes

None

### Related issues

- fixes https://github.com/hyperlane-xyz/issues/issues/820

### Backward compatibility

Yes

### Testing

Unit
pull/3241/head
Kunal Arora 10 months ago committed by GitHub
parent 58b4aaa559
commit ab17af5f7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/few-cups-deny.md
  2. 5
      typescript/infra/scripts/deploy.ts
  3. 3
      typescript/infra/scripts/gas-oracle/compare-token-exchange-rates.ts
  4. 13
      typescript/infra/scripts/gas-oracle/update-storage-gas-oracle.ts
  5. 20
      typescript/infra/scripts/gas-oracle/utils.ts
  6. 1
      typescript/sdk/src/consts/igp.ts
  7. 108
      typescript/sdk/src/gas/HyperlaneIgpDeployer.ts
  8. 82
      typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts
  9. 26
      typescript/sdk/src/gas/oracle/logging.ts
  10. 18
      typescript/sdk/src/gas/oracle/types.ts
  11. 4
      typescript/sdk/src/index.ts
  12. 54
      typescript/sdk/src/test/testUtils.ts

@ -0,0 +1,7 @@
---
'@hyperlane-xyz/infra': patch
'@hyperlane-xyz/cli': patch
'@hyperlane-xyz/sdk': patch
---
Updating HyperlaneIgpDeployer to configure storage gas oracles as part of deployment

@ -73,7 +73,10 @@ 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;
config = {
...envConfig.igp,
oracleConfig: envConfig.storageGasOracleConfig,
};
deployer = new HyperlaneIgpDeployer(multiProvider);
} else if (module === Modules.INTERCHAIN_ACCOUNTS) {
const core = HyperlaneCore.fromEnvironment(env, multiProvider);

@ -4,6 +4,7 @@ import {
ChainName,
CoinGeckoTokenPriceGetter,
HyperlaneCore,
prettyTokenExchangeRate,
} from '@hyperlane-xyz/sdk';
import { StorageGasOracleConfig } from '../../src/config';
@ -15,8 +16,6 @@ import {
import { getArgs } from '../agent-utils';
import { getEnvironmentConfig } from '../core-utils';
import { prettyTokenExchangeRate } from './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.

@ -1,4 +1,9 @@
import { ChainName, HyperlaneIgp, MultiProvider } from '@hyperlane-xyz/sdk';
import {
ChainName,
HyperlaneIgp,
MultiProvider,
prettyRemoteGasData,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { RemoteGasData, StorageGasOracleConfig } from '../../src/config';
@ -7,11 +12,7 @@ import { RemoteGasDataConfig } from '../../src/config/gas-oracle';
import { getArgs, withNetwork } from '../agent-utils';
import { getEnvironmentConfig } from '../core-utils';
import {
eqRemoteGasData,
prettyRemoteGasData,
prettyRemoteGasDataConfig,
} from './utils';
import { eqRemoteGasData, prettyRemoteGasDataConfig } from './utils';
/**
* Idempotent. Use `--dry-run` to not send any transactions.

@ -1,6 +1,4 @@
import { BigNumber, ethers } from 'ethers';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { MultiProvider, prettyRemoteGasData } from '@hyperlane-xyz/sdk';
import { RemoteGasData } from '../../src/config';
import { RemoteGasDataConfig } from '../../src/config/gas-oracle';
@ -14,22 +12,6 @@ export function prettyRemoteGasDataConfig(
)})\n${prettyRemoteGasData(config)}`;
}
export function prettyRemoteGasData(data: RemoteGasData) {
return `\tToken exchange rate: ${prettyTokenExchangeRate(
data.tokenExchangeRate,
)}\n\tGas price: ${data.gasPrice.toString()} (${ethers.utils.formatUnits(
data.gasPrice,
'gwei',
)} gwei)`;
}
export function prettyTokenExchangeRate(tokenExchangeRate: BigNumber) {
return `${tokenExchangeRate.toString()} (${ethers.utils.formatUnits(
tokenExchangeRate,
10,
)})`;
}
export function eqRemoteGasData(a: RemoteGasData, b: RemoteGasData): boolean {
return (
a.tokenExchangeRate.eq(b.tokenExchangeRate) && a.gasPrice.eq(b.gasPrice)

@ -0,0 +1 @@
export const TOKEN_EXCHANGE_RATE_EXPONENT = 10;

@ -1,22 +1,26 @@
import debug from 'debug';
import { ethers } from 'ethers';
import {
InterchainGasPaymaster,
ProxyAdmin,
StorageGasOracle,
StorageGasOracle__factory,
} from '@hyperlane-xyz/core';
import { eqAddress } from '@hyperlane-xyz/utils';
import { Address, eqAddress, warn } from '@hyperlane-xyz/utils';
import { HyperlaneContracts } from '../contracts/types';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer';
import { MultiProvider } from '../providers/MultiProvider';
import { ChainName } from '../types';
import { ChainMap, ChainName } from '../types';
import { IgpFactories, igpFactories } from './contracts';
import { prettyRemoteGasData } from './oracle/logging';
import { OracleConfig, StorageGasOracleConfig } from './oracle/types';
import { IgpConfig } from './types';
export class HyperlaneIgpDeployer extends HyperlaneDeployer<
IgpConfig,
IgpConfig & Partial<OracleConfig>,
IgpFactories
> {
constructor(multiProvider: MultiProvider) {
@ -31,14 +35,13 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
storageGasOracle: StorageGasOracle,
config: IgpConfig,
): Promise<InterchainGasPaymaster> {
const owner = config.owner;
const beneficiary = config.beneficiary;
const igp = await this.deployProxiedContract(
chain,
'interchainGasPaymaster',
proxyAdmin.address,
[],
[owner, beneficiary],
[await this.multiProvider.getSignerAddress(chain), beneficiary],
);
const gasParamsToSet: InterchainGasPaymaster.GasParamStruct[] = [];
@ -52,7 +55,9 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
!eqAddress(currentGasConfig.gasOracle, storageGasOracle.address) ||
!currentGasConfig.gasOverhead.eq(newGasOverhead)
) {
this.logger(`Setting gas params for ${remote} to ${newGasOverhead}`);
this.logger(
`Setting gas params for ${chain} -> ${remote}: gasOverhead = ${newGasOverhead} gasOracle = ${storageGasOracle.address}`,
);
gasParamsToSet.push({
remoteDomain: remoteId,
config: {
@ -74,6 +79,7 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
),
);
}
return igp;
}
@ -81,9 +87,87 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
return this.deployContract(chain, 'storageGasOracle', []);
}
async configureStorageGasOracle(
chain: ChainName,
igp: InterchainGasPaymaster,
gasOracleConfig: ChainMap<StorageGasOracleConfig>,
): Promise<void> {
this.logger(`Configuring gas oracles for ${chain}...`);
const remotes = Object.keys(gasOracleConfig);
const configsToSet: Record<
Address,
StorageGasOracle.RemoteGasDataConfigStruct[]
> = {};
// 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 (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(`Checking gas oracle ${gasOracleAddress} for ${remote}...`);
const remoteGasDataConfig = await gasOracle.remoteGasData(remoteId);
if (
!remoteGasDataConfig.gasPrice.eq(desiredGasData.gasPrice) ||
!remoteGasDataConfig.tokenExchangeRate.eq(
desiredGasData.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,
});
}
}
// 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[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),
),
);
});
}
}
}
async deployContracts(
chain: ChainName,
config: IgpConfig,
config: IgpConfig & Partial<OracleConfig>,
): Promise<HyperlaneContracts<IgpFactories>> {
// NB: To share ProxyAdmins with HyperlaneCore, ensure the ProxyAdmin
// is loaded into the contract cache.
@ -96,6 +180,16 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
storageGasOracle,
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,
});

@ -0,0 +1,82 @@
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { InterchainGasPaymaster } from '@hyperlane-xyz/core';
import { MultiProvider } from '../../providers/MultiProvider';
import { testIgpConfig } from '../../test/testUtils';
import { ChainMap } from '../../types';
import { HyperlaneIgpDeployer } from '../HyperlaneIgpDeployer';
import { IgpConfig } from '../types';
import { OracleConfig } from './types';
describe('HyperlaneIgpDeployer', () => {
const local = 'test1';
const remote = 'test2';
let remoteId: number;
let deployer: HyperlaneIgpDeployer;
let igp: InterchainGasPaymaster;
let multiProvider: MultiProvider;
let testConfig: ChainMap<IgpConfig & Partial<OracleConfig>>;
before(async () => {
const [signer] = await ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
remoteId = multiProvider.getDomainId(remote);
deployer = new HyperlaneIgpDeployer(multiProvider);
testConfig = testIgpConfig([local, remote], signer.address);
});
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,
);
}
});
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,
);
}
});
});

@ -0,0 +1,26 @@
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,
)})`;
}

@ -0,0 +1,18 @@
import { BigNumber } from 'ethers';
import { ChainMap } from '../../types';
export enum GasOracleContractType {
StorageGasOracle = 'StorageGasOracle',
}
// Gas data to configure on a single destination chain.
export type StorageGasOracleConfig = {
tokenExchangeRate: BigNumber;
gasPrice: BigNumber;
};
// StorageGasOracleConfig for each local chain
export type StorageGasOraclesConfig = ChainMap<StorageGasOracleConfig>;
export type OracleConfig = {
oracleConfig: StorageGasOraclesConfig;
};

@ -105,6 +105,10 @@ export {
SealevelOverheadIgpDataSchema,
} from './gas/adapters/serialization';
export { IgpFactories, igpFactories } from './gas/contracts';
export {
prettyRemoteGasData,
prettyTokenExchangeRate,
} from './gas/oracle/logging';
export { CoinGeckoTokenPriceGetter } from './gas/token-prices';
export {
GasOracleContractType,

@ -1,18 +1,20 @@
import { BigNumber, ethers } from 'ethers';
import { Address, objMap } from '@hyperlane-xyz/utils';
import { Address, exclude, objMap } from '@hyperlane-xyz/utils';
import { chainMetadata } from '../consts/chainMetadata';
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 { HookType } from '../hook/types';
import { IsmType } from '../ism/types';
import { RouterConfig } from '../router/types';
@ -68,6 +70,56 @@ export function testCoreConfig(
return Object.fromEntries(chains.map((local) => [local, chainConfig]));
}
function testOracleConfigs(
chains: ChainName[],
): ChainMap<StorageGasOraclesConfig> {
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,
]),
);
}
export function testIgpConfig(
chains: ChainName[],
owner = nonZeroAddress,
): ChainMap<IgpConfig & Partial<OracleConfig>> {
const oracleConfig = testOracleConfigs(chains);
return Object.fromEntries(
chains.map((local) => [
local,
{
owner,
beneficiary: owner,
oracleKey: owner,
gasOracleType: getGasOracleTypes(chains, local),
overhead: Object.fromEntries(
exclude(local, chains).map((remote) => [remote, 60000]),
),
oracleConfig: oracleConfig[local],
},
]),
);
}
// A mock CoinGecko intended to be used by tests
export class MockCoinGecko implements CoinGeckoInterface {
// Prices keyed by coingecko id

Loading…
Cancel
Save