Skip to content

Commit

Permalink
Merge branch 'master' into pxrl/cleanTest
Browse files Browse the repository at this point in the history
  • Loading branch information
pxrl authored Oct 9, 2023
2 parents 105b216 + 640ef8c commit 7cfa842
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 61 deletions.
78 changes: 45 additions & 33 deletions src/clients/AcrossAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { winston, BigNumber, getL2TokenAddresses } from "../utils";
import { isDefined, winston, BigNumber, getL2TokenAddresses } from "../utils";
import axios, { AxiosError } from "axios";
import { HubPoolClient } from "./HubPoolClient";
import { utils as sdkUtils } from "@across-protocol/sdk-v2";
import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants-v2";
import { SpokePoolClientsByChain } from "../interfaces";
import _ from "lodash";

const { bnZero } = sdkUtils;

export interface DepositLimits {
maxDeposit: BigNumber;
}
Expand Down Expand Up @@ -58,36 +61,42 @@ export class AcrossApiClient {
if (!mainnetSpokePoolClient.isUpdated) {
throw new Error("Mainnet SpokePoolClient for chainId must be updated before AcrossAPIClient");
}

const data = await Promise.all(
tokensQuery.map((l1Token) => {
const l2TokenAddresses = getL2TokenAddresses(l1Token);
const validDestinationChainForL1Token = Object.keys(l2TokenAddresses).find((chainId) => {
return (
mainnetSpokePoolClient.isDepositRouteEnabled(l1Token, Number(chainId)) &&
Number(chainId) !== CHAIN_IDs.MAINNET &&
Object.keys(this.spokePoolClients).includes(chainId)
);
});
const destinationChains = Object.keys(l2TokenAddresses)
.map((chainId) => Number(chainId))
.filter((chainId) => {
return (
chainId !== CHAIN_IDs.MAINNET &&
mainnetSpokePoolClient.isDepositRouteEnabled(l1Token, chainId) &&
Object.keys(this.spokePoolClients).includes(chainId.toString())
);
});

// No valid deposit routes from mainnet for this token. We won't record a limit for it.
if (validDestinationChainForL1Token === undefined) {
if (destinationChains.length === 0) {
return undefined;
}
return this.callLimits(l1Token, Number(validDestinationChainForL1Token));

return this.callLimits(l1Token, destinationChains);
})
);
for (let i = 0; i < tokensQuery.length; i++) {

tokensQuery.forEach((token, i) => {
const resolvedData = data[i];
if (resolvedData === undefined) {
if (isDefined(resolvedData)) {
this.limits[token] = data[i].maxDeposit;
} else {
this.logger.debug({
at: "AcrossAPIClient",
message: "No valid deposit routes for enabled LP token, skipping",
token: tokensQuery[i],
token,
});
continue;
}
const l1Token = tokensQuery[i];
this.limits[l1Token] = resolvedData.maxDeposit;
}
});

this.logger.debug({
at: "AcrossAPIClient",
message: "🏁 Fetched max deposit limits",
Expand All @@ -105,26 +114,29 @@ export class AcrossApiClient {

private async callLimits(
l1Token: string,
destinationChainId: number,
destinationChainIds: number[],
timeout = this.timeout
): Promise<DepositLimits> {
const path = "limits";
const url = `${this.endpoint}/${path}`;
const params = { token: l1Token, destinationChainId, originChainId: 1 };

try {
const result = await axios(url, { timeout, params });
return result.data;
} catch (err) {
const msg = _.get(err, "response.data", _.get(err, "response.statusText", (err as AxiosError).message));
this.logger.warn({
at: "AcrossAPIClient",
message: "Failed to get /limits, setting limit to 0",
url,
params,
msg,
});
return { maxDeposit: BigNumber.from(0) };

for (const destinationChainId of destinationChainIds) {
const params = { token: l1Token, destinationChainId, originChainId: 1 };
try {
const result = await axios(url, { timeout, params });
return result.data;
} catch (err) {
const msg = _.get(err, "response.data", _.get(err, "response.statusText", (err as AxiosError).message));
this.logger.warn({
at: "AcrossAPIClient",
message: "Failed to get /limits",
url,
params,
msg,
});
}
}

return { maxDeposit: bnZero };
}
}
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;
}
13 changes: 8 additions & 5 deletions src/relayer/RelayerClientHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,14 @@ export async function constructRelayerClients(

// We only use the API client to load /limits for chains so we should remove any chains that are not included in the
// destination chain list.
const destinationSpokePoolClients = Object.fromEntries(
Object.keys(spokePoolClients)
.filter((chainId) => config.relayerDestinationChains.includes(Number(chainId)))
.map((chainId) => [chainId, spokePoolClients[chainId]])
);
const destinationSpokePoolClients =
config.relayerDestinationChains.length === 0
? spokePoolClients
: Object.fromEntries(
Object.keys(spokePoolClients)
.filter((chainId) => config.relayerDestinationChains.includes(Number(chainId)))
.map((chainId) => [chainId, spokePoolClients[chainId]])
);

const acrossApiClient = new AcrossApiClient(
logger,
Expand Down
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

0 comments on commit 7cfa842

Please sign in to comment.