Bridge to L2 in key funder (#1386)

pull/1504/head
Asa Oines 2 years ago committed by GitHub
parent e97cac2325
commit 3e85da4af1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      typescript/infra/package.json
  2. 306
      typescript/infra/scripts/funding/fund-keys-from-deployer.ts
  3. 6
      typescript/sdk/src/core/TestCoreDeployer.ts
  4. 765
      yarn.lock

@ -3,12 +3,17 @@
"description": "Infrastructure utilities for the Hyperlane Network",
"version": "1.0.0-beta2",
"dependencies": {
"@arbitrum/sdk": "^3.0.0",
"@aws-sdk/client-iam": "^3.74.0",
"@aws-sdk/client-kms": "3.48.0",
"@aws-sdk/client-s3": "^3.74.0",
"@eth-optimism/sdk": "^1.7.0",
"@ethersproject/experimental": "^5.7.0",
"@ethersproject/hardware-wallets": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@gnosis.pm/safe-core-sdk": "^2.3.2",
"@gnosis.pm/safe-ethers-lib": "^1.4.0",
"@gnosis.pm/safe-service-client": "^1.2.0",
"@hyperlane-xyz/celo-ethers-provider": "^0.1.1",
"@hyperlane-xyz/helloworld": "1.0.0-beta3",
"@hyperlane-xyz/sdk": "1.0.0-beta3",

@ -1,4 +1,5 @@
import { ethers } from 'ethers';
import { EthBridger, getL2Network } from '@arbitrum/sdk';
import { BigNumber, ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client';
import { format } from 'util';
@ -6,9 +7,12 @@ import {
AllChains,
ChainConnection,
ChainName,
ChainNameToDomainId,
Chains,
CompleteChainMap,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { ChainMap } from '@hyperlane-xyz/sdk/dist/types';
import { error, log } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts';
@ -34,6 +38,29 @@ import {
getCoreEnvironmentConfig,
} from '../utils';
type L2Chain =
| Chains.optimism
| Chains.optimismgoerli
| Chains.arbitrum
| Chains.arbitrumgoerli;
const L2Chains: ChainName[] = [
Chains.optimism,
Chains.optimismgoerli,
Chains.arbitrum,
Chains.arbitrumgoerli,
];
const L2ToL1: ChainMap<L2Chain, ChainName> = {
optimismgoerli: 'goerli',
arbitrumgoerli: 'goerli',
optimism: 'ethereum',
arbitrum: 'ethereum',
};
// Missing types declaration for bufio
const CrossChainMessenger = require('@eth-optimism/sdk').CrossChainMessenger; // eslint-disable-line
const constMetricLabels = {
// this needs to get set in main because of async reasons
hyperlane_deployment: '',
@ -192,7 +219,7 @@ class ContextFunder {
keys.map((key) => key.chainName!).filter((chain) => chain !== undefined),
);
this.chains = Array.from(uniqueChains);
this.chains = Array.from(uniqueChains) as ChainName[];
}
static fromSerializedAddressFile(
@ -268,54 +295,38 @@ class ContextFunder {
async fund(): Promise<boolean> {
let failureOccurred = false;
for (const role of this.rolesToFund) {
const failure =
role === KEY_ROLE_ENUM.Relayer
? await this.fundRelayersOnAllRequiredChains()
: await this.fundNonRelayerKeysOnAllChains(role);
if (failure) {
failureOccurred = true;
}
}
return failureOccurred;
}
// Returns whether a failure occurred.
private async fundNonRelayerKeysOnAllChains(
roleToFund: KEY_ROLE_ENUM,
): Promise<boolean> {
let failureOccurred = false;
const keys = this.getKeysWithRole(roleToFund);
for (const chain of this.chains) {
for (const key of keys) {
const failure = await this.attemptToFundKey(key, chain);
if (failure) {
failureOccurred = true;
const chainKeys = this.getChainKeys();
await Promise.all(
Object.entries(chainKeys).map(async ([chain, keys]) => {
if (keys.length > 0) {
await this.bridgeIfL2(chain as ChainName);
}
}
}
keys.forEach(async (key) => {
const failure = await this.attemptToFundKey(key, chain as ChainName);
failureOccurred = failureOccurred || failure;
});
}),
);
return failureOccurred;
}
// Funds the relayers on all the chains found in `this.chains`.
// Does not fund a relayer key on its outbox chain.
// Returns whether a failure occurred.
private async fundRelayersOnAllRequiredChains(): Promise<boolean> {
let failureOccurred = false;
const keys = this.getKeysWithRole(KEY_ROLE_ENUM.Relayer);
for (const chain of this.chains) {
for (const key of keys.filter((k) => k.chainName !== chain)) {
const failure = await this.attemptToFundKey(key, chain);
if (failure) {
failureOccurred = true;
}
private getChainKeys() {
const entries = AllChains.map((c) => {
return [c, []];
});
const chainKeys: CompleteChainMap<BaseCloudAgentKey[]> =
Object.fromEntries(entries);
for (const role of this.rolesToFund) {
const keys = this.getKeysWithRole(role);
for (const chain of this.chains) {
// Relayer keys should not be funded on the origin chain.
const filteredKeys = keys.filter(
(key) => role !== KEY_ROLE_ENUM.Relayer || key.chainName !== chain,
);
chainKeys[chain] = filteredKeys;
}
}
return failureOccurred;
return chainKeys;
}
private async attemptToFundKey(
@ -338,7 +349,7 @@ class ContextFunder {
await this.fundKeyIfRequired(chainConnection, chain, key, desiredBalance);
} catch (err) {
error('Error funding key', {
key: getKeyInfo(key),
key: await getKeyInfo(key, chain, chainConnection),
context: this.context,
error: err,
});
@ -349,6 +360,43 @@ class ContextFunder {
return failureOccurred;
}
private async bridgeIfL2(chain: ChainName) {
if (L2Chains.includes(chain)) {
const chainConnection = this.multiProvider.tryGetChainConnection(chain)!;
const funderAddress = await chainConnection.getAddress()!;
const desiredBalanceEther = ethers.utils.parseUnits(
desiredBalancePerChain[chain],
'ether',
);
// Optionally bridge ETH to L2 before funding the desired key.
// By bridging the funder with 10x the desired balance we save
// on L1 gas.
const bridgeAmount = await this.getFundingAmount(
chainConnection,
chain,
funderAddress,
desiredBalanceEther.mul(10),
);
if (bridgeAmount.gt(0)) {
await this.bridgeToL2(chain as L2Chain, funderAddress, bridgeAmount);
}
}
}
private async getFundingAmount(
chainConnection: ChainConnection,
chain: ChainName,
address: string,
desiredBalance: BigNumber,
): Promise<BigNumber> {
const currentBalance = await chainConnection.provider.getBalance(address);
const delta = desiredBalance.sub(currentBalance);
const minDelta = desiredBalance
.mul(MIN_DELTA_NUMERATOR)
.div(MIN_DELTA_DENOMINATOR);
return delta.gt(minDelta) ? delta : BigNumber.from(0);
}
// Tops up the key's balance to the desired balance if the current balance
// is lower than the desired balance by the min delta
private async fundKeyIfRequired(
@ -357,64 +405,124 @@ class ContextFunder {
key: BaseCloudAgentKey,
desiredBalance: string,
) {
const currentBalance = await chainConnection.provider.getBalance(
key.address,
);
const desiredBalanceEther = ethers.utils.parseUnits(
desiredBalance,
'ether',
);
const delta = desiredBalanceEther.sub(currentBalance);
const minDelta = desiredBalanceEther
.mul(MIN_DELTA_NUMERATOR)
.div(MIN_DELTA_DENOMINATOR);
const keyInfo = getKeyInfo(key);
log('Assessing key for funding', {
key: keyInfo,
keyBalanceDelta: ethers.utils.formatEther(delta),
minKeyBalanceDelta: ethers.utils.formatEther(minDelta),
currentKeyBalance: ethers.utils.formatEther(currentBalance),
desiredKeyBalance: desiredBalance,
funder: {
address: await chainConnection.getAddress(),
balance: ethers.utils.formatEther(
await chainConnection.signer!.getBalance(),
),
},
context: this.context,
const fundingAmount = await this.getFundingAmount(
chainConnection,
chain,
});
key.address,
desiredBalanceEther,
);
const keyInfo = await getKeyInfo(key, chain, chainConnection);
const funderAddress = await chainConnection.getAddress()!;
if (delta.gt(minDelta)) {
log('Sending funds...', {
if (fundingAmount.eq(0)) {
log('Skipping funding for key', {
key: keyInfo,
amount: ethers.utils.formatEther(delta),
context: this.context,
chain,
});
const tx = await chainConnection.signer!.sendTransaction({
to: key.address,
value: delta,
...chainConnection.overrides,
});
log('Sent transaction', {
key: keyInfo,
txUrl: chainConnection.getTxUrl(tx),
context: this.context,
return;
} else {
log('Funding key', {
chain,
});
const receipt = await tx.wait(chainConnection.confirmations);
log('Got transaction receipt', {
amount: ethers.utils.formatEther(fundingAmount),
key: keyInfo,
receipt,
funder: {
address: funderAddress,
balance: ethers.utils.formatEther(
await chainConnection.signer!.getBalance(),
),
},
context: this.context,
chain,
});
}
const tx = await chainConnection.signer!.sendTransaction({
to: key.address,
value: fundingAmount,
...chainConnection.overrides,
});
log('Sent transaction', {
key: keyInfo,
txUrl: chainConnection.getTxUrl(tx),
context: this.context,
chain,
});
const receipt = await tx.wait(chainConnection.confirmations);
log('Got transaction receipt', {
key: keyInfo,
receipt,
context: this.context,
chain,
});
}
private async bridgeToL2(l2Chain: L2Chain, to: string, amount: BigNumber) {
const l1Chain = L2ToL1[l2Chain];
const l1ChainConnection = await this.multiProvider.tryGetChainConnection(
l1Chain,
)!;
const l2ChainConnection = await this.multiProvider.tryGetChainConnection(
l2Chain,
)!;
log('Bridging ETH to L2', {
amount: ethers.utils.formatEther(amount),
l1Funder: await getAddressInfo(
await l1ChainConnection.getAddress()!,
l1Chain,
l1ChainConnection,
),
l2Funder: await getAddressInfo(to, l2Chain, l2ChainConnection),
});
let tx;
if (l2Chain.includes('optimism')) {
tx = await this.bridgeToOptimism(l2Chain, amount, to);
} else if (l2Chain.includes('arbitrum')) {
tx = await this.bridgeToArbitrum(l2Chain, amount);
} else {
throw new Error(`${l2Chain} is not an L2`);
}
await this.multiProvider
.tryGetChainConnection(L2ToL1[l2Chain])!
.handleTx(tx);
}
private async bridgeToOptimism(
l2Chain: L2Chain,
amount: BigNumber,
to: string,
) {
const l1Chain = L2ToL1[l2Chain];
const l1ChainConnection =
this.multiProvider.tryGetChainConnection(l1Chain)!;
const l2ChainConnection =
this.multiProvider.tryGetChainConnection(l2Chain)!;
const crossChainMessenger = new CrossChainMessenger({
l1ChainId: ChainNameToDomainId[l1Chain],
l2ChainId: ChainNameToDomainId[l2Chain],
l1SignerOrProvider: l1ChainConnection.signer!,
l2SignerOrProvider: l2ChainConnection.provider,
});
return crossChainMessenger.depositETH(amount, {
recipient: to,
overrides: l1ChainConnection.overrides,
});
}
private async bridgeToArbitrum(l2Chain: L2Chain, amount: BigNumber) {
const l1Chain = L2ToL1[l2Chain];
const l1ChainConnection =
this.multiProvider.tryGetChainConnection(l1Chain)!;
const l2Network = await getL2Network(ChainNameToDomainId[l2Chain]);
const ethBridger = new EthBridger(l2Network);
return ethBridger.deposit({
amount,
l1Signer: l1ChainConnection.signer!,
overrides: l1ChainConnection.overrides,
});
}
private async updateWalletBalanceGauge(
@ -443,10 +551,28 @@ class ContextFunder {
}
}
function getKeyInfo(key: BaseCloudAgentKey) {
async function getAddressInfo(
address: string,
chain: ChainName,
chainConnection: ChainConnection,
) {
return {
chain,
balance: ethers.utils.formatEther(
await chainConnection.provider.getBalance(address),
),
address,
};
}
async function getKeyInfo(
key: BaseCloudAgentKey,
chain: ChainName,
chainConnection: ChainConnection,
) {
return {
...(await getAddressInfo(key.address, chain, chainConnection)),
context: key.context,
address: key.address,
originChain: key.chainName,
role: key.role,
};

@ -2,7 +2,6 @@ import { ethers } from 'ethers';
import {
MultisigIsm,
Ownable,
TestIsm__factory,
TestMailbox__factory,
} from '@hyperlane-xyz/core';
@ -66,10 +65,7 @@ export class TestCoreDeployer<
}
// TestIsm is not ownable, so we skip ownership transfer
async transferOwnershipOfContracts(
chain: TestChain,
ownables: Ownable[],
): Promise<ethers.ContractReceipt[]> {
async transferOwnershipOfContracts(): Promise<ethers.ContractReceipt[]> {
return [];
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save