Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inventory-manager): Wrap ETH on Arbitrum #969

Merged
merged 10 commits into from
Oct 6, 2023
2 changes: 1 addition & 1 deletion src/clients/InventoryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ export class InventoryClient {
return;
}
this.log("Checking ETH->WETH Wrap status");
await this.adapterManager.wrapEthIfAboveThreshold(this.inventoryConfig.wrapEtherThreshold, this.simMode);
await this.adapterManager.wrapEthIfAboveThreshold(this.inventoryConfig, this.simMode);
}

async update(): Promise<void> {
Expand Down
24 changes: 16 additions & 8 deletions src/clients/bridges/AdapterManager.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse } from "../../utils";
import { BigNumber, isDefined, winston, Signer, getL2TokenAddresses, TransactionResponse, assert } from "../../utils";
import { SpokePoolClient, HubPoolClient } from "../";
import { OptimismAdapter, ArbitrumAdapter, PolygonAdapter, BaseAdapter, ZKSyncAdapter } from "./";
import { OutstandingTransfers } from "../../interfaces";
import { InventoryConfig, OutstandingTransfers } from "../../interfaces";
import { utils } from "@across-protocol/sdk-v2";
import { CHAIN_IDs } from "@across-protocol/constants-v2";
import { BaseChainAdapter } from "./op-stack/base/BaseChainAdapter";
import { spokesThatHoldEthAndWeth } from "../../common/Constants";
export class AdapterManager {
public adapters: { [chainId: number]: BaseAdapter } = {};

// Some L2's canonical bridges send ETH, not WETH, over the canonical bridges, resulting in recipient addresses
// receiving ETH that needs to be wrapped on the L2. This array contains the chainIds of the chains that this
// manager will attempt to wrap ETH on into WETH. This is not necessary for chains that receive WETH, the ERC20,
// over the bridge.
public chainsToWrapEtherOn = spokesThatHoldEthAndWeth;
// manager will attempt to wrap ETH on into WETH. This list also includes chains like Arbitrum where the relayer is
// expected to receive ETH as a gas refund from an L1 to L2 deposit that was intended to rebalance inventory.
public chainsToWrapEtherOn = [...spokesThatHoldEthAndWeth, CHAIN_IDs.ARBITRUM];

constructor(
readonly logger: winston.Logger,
Expand Down Expand Up @@ -77,12 +78,19 @@ export class AdapterManager {

// Check how much ETH is on the target chain and if it is above the threshold the wrap it to WETH. Note that this only
// needs to be done on chains where rebalancing WETH from L1 to L2 results in the relayer receiving ETH
// (not the ERC20).
async wrapEthIfAboveThreshold(wrapThreshold: BigNumber, simMode = false): Promise<void> {
// (not the ERC20), or if the relayer expects to be sent ETH perhaps as a gas refund from an original L1 to L2
// deposit.
async wrapEthIfAboveThreshold(inventoryConfig: InventoryConfig, simMode = false): Promise<void> {
await utils.mapAsync(
this.chainsToWrapEtherOn.filter((chainId) => isDefined(this.spokePoolClients[chainId])),
async (chainId) => {
await this.adapters[chainId].wrapEthIfAboveThreshold(wrapThreshold, simMode);
const wrapThreshold = inventoryConfig.wrapEtherThresholdPerChain[chainId] ?? inventoryConfig.wrapEtherThreshold;
const wrapTarget = inventoryConfig.wrapEtherTargetPerChain[chainId] ?? inventoryConfig.wrapEtherTarget;
assert(
wrapThreshold.gte(wrapTarget),
`wrapEtherThreshold ${wrapThreshold.toString()} must be >= wrapEtherTarget ${wrapTarget.toString()}`
);
await this.adapters[chainId].wrapEthIfAboveThreshold(wrapThreshold, wrapTarget, simMode);
}
);
}
Expand Down
24 changes: 22 additions & 2 deletions src/clients/bridges/ArbitrumAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
toWei,
paginatedEventQuery,
Event,
assert,
} from "../../utils";
import { SpokePoolClient } from "../../clients";
import { BaseAdapter } from "./BaseAdapter";
Expand Down Expand Up @@ -194,8 +195,27 @@ export class ArbitrumAdapter extends BaseAdapter {
);
}

async wrapEthIfAboveThreshold(): Promise<TransactionResponse | null> {
throw new Error("Unnecessary to wrap ETH on Arbitrum");
// The arbitrum relayer expects to receive ETH steadily per HubPool bundle processed, since it is the L2 refund
// address hardcoded in the Arbitrum Adapter.
async wrapEthIfAboveThreshold(
threshold: BigNumber,
target: BigNumber,
simMode = false
): Promise<TransactionResponse | null> {
const { chainId } = this;
assert(42161 === chainId, `chainId ${chainId} is not supported`);

const weth = CONTRACT_ADDRESSES[this.chainId].weth;
const ethBalance = await this.getSigner(chainId).getBalance();

if (ethBalance.gt(threshold)) {
const l2Signer = this.getSigner(chainId);
const contract = new Contract(weth.address, weth.abi, l2Signer);
const value = ethBalance.sub(target);
this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, target, value, ethBalance });
return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode);
}
return null;
}

getL1Bridge(l1Token: SupportedL1Token): Contract {
Expand Down
15 changes: 11 additions & 4 deletions src/clients/bridges/BaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,13 @@ export abstract class BaseAdapter {
): Promise<TransactionResponse> {
const { chainId, txnClient } = this;
const method = "deposit";
const formatFunc = createFormatFunction(2, 4, false, 18);
const mrkdwn =
`Ether on chain ${this.chainId} was wrapped due to being over the threshold of ` +
`${createFormatFunction(2, 4, false, 18)(toBN(wrapThreshold).toString())} ETH.`;
const message = `Eth wrapped on target chain ${this.chainId}🎁`;
`${formatFunc(
toBN(value).toString()
)} Ether on chain ${chainId} was wrapped due to being over the threshold of ` +
`${formatFunc(toBN(wrapThreshold).toString())} ETH.`;
const message = `${formatFunc(toBN(value).toString())} Eth wrapped on target chain ${chainId}🎁`;
if (simMode) {
const { succeed, reason } = (
await txnClient.simulate([{ contract: l2WEthContract, chainId, method, args: [], value, mrkdwn, message }])
Expand Down Expand Up @@ -362,5 +365,9 @@ export abstract class BaseAdapter {

abstract checkTokenApprovals(address: string, l1Tokens: string[]): Promise<void>;

abstract wrapEthIfAboveThreshold(threshold: BigNumber, simMode: boolean): Promise<TransactionResponse | null>;
abstract wrapEthIfAboveThreshold(
threshold: BigNumber,
target: BigNumber,
simMode: boolean
): Promise<TransactionResponse | null>;
}
10 changes: 7 additions & 3 deletions src/clients/bridges/ZKSyncAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@ export class ZKSyncAdapter extends BaseAdapter {
* @param threshold
* @returns
*/
async wrapEthIfAboveThreshold(threshold: BigNumber, simMode = false): Promise<TransactionResponse | null> {
async wrapEthIfAboveThreshold(
threshold: BigNumber,
target: BigNumber,
simMode = false
): Promise<TransactionResponse | null> {
const { chainId } = this;
assert(chainId === 324, `chainId ${chainId} is not supported`);

Expand All @@ -239,8 +243,8 @@ export class ZKSyncAdapter extends BaseAdapter {
const l2Signer = this.getSigner(chainId);
// @dev Can re-use ABI from L1 weth as its the same for the purposes of this function.
const contract = new Contract(l2WethAddress, CONTRACT_ADDRESSES[this.hubChainId].weth.abi, l2Signer);
const value = ethBalance.sub(threshold);
this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, value, ethBalance });
const value = ethBalance.sub(target);
this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, target, value, ethBalance });
return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode);
}
return null;
Expand Down
12 changes: 8 additions & 4 deletions src/clients/bridges/op-stack/OpStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,21 @@ export class OpStackAdapter extends BaseAdapter {
);
}

async wrapEthIfAboveThreshold(threshold: BigNumber, simMode = false): Promise<TransactionResponse | null> {
async wrapEthIfAboveThreshold(
threshold: BigNumber,
target: BigNumber,
simMode = false
): Promise<TransactionResponse | null> {
const { chainId } = this;
assert(chainId === this.chainId, `chainId ${chainId} is not supported`);
assert([10, 8453].includes(chainId), `chainId ${chainId} is not supported`);

const ovmWeth = CONTRACT_ADDRESSES[this.chainId].weth;
const ethBalance = await this.getSigner(chainId).getBalance();
if (ethBalance.gt(threshold)) {
const l2Signer = this.getSigner(chainId);
const contract = new Contract(ovmWeth.address, ovmWeth.abi, l2Signer);
const value = ethBalance.sub(threshold);
this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, value, ethBalance });
const value = ethBalance.sub(target);
this.logger.debug({ at: this.getName(), message: "Wrapping ETH", threshold, target, value, ethBalance });
return await this._wrapEthIfAboveThreshold(threshold, contract, value, simMode);
}
return null;
Expand Down
14 changes: 14 additions & 0 deletions src/common/ContractAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,20 @@ export const CONTRACT_ADDRESSES: {
},
],
},
weth: {
address: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
abi: [
{
constant: false,
inputs: [],
name: "deposit",
outputs: [],
payable: true,
stateMutability: "payable",
type: "function",
},
],
},
outbox: {
address: "0x0B9857ae2D4A3DBe74ffE1d7DF045bb7F96E4840",
abi: [
Expand Down
10 changes: 9 additions & 1 deletion src/interfaces/InventoryManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,13 @@ export interface InventoryConfig {
};
};
};
wrapEtherThreshold: BigNumber; // Number of Ether, that if the balance is above, wrap it to WETH on the L2. in wei
// If ETH balance on chain is above threshold, wrap the excess over the target to WETH.
wrapEtherTargetPerChain: {
[chainId: number]: BigNumber;
};
wrapEtherTarget: BigNumber;
wrapEtherThresholdPerChain: {
[chainId: number]: BigNumber;
};
wrapEtherThreshold: BigNumber;
}
32 changes: 32 additions & 0 deletions src/relayer/RelayerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,39 @@ export class RelayerConfig extends CommonConfig {
this.inventoryConfig.wrapEtherThreshold = this.inventoryConfig.wrapEtherThreshold
? toBNWei(this.inventoryConfig.wrapEtherThreshold)
: toBNWei(1); // default to keeping 2 Eth on the target chains and wrapping the rest to WETH.
this.inventoryConfig.wrapEtherThresholdPerChain ??= {};
this.inventoryConfig.wrapEtherTarget = this.inventoryConfig.wrapEtherTarget
? toBNWei(this.inventoryConfig.wrapEtherTarget)
: this.inventoryConfig.wrapEtherThreshold; // default to wrapping ETH to threshold, same as target.
this.inventoryConfig.wrapEtherTargetPerChain ??= {};
assert(
this.inventoryConfig.wrapEtherThreshold.gte(this.inventoryConfig.wrapEtherTarget),
`default wrapEtherThreshold ${this.inventoryConfig.wrapEtherThreshold} must be >= default wrapEtherTarget ${this.inventoryConfig.wrapEtherTarget}}`
);

// Validate the per chain target and thresholds for wrapping ETH:
Object.keys(this.inventoryConfig.wrapEtherThresholdPerChain).forEach((chainId) => {
if (this.inventoryConfig.wrapEtherThresholdPerChain[chainId] !== undefined) {
this.inventoryConfig.wrapEtherThresholdPerChain[chainId] = toBNWei(
this.inventoryConfig.wrapEtherThresholdPerChain[chainId]
);
}
});
Object.keys(this.inventoryConfig.wrapEtherTargetPerChain).forEach((chainId) => {
if (this.inventoryConfig.wrapEtherTargetPerChain[chainId] !== undefined) {
this.inventoryConfig.wrapEtherTargetPerChain[chainId] = toBNWei(
this.inventoryConfig.wrapEtherTargetPerChain[chainId]
);
// Check newly set target against threshold
const threshold =
this.inventoryConfig.wrapEtherThresholdPerChain[chainId] ?? this.inventoryConfig.wrapEtherThreshold;
const target = this.inventoryConfig.wrapEtherTargetPerChain[chainId];
assert(
threshold.gte(target),
`wrapEtherThresholdPerChain ${threshold.toString()} must be >= wrapEtherTargetPerChain ${target}`
);
}
});
Object.keys(this.inventoryConfig.tokenConfig).forEach((l1Token) => {
Object.keys(this.inventoryConfig.tokenConfig[l1Token]).forEach((chainId) => {
const { targetPct, thresholdPct, unwrapWethThreshold, unwrapWethTarget } =
Expand Down