Skip to content

Commit

Permalink
fix(generic bridges): use scroll's canonical bridge function (across-…
Browse files Browse the repository at this point in the history
…protocol#1708)

Fix Scroll cross-chain adapters:
 - Use the native ERC20 bridge for WETH, rather than the 
   AtomicWethDepositor.
 - Correct the gateways used for USDC transfer tracking.
 - Add Scroll tests.
 - Fix an issue in the new Arbitrum cross-chain adapter. This new 
   adapter is not active in prod yet.

---------

Signed-off-by: bennett <[email protected]>
  • Loading branch information
bmzig authored and sameersubudhi committed Sep 10, 2024
1 parent 5b0b852 commit d57c91e
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 368 deletions.
30 changes: 30 additions & 0 deletions contracts/MockScrollBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This file emulates a Scroll ERC20 Gateway. For testing, it is used as both the L1 and L2 bridge contract.

pragma solidity ^0.8.0;

contract ScrollBridge {
event DepositERC20(
address indexed l1Token,
address indexed l2Token,
address indexed from,
address to,
uint256 amount,
bytes data
);
event FinalizeDepositERC20(
address indexed l1Token,
address indexed l2Token,
address indexed from,
address to,
uint256 amount,
bytes data
);

function deposit(address l1Token, address l2Token, address from, address to, uint256 amount) public {
emit DepositERC20(l1Token, l2Token, from, to, amount, new bytes(0));
}

function finalize(address l1Token, address l2Token, address from, address to, uint256 amount) public {
emit FinalizeDepositERC20(l1Token, l2Token, from, to, amount, new bytes(0));
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"node": ">=20"
},
"dependencies": {
"@across-protocol/constants": "^3.1.10",
"@across-protocol/constants": "^3.1.11",
"@across-protocol/contracts": "^3.0.6",
"@across-protocol/sdk": "^3.1.13",
"@across-protocol/sdk": "^3.1.14",
"@arbitrum/sdk": "^3.1.3",
"@aws-sdk/client-kms": "^3.592.0",
"@aws-sdk/client-s3": "^3.592.0",
Expand Down
2 changes: 1 addition & 1 deletion src/adapter/AdapterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class GenericAdapterManager extends AdapterManager {

SUPPORTED_TOKENS[chainId]?.map((symbol) => {
const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[hubChainId];
const bridgeConstructor = CUSTOM_BRIDGE[chainId][l1Token] ?? CANONICAL_BRIDGE[chainId];
const bridgeConstructor = CUSTOM_BRIDGE[chainId]?.[l1Token] ?? CANONICAL_BRIDGE[chainId];

bridges[l1Token] = new bridgeConstructor(chainId, hubChainId, l1Signer, l2Signer, l1Token);
});
Expand Down
18 changes: 15 additions & 3 deletions src/adapter/bridges/ArbitrumOneBridge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Contract, BigNumber, paginatedEventQuery, Signer, EventSearchConfig, Provider, toBN } from "../../utils";
import {
Contract,
BigNumber,
paginatedEventQuery,
Signer,
EventSearchConfig,
Provider,
toBN,
toWei,
} from "../../utils";
import { CONTRACT_ADDRESSES, CUSTOM_ARBITRUM_GATEWAYS } from "../../common";
import { BridgeTransactionDetails, BaseBridgeAdapter, BridgeEvents } from "./BaseBridgeAdapter";
import { processEvent } from "../utils";
Expand All @@ -15,6 +24,7 @@ export class ArbitrumOneBridge extends BaseBridgeAdapter {
"0x000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000";
private readonly l2GasLimit = toBN(150000);
private readonly l2GasPrice = toBN(20e9);
private readonly l1SubmitValue = toWei(0.013);

constructor(
l2chainId: number,
Expand All @@ -40,10 +50,12 @@ export class ArbitrumOneBridge extends BaseBridgeAdapter {
l2Token: string,
amount: BigNumber
): Promise<BridgeTransactionDetails> {
const { l1Gateway, l2GasLimit, l2GasPrice, transactionSubmissionData, l1SubmitValue } = this;
return Promise.resolve({
contract: this.l1Gateway,
contract: l1Gateway,
method: "outboundTransfer",
args: [l1Token, toAddress, amount, this.l2GasLimit, this.l2GasPrice, this.transactionSubmissionData],
args: [l1Token, toAddress, amount, l2GasLimit, l2GasPrice, transactionSubmissionData],
value: l1SubmitValue,
});
}

Expand Down
9 changes: 8 additions & 1 deletion src/adapter/bridges/BaseBridgeAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CHAIN_IDs,
Contract,
BigNumber,
EventSearchConfig,
Expand All @@ -7,6 +8,7 @@ import {
getTranslatedTokenAddress,
assert,
isDefined,
TOKEN_SYMBOLS_MAP,
} from "../../utils";
import { SortableEvent } from "../../interfaces";

Expand Down Expand Up @@ -59,7 +61,12 @@ export abstract class BaseBridgeAdapter {
): Promise<BridgeEvents>;

protected resolveL2TokenAddress(l1Token: string): string {
return getTranslatedTokenAddress(l1Token, this.hubChainId, this.l2chainId, false);
// @todo: Fix call to `getTranslatedTokenAddress()` such that it does not require
// ifDefined(...). This is incompatible with remote chains where both native and
// bridged USDC are defined.
const isNativeUSDC =
this.l2chainId === CHAIN_IDs.SCROLL && isDefined(TOKEN_SYMBOLS_MAP.USDC.addresses[this.l2chainId]);
return getTranslatedTokenAddress(l1Token, this.hubChainId, this.l2chainId, isNativeUSDC);
}

protected getL1Bridge(): Contract {
Expand Down
87 changes: 73 additions & 14 deletions src/adapter/bridges/ScrollERC20Bridge.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,54 @@
import { Contract, BigNumber, paginatedEventQuery, EventSearchConfig, Signer, Provider } from "../../utils";
import { CONTRACT_ADDRESSES } from "../../common";
import {
Contract,
BigNumber,
paginatedEventQuery,
EventSearchConfig,
Signer,
Provider,
toWei,
fixedPointAdjustment,
isContractDeployedToAddress,
bnZero,
} from "../../utils";
import { CONTRACT_ADDRESSES, SCROLL_CUSTOM_GATEWAY } from "../../common";
import { BaseBridgeAdapter, BridgeTransactionDetails, BridgeEvents } from "./BaseBridgeAdapter";
import { processEvent } from "../utils";

const SCROLL_STANDARD_GATEWAY: { l1: string; l2: string } = {
l1: "0xD8A791fE2bE73eb6E6cF1eb0cb3F36adC9B3F8f9",
l2: "0xE2b4795039517653c5Ae8C2A9BFdd783b48f447A",
};

export class ScrollERC20Bridge extends BaseBridgeAdapter {
protected l2Gas = 20000;
// Gas limit obtained here: https://docs.scroll.io/en/developers/l1-and-l2-bridging/eth-and-erc20-token-bridge
protected l2Gas = 200000;
protected feeMultiplier = toWei(1.5);
protected readonly scrollGasPriceOracle: Contract;

protected readonly scrollGatewayRouter;
protected readonly hubPoolAddress;
constructor(
l2chainId: number,
hubChainId: number,
l1Signer: Signer,
l2SignerOrProvider: Signer | Provider,
_l1Token: string
l1Token: string
) {
// Lint Appeasement
_l1Token;
const { address: l1Address, abi: l1Abi } = CONTRACT_ADDRESSES[hubChainId].scrollGatewayRouter;
const { address: l2Address, abi: l2Abi } = CONTRACT_ADDRESSES[l2chainId].scrollGatewayRouter;
const l2Abi = CONTRACT_ADDRESSES[l2chainId].scrollGatewayRouter.abi;
const { l1: l1BridgeAddress, l2: l2BridgeAddress } = SCROLL_CUSTOM_GATEWAY[l1Token] ?? SCROLL_STANDARD_GATEWAY;

const { address: gasPriceOracleAddress, abi: gasPriceOracleAbi } =
CONTRACT_ADDRESSES[hubChainId].scrollGasPriceOracle;
super(l2chainId, hubChainId, l1Signer, l2SignerOrProvider, [l1Address]);

this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer);
this.l2Bridge = new Contract(l2Address, l2Abi, l2SignerOrProvider);
this.l1Bridge = new Contract(l1BridgeAddress, l1Abi, l1Signer);
this.l2Bridge = new Contract(l2BridgeAddress, l2Abi, l2SignerOrProvider);

this.scrollGatewayRouter = new Contract(l1Address, l1Abi, l1Signer);
this.scrollGasPriceOracle = new Contract(gasPriceOracleAddress, gasPriceOracleAbi, l1Signer);

this.hubPoolAddress = CONTRACT_ADDRESSES[hubChainId].hubPool.address;
}

async constructL1ToL2Txn(
Expand All @@ -28,27 +57,43 @@ export class ScrollERC20Bridge extends BaseBridgeAdapter {
l2Token: string,
amount: BigNumber
): Promise<BridgeTransactionDetails> {
const baseFee = await this.getScrollGasPriceOracle().estimateCrossDomainMessageFee(this.l2Gas);
const bufferedFee = baseFee.mul(this.feeMultiplier).div(fixedPointAdjustment);
return Promise.resolve({
contract: this.getL1Bridge(),
contract: this.getScrollGatewayRouter(),
method: "depositERC20",
args: [l1Token, toAddress, amount, this.l2Gas],
value: bufferedFee,
});
}

// The deposit/finalize events that Scroll gateways emit do not index the recipient of transfers. This makes cross-chain transfer tracking a bit
// tricky since the recipient of a deposit may not be the same as the depositor (e.g. hub -> spoke transfers, or randomEoa -> monitoredEoa transfers).
// For both queryL1BridgeInitiationEvents and queryL2BridgeFinalizationEvents, we currently make the assumption that if we are sending to an L2 contract, then
// this L2 contract is the spoke pool, and the original depositor is the hub pool. Likewise, if we are sending to an EOA, then the recipient is the same as the depositor.
// While this limits our ability to track all types of events (and incurs an extra RPC call), we must do this to prevent an expensive event query
// (i.e. ...filters.DepositERC20(l1Token, undefined, undefined), which would return, for example, all WETH deposits to Scroll within the past 10,000 blocks).
async queryL1BridgeInitiationEvents(
l1Token: string,
fromAddress: string,
toAddress: string,
eventConfig: EventSearchConfig
): Promise<BridgeEvents> {
const isL2Contract = await isContractDeployedToAddress(toAddress, this.l2Bridge.provider);
const monitoredFromAddress = isL2Contract ? this.hubPoolAddress : fromAddress;

const l1Bridge = this.getL1Bridge();
const events = await paginatedEventQuery(
l1Bridge,
l1Bridge.filters.DepositERC20(l1Token, undefined, fromAddress),
l1Bridge.filters.DepositERC20(l1Token, undefined, monitoredFromAddress),
eventConfig
);
// Take all events which are sending an amount greater than 0.
const processedEvents = events
.map((event) => processEvent(event, "amount", "to", "from"))
.filter(({ amount }) => amount > bnZero);
return {
[this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount", "to", "from")),
[this.resolveL2TokenAddress(l1Token)]: processedEvents.filter(({ to }) => to === toAddress), // Only return the events which match to the toAddress
};
}

Expand All @@ -58,14 +103,28 @@ export class ScrollERC20Bridge extends BaseBridgeAdapter {
toAddress: string,
eventConfig: EventSearchConfig
): Promise<BridgeEvents> {
const isL2Contract = await isContractDeployedToAddress(toAddress, this.l2Bridge.provider);
const monitoredFromAddress = isL2Contract ? this.hubPoolAddress : fromAddress;

const l2Bridge = this.getL2Bridge();
const events = await paginatedEventQuery(
l2Bridge,
l2Bridge.filters.FinalizeDepositERC20(l1Token, undefined, fromAddress),
l2Bridge.filters.FinalizeDepositERC20(l1Token, undefined, monitoredFromAddress),
eventConfig
);
const processedEvents = events
.map((event) => processEvent(event, "amount", "to", "from"))
.filter(({ amount }) => amount > bnZero);
return {
[this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount", "to", "from")),
[this.resolveL2TokenAddress(l1Token)]: processedEvents.filter(({ to }) => to === toAddress),
};
}

protected getScrollGasPriceOracle(): Contract {
return this.scrollGasPriceOracle;
}

protected getScrollGatewayRouter(): Contract {
return this.scrollGatewayRouter;
}
}
75 changes: 0 additions & 75 deletions src/adapter/bridges/ScrollWethBridge.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/adapter/bridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,3 @@ export * from "./ZKSyncWethBridge";
export * from "./LineaWethBridge";
export * from "./BlastBridge";
export * from "./ScrollERC20Bridge";
export * from "./ScrollWethBridge";
2 changes: 1 addition & 1 deletion src/clients/bridges/ScrollAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class ScrollAdapter extends BaseChainAdapter {
const l1Signer = spokePoolClients[MAINNET].spokePool.signer;
SUPPORTED_TOKENS[SCROLL]?.map((symbol) => {
const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[MAINNET];
const bridgeConstructor = CUSTOM_BRIDGE[SCROLL][l1Token] ?? CANONICAL_BRIDGE[SCROLL];
const bridgeConstructor = CUSTOM_BRIDGE[SCROLL]?.[l1Token] ?? CANONICAL_BRIDGE[SCROLL];
bridges[l1Token] = new bridgeConstructor(SCROLL, MAINNET, l1Signer, l2Signer, l1Token);
});
super(
Expand Down
18 changes: 14 additions & 4 deletions src/common/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
LineaWethBridge,
BlastBridge,
ScrollERC20Bridge,
ScrollWethBridge,
} from "../adapter/bridges";
import { DEFAULT_L2_CONTRACT_ADDRESSES } from "@eth-optimism/sdk";
import { CONTRACT_ADDRESSES } from "./ContractAddresses";
Expand Down Expand Up @@ -420,9 +419,6 @@ export const CUSTOM_BRIDGE: {
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: PolygonWethBridge,
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: UsdcTokenSplitterBridge,
},
[CHAIN_IDs.SCROLL]: {
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: ScrollWethBridge,
},
[CHAIN_IDs.ZK_SYNC]: {
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: ZKSyncWethBridge,
},
Expand Down Expand Up @@ -458,6 +454,20 @@ export const CUSTOM_ARBITRUM_GATEWAYS: { [chainId: number]: { l1: string; l2: st
},
};

// We currently support WBTC, USDT, USDC, and WETH as routes on scroll. WBTC, USDT, and USDC transfer events can all be queried from the standard ERC20
// gateway, WETH has its own custom gateways, and other ERC20s may also have their own gateway, so it is very important to define unique gateways (ones
// which are NOT the standard ERC20 gateway) if/when we add new deposit routes.
export const SCROLL_CUSTOM_GATEWAY: { [chainId: number]: { l1: string; l2: string } } = {
[TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.MAINNET]]: {
l1: "0x7AC440cAe8EB6328de4fA621163a792c1EA9D4fE",
l2: "0x7003E7B7186f0E6601203b99F7B8DECBfA391cf9",
},
[TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.MAINNET]]: {
l1: "0xf1AF3b23DE0A5Ca3CAb7261cb0061C0D779A5c7B",
l2: "0x33B60d5Dd260d453cAC3782b0bDC01ce84672142",
},
};

// Expected worst-case time for message from L1 to propogate to L2 in seconds
export const EXPECTED_L1_TO_L2_MESSAGE_TIME = {
[CHAIN_IDs.ARBITRUM]: 20 * 60,
Expand Down
Loading

0 comments on commit d57c91e

Please sign in to comment.