Skip to content

Commit

Permalink
fix(InventoryClient): Map outstanding crosschain transfers to correct…
Browse files Browse the repository at this point in the history
… relayer address (#1457)

* improve(InventoryClient): Track L2 WETH Deposit events when computing outstanding crosschain transfers

## Summary

The rebalancer is designed to [wrap any excess ETH into WETH](https://github.com/across-protocol/relayer-v3/blob/dd5e20ec4538c8c3e7bfb923187f521f92d85882/src/relayer/RelayerClientHelper.ts#L216) before [updating the `TokenClient`](https://github.com/across-protocol/relayer-v3/blob/dd5e20ec4538c8c3e7bfb923187f521f92d85882/src/relayer/RelayerClientHelper.ts#L223) and eventually proceeding to the [rebalancing inventory step](https://github.com/across-protocol/relayer-v3/blob/dd5e20ec4538c8c3e7bfb923187f521f92d85882/src/relayer/index.ts#L118). However, in practice we've seen that the rebalancer is not able to detect these wrapped ETH events in time to prevent a duplicate rebalance.

This PR adds safety to the chain adapters where we bridge ETH via the AtomicDepositor and expect to receive ETH on the L2 side and subsequently expect to wrap it into WETH. We now only mark a deposit as finalized in the inventory client when the L2 deposit event is followed by a wrap event.

This algorithm can clearly fall prey to false positives where an unrelated ETH unwrap leads to the rebalancer falsely detecting that an L1 to L2 WETH transfer has finalized. However, for the most part, WETH wraps should only be triggered by the rebalancer, and even if wrapping ETH was unrelated to the specific L1 to L2 transfer we care about, it should still convert the ETH into WETH balance which is what we care about and would prevent WETH duplicate cross chain transfers.

## Additional Fixes

- This PR fixes a bug in the ZkSyncAdapter where the `initiatedQueryResult` for WETH were not being properly loaded into the cross chain outstanding transfers object. This caused the inventory manager to NEVER identify outstanding zksync deposits, leading to possible duplicate rebalances until the cross chain transferred ETH was wrapped into WETH on the L2 side.
- This PR fixes a bug in the Optimism Adapter where outstanding ETH cross chain transfer amounts are being credited to the atomic depositor address and therefore don't factor into the optimism/base relayer address. This similarly causes issues where the relayer's balance will look off by the amount of ETH transferred until it gets wrapped into WETH on the L2 side and would get picked up by the TokenClient at that point

* Add early exits to adapters

* Refactor constructor

* Update ArbitrumAdapter.ts

* Update PolygonAdapter.ts

* Refactor

* ? atomicDepositor

* comments, remove getL1Weth()

* Update OpStackAdapter.ts

* Update utils.ts

* Fix Arbitrum Adapter and Polygon adapter bugs

* Restore adapter logic without weth wrapping tracking

* Update LineaAdapter.ts

* Fix monitor

* Add delayed token client update

* Fix

* remove floor()

* FIx OP adapter

* fix zksync bridge and weth adapter

* Filter out other spoke pool addresses + fix weth bridge

* zksync

* Update WethBridge.ts

* Update ZKSyncAdapter.ts

* Update WethBridge.ts

* Update BaseAdapter.ts

* Update LineaAdapter.ts

* Add WETH checking to ZkSync

* Update OpStackAdapter.ts

* Fix Linea filters

* Linea refactor + unit tested

* Linea refactored + unit tested

* fix polygon

* Update PolygonAdapter.ts

* Update ZKSyncAdapter.ts

* WETH op stack  works

* Unit test op stack weth bridge

* Refactors

* lint

* polygon and zk optimizations

* fix(bridges): Ensure l1Tokens and bridges are same length

## OpStackAdapter fix:
- If `bridges` is type string, we can't call .length on it otherwise we'll inadvertently use the char count of the string

## PolygonAdapter fix:
- Must add WETH to l1Token list before early exiting

* rename l1Gateway to l1Gateways

---------

Co-authored-by: Paul <[email protected]>
  • Loading branch information
nicholaspai and pxrl authored May 3, 2024
1 parent c800b60 commit 28ec6c1
Show file tree
Hide file tree
Showing 18 changed files with 980 additions and 151 deletions.
60 changes: 60 additions & 0 deletions contracts/MockLineaEvents.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// This file contains contracts that can be used to unit test the src/clients/bridges/LineaAdapter.ts
/// code which reads events from Linea contracts facilitating cross chain transfers.

pragma solidity ^0.8.0;

contract LineaWethBridge {
event MessageClaimed(bytes32 indexed _messageHash);
event MessageSent(
address indexed _from,
address indexed _to,
uint256 _fee,
uint256 _value,
uint256 _nonce,
bytes _calldata,
bytes32 indexed _messageHash
);

function emitMessageSent(address from, address to, uint256 value) external {
emit MessageSent(from, to, 0, value, 0, new bytes(0), bytes32(0));
}

function emitMessageSentWithMessageHash(address from, address to, uint256 value, bytes32 messageHash) external {
emit MessageSent(from, to, 0, value, 0, new bytes(0), messageHash);
}

function emitMessageClaimed(bytes32 messageHash) external {
emit MessageClaimed(messageHash);
}
}

contract LineaUsdcBridge {
event Deposited(address indexed depositor, uint256 amount, address indexed to);
event ReceivedFromOtherLayer(address indexed recipient, uint256 amount);

function emitDeposited(address depositor, address to) external {
emit Deposited(depositor, 0, to);
}

function emitReceivedFromOtherLayer(address recipient) external {
emit ReceivedFromOtherLayer(recipient, 0);
}
}

contract LineaERC20Bridge {
event BridgingInitiated(address indexed sender, address recipient, address indexed token, uint256 indexed amount);
event BridgingFinalized(
address indexed nativeToken,
address indexed bridgedToken,
uint256 indexed amount,
address recipient
);

function emitBridgingInitiated(address sender, address recipient, address token) external {
emit BridgingInitiated(sender, recipient, token, 0);
}

function emitBridgingFinalized(address l1Token, address recipient) external {
emit BridgingFinalized(l1Token, address(0), 0, recipient);
}
}
24 changes: 24 additions & 0 deletions contracts/MockOpStackEvents.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// This file contains contracts that can be used to unit test the src/clients/bridges/op-stack
/// code which reads events from OpStack contracts facilitating cross chain transfers.

pragma solidity ^0.8.0;

contract OpStackWethBridge {
event ETHDepositInitiated(address indexed _from, address indexed _to, uint256 _amount, bytes _data);
event DepositFinalized(
address indexed _l1Token,
address indexed _l2Token,
address indexed _from,
address _to,
uint256 _amount,
bytes _data
);

function emitDepositInitiated(address from, address to, uint256 amount) external {
emit ETHDepositInitiated(from, to, amount, new bytes(0));
}

function emitDepositFinalized(address from, address to, uint256 amount) external {
emit DepositFinalized(address(0), address(0), from, to, amount, new bytes(0));
}
}
24 changes: 18 additions & 6 deletions src/clients/bridges/AdapterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,35 @@ export class AdapterManager {
if (!spokePoolClients) {
return;
}
const spokePoolAddresses = Object.values(spokePoolClients).map((client) => client.spokePool.address);

// The adapters are only set up to monitor EOA's and the HubPool and SpokePool address, so remove
// spoke pool addresses from other chains.
const filterMonitoredAddresses = (chainId: number) => {
return monitoredAddresses.filter(
(address) =>
this.hubPoolClient.hubPool.address === address ||
this.spokePoolClients[chainId].spokePool.address === address ||
!spokePoolAddresses.includes(address)
);
};
if (this.spokePoolClients[10] !== undefined) {
this.adapters[10] = new OptimismAdapter(logger, spokePoolClients, monitoredAddresses);
this.adapters[10] = new OptimismAdapter(logger, spokePoolClients, filterMonitoredAddresses(10));
}
if (this.spokePoolClients[137] !== undefined) {
this.adapters[137] = new PolygonAdapter(logger, spokePoolClients, monitoredAddresses);
this.adapters[137] = new PolygonAdapter(logger, spokePoolClients, filterMonitoredAddresses(137));
}
if (this.spokePoolClients[42161] !== undefined) {
this.adapters[42161] = new ArbitrumAdapter(logger, spokePoolClients, monitoredAddresses);
this.adapters[42161] = new ArbitrumAdapter(logger, spokePoolClients, filterMonitoredAddresses(42161));
}
if (this.spokePoolClients[324] !== undefined) {
this.adapters[324] = new ZKSyncAdapter(logger, spokePoolClients, monitoredAddresses);
this.adapters[324] = new ZKSyncAdapter(logger, spokePoolClients, filterMonitoredAddresses(324));
}
if (this.spokePoolClients[8453] !== undefined) {
this.adapters[8453] = new BaseChainAdapter(logger, spokePoolClients, monitoredAddresses);
this.adapters[8453] = new BaseChainAdapter(logger, spokePoolClients, filterMonitoredAddresses(8453));
}
if (this.spokePoolClients[59144] !== undefined) {
this.adapters[59144] = new LineaAdapter(logger, spokePoolClients, monitoredAddresses);
this.adapters[59144] = new LineaAdapter(logger, spokePoolClients, filterMonitoredAddresses(59144));
}

logger.debug({
Expand Down
14 changes: 8 additions & 6 deletions src/clients/bridges/ArbitrumAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ export class ArbitrumAdapter extends CCTPAdapter {

const promises: Promise<Event[]>[] = [];
const cctpOutstandingTransfersPromise: Record<string, Promise<SortableEvent[]>> = {};
const validTokens: string[] = [];
// Fetch bridge events for all monitored addresses.
for (const monitoredAddress of this.monitoredAddresses) {
for (const l1Token of availableL1Tokens) {
Expand All @@ -113,7 +112,6 @@ export class ArbitrumAdapter extends CCTPAdapter {
paginatedEventQuery(l1Bridge, l1Bridge.filters.DepositInitiated(...l1SearchFilter), l1SearchConfig),
paginatedEventQuery(l2Bridge, l2Bridge.filters.DepositFinalized(...l2SearchFilter), l2SearchConfig)
);
validTokens.push(l1Token);
}
}

Expand All @@ -126,13 +124,13 @@ export class ArbitrumAdapter extends CCTPAdapter {
);

// 2 events per token.
const numEventsPerMonitoredAddress = 2 * validTokens.length;
const numEventsPerMonitoredAddress = 2 * availableL1Tokens.length;

// Segregate the events list by monitored address.
const resultsByMonitoredAddress = Object.fromEntries(
this.monitoredAddresses.map((monitoredAddress, index) => {
const start = index * numEventsPerMonitoredAddress;
return [monitoredAddress, results.slice(start, start + numEventsPerMonitoredAddress + 1)];
return [monitoredAddress, results.slice(start, start + numEventsPerMonitoredAddress)];
})
);

Expand All @@ -142,7 +140,11 @@ export class ArbitrumAdapter extends CCTPAdapter {
// The logic below takes the results from the promises and spreads them into the l1DepositInitiatedEvents and
// l2DepositFinalizedEvents state from the BaseAdapter.
eventsToProcess.forEach((result, index) => {
const l1Token = validTokens[Math.floor(index / 2)];
if (eventsToProcess.length === 0) {
return;
}
assert(eventsToProcess.length % 2 === 0, "Events list length should be even");
const l1Token = availableL1Tokens[Math.floor(index / 2)];
// l1Token is not an indexed field on Aribtrum gateway's deposit events, so these events are for all tokens.
// Therefore, we need to filter unrelated deposits of other tokens.
const filteredEvents = result.filter((event) => spreadEvent(event.args)["l1Token"] === l1Token);
Expand Down Expand Up @@ -170,7 +172,7 @@ export class ArbitrumAdapter extends CCTPAdapter {
}
}

return this.computeOutstandingCrossChainTransfers(validTokens);
return this.computeOutstandingCrossChainTransfers(availableL1Tokens);
}

async checkTokenApprovals(address: string, l1Tokens: string[]): Promise<void> {
Expand Down
21 changes: 18 additions & 3 deletions src/clients/bridges/BaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { CONTRACT_ADDRESSES, TOKEN_APPROVALS_TO_FIRST_ZERO } from "../../common"
import { OutstandingTransfers, SortableEvent } from "../../interfaces";
export interface DepositEvent extends SortableEvent {
amount: BigNumber;
to: string;
transactionHash: string;
}

interface Events {
Expand All @@ -56,7 +56,7 @@ export abstract class BaseAdapter {
baseL1SearchConfig: MakeOptional<EventSearchConfig, "toBlock">;
baseL2SearchConfig: MakeOptional<EventSearchConfig, "toBlock">;
readonly wethAddress = TOKEN_SYMBOLS_MAP.WETH.addresses[this.hubChainId];
readonly atomicDepositorAddress = CONTRACT_ADDRESSES[this.hubChainId].atomicDepositor.address;
readonly atomicDepositorAddress = CONTRACT_ADDRESSES[this.hubChainId]?.atomicDepositor.address;

l1DepositInitiatedEvents: Events = {};
l2DepositFinalizedEvents: Events = {};
Expand Down Expand Up @@ -264,6 +264,14 @@ export abstract class BaseAdapter {
return compareAddressesSimple(l1Token, this.wethAddress);
}

isHubChainContract(address: string): Promise<Boolean> {
return utils.isContractDeployedToAddress(address, this.getProvider(this.hubChainId));
}

isL2ChainContract(address: string): Promise<Boolean> {
return utils.isContractDeployedToAddress(address, this.getProvider(this.chainId));
}

/**
* Get L1 Atomic WETH depositor contract
* @returns L1 Atomic WETH depositor contract
Expand All @@ -277,6 +285,14 @@ export abstract class BaseAdapter {
);
}

getHubPool(): Contract {
const hubPoolContractData = CONTRACT_ADDRESSES[this.hubChainId]?.hubPool;
if (!hubPoolContractData) {
throw new Error(`hubPoolContractData not found for chain ${this.hubChainId}`);
}
return new Contract(hubPoolContractData.address, hubPoolContractData.abi, this.getSigner(this.hubChainId));
}

/**
* Determine whether this adapter supports an l1 token address
* @param l1Token an address
Expand Down Expand Up @@ -335,7 +351,6 @@ export abstract class BaseAdapter {
at: `${this.getName()}#_sendTokenToTargetChain`,
message: "Simulation result",
succeed,
...txnRequest,
});
return { hash: ZERO_ADDRESS } as TransactionResponse;
}
Expand Down
Loading

0 comments on commit 28ec6c1

Please sign in to comment.