Skip to content

Single AddressBook for all adapters #919

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

Merged
merged 7 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions contracts/SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ abstract contract SpokePool is
event PausedFills(bool isPaused);
event SetOFTMessenger(address indexed token, address indexed messenger);

error OFTTokenMismatch();

/**
* @notice Construct the SpokePool. Normally, logic contracts used in upgradeable proxies shouldn't
* have constructors since the following code will be executed within the logic contract's state, not the
Expand Down Expand Up @@ -1757,6 +1759,9 @@ abstract contract SpokePool is
}

function _setOftMessenger(address _token, address _messenger) internal {
if (IOFT(_messenger).token() != _token) {
revert OFTTokenMismatch();
}
oftMessengers[_token] = _messenger;
emit SetOFTMessenger(_token, _messenger);
}
Expand Down
20 changes: 13 additions & 7 deletions contracts/chain-adapters/Arbitrum_Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import "../libraries/CircleCCTPAdapter.sol";
import "../libraries/OFTTransportAdapter.sol";
import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridge.sol";
import { OFTAddressBook } from "../libraries/OFTAddressBook.sol";
import { AdapterStore } from "../libraries/AdapterStore.sol";

/**
* @notice Contract containing logic to send messages from L1 to Arbitrum.
Expand Down Expand Up @@ -46,19 +46,22 @@
address public constant L1_DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F;

// This address on L2 receives extra ETH that is left over after relaying a message via the inbox.
address public immutable L2_REFUND_L2_ADDRESS;

Check warning on line 49 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

Check warning on line 49 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

// Inbox system contract to send messages to Arbitrum. Token bridges use this to send tokens to L2.
// https://github.com/OffchainLabs/nitro-contracts/blob/f7894d3a6d4035ba60f51a7f1334f0f2d4f02dce/src/bridge/Inbox.sol
ArbitrumL1InboxLike public immutable L1_INBOX;

Check warning on line 53 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

Check warning on line 53 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

// Router contract to send tokens to Arbitrum. Routes to correct gateway to bridge tokens. Internally this
// contract calls the Inbox.
// Generic gateway: https://github.com/OffchainLabs/token-bridge-contracts/blob/main/contracts/tokenbridge/ethereum/gateway/L1ArbitrumGateway.sol
ArbitrumL1ERC20GatewayLike public immutable L1_ERC20_GATEWAY_ROUTER;

Check warning on line 58 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

Check warning on line 58 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

// Helper contract to help us map token -> OFT messenger for OFT-enabled tokens
OFTAddressBook public immutable OFT_ADDRESS_BOOK;
// Helper storage contract to help this Adapter map token => OFT messenger for OFT bridging.
AdapterStore public immutable ADAPTER_STORE;

Check warning on line 61 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

Check warning on line 61 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

// Chain id of the chain this adapter helps bridge to.
uint256 public immutable DESTINATION_CHAIN_ID;

Check warning on line 64 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

Check warning on line 64 in contracts/chain-adapters/Arbitrum_Adapter.sol

View workflow job for this annotation

GitHub Actions / Lint (20)

Variable name must be in mixedCase

/**
* @notice Constructs new Adapter.
Expand All @@ -67,7 +70,8 @@
* @param _l2RefundL2Address L2 address to receive gas refunds on after a message is relayed.
* @param _l1Usdc USDC address on L1.
* @param _cctpTokenMessenger TokenMessenger contract to bridge via CCTP.
* @param _oftAddressBook OFTAddressBook contract to help identify token -> oftMessenger relationship for OFT bridging.
* @param _dstChainId Chain id of a destination chain for this adapter.
* @param _adapterStore AdapterStore contract to help identify token => oftMessenger relationship for OFT bridging.
* @param _oftFeeCap A fee cap we apply to OFT bridge native payment. A good default is 1 ether
*/
constructor(
Expand All @@ -76,7 +80,8 @@
address _l2RefundL2Address,
IERC20 _l1Usdc,
ITokenMessenger _cctpTokenMessenger,
OFTAddressBook _oftAddressBook,
uint256 _dstChainId,
AdapterStore _adapterStore,
uint256 _oftFeeCap
)
CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Arbitrum)
Expand All @@ -85,7 +90,8 @@
L1_INBOX = _l1ArbitrumInbox;
L1_ERC20_GATEWAY_ROUTER = _l1ERC20GatewayRouter;
L2_REFUND_L2_ADDRESS = _l2RefundL2Address;
OFT_ADDRESS_BOOK = _oftAddressBook;
DESTINATION_CHAIN_ID = _dstChainId;
ADAPTER_STORE = _adapterStore;
}

/**
Expand Down Expand Up @@ -203,6 +209,6 @@
* @return messenger OFT messenger contract
*/
function _getOftMessenger(address _token) internal view returns (address) {
return OFT_ADDRESS_BOOK.oftMessengers(_token);
return ADAPTER_STORE.oftMessengers(DESTINATION_CHAIN_ID, _token);
}
}
6 changes: 6 additions & 0 deletions contracts/interfaces/IOFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ struct OFTReceipt {
* @dev This specific interface ID is '0x02e49c2c'.
*/
interface IOFT {
/**
* @notice Retrieves the address of the token associated with the OFT.
* @return token The address of the ERC20 token implementation.
*/
function token() external view returns (address);

/**
* @notice Provides a quote for the send() operation.
* @param _sendParam The parameters for the send() operation.
Expand Down
52 changes: 52 additions & 0 deletions contracts/libraries/AdapterStore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IOFT } from "../interfaces/IOFT.sol";

/**
* @dev A helper contract for chain adapters that support OFT or XERC20 messaging on L1
* @dev Handles token => messenger/router mapping storage, as adapters are called via delegatecall and don't have relevant storage space
*/
contract AdapterStore is Ownable {
mapping(uint256 => mapping(address => address)) public oftMessengers;

event OFTMessengerSet(uint256 indexed adapterDstId, address indexed l1Token, address oftMessenger);

error OFTTokenMismatch();
error ArrayLengthMismatch();

function setOFTMessenger(
uint256 adapterDstId,
address l1Token,
address oftMessenger
) external onlyOwner {
_setOFTMessenger(adapterDstId, l1Token, oftMessenger);
}

function batchSetOFTMessenger(
uint256[] calldata adapterIds,
address[] calldata tokens,
address[] calldata messengers
) external onlyOwner {
if (adapterIds.length != tokens.length || adapterIds.length != messengers.length) {
revert ArrayLengthMismatch();
}

for (uint256 i = 0; i < adapterIds.length; i++) {
_setOFTMessenger(adapterIds[i], tokens[i], messengers[i]);
}
}

function _setOFTMessenger(
uint256 _adapterChainId,
address _l1Token,
address _oftMessenger
) internal {
if (IOFT(_oftMessenger).token() != _l1Token) {
revert OFTTokenMismatch();
}
oftMessengers[_adapterChainId][_l1Token] = _oftMessenger;
emit OFTMessengerSet(_adapterChainId, _l1Token, _oftMessenger);
}
}
19 changes: 0 additions & 19 deletions contracts/libraries/OFTAddressBook.sol

This file was deleted.

8 changes: 5 additions & 3 deletions contracts/libraries/OFTTransportAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ contract OFTTransportAdapter {

// `false` in the 2nd param here refers to `bool _payInLzToken`. We will pay in native token, so set to `false`
MessagingFee memory fee = _messenger.quoteSend(sendParam, false);
if (fee.nativeFee > FEE_CAP) revert OftFeeCapExceeded();
if (fee.nativeFee > address(this).balance) revert OftInsufficientBalanceForFee();
// Create a stack variable to optimize gas usage on subsequent reads
uint256 nativeFee = fee.nativeFee;
if (nativeFee > FEE_CAP) revert OftFeeCapExceeded();
if (nativeFee > address(this).balance) revert OftInsufficientBalanceForFee();

// Approve the exact _amount for `_messenger` to spend. Fee will be paid in native token
_token.forceApprove(address(_messenger), _amount);

(, OFTReceipt memory oftReceipt) = _messenger.send{ value: fee.nativeFee }(sendParam, fee, address(this));
(, OFTReceipt memory oftReceipt) = _messenger.send{ value: nativeFee }(sendParam, fee, address(this));

// The HubPool expects that the amount received by the SpokePool is exactly the sent amount
if (_amount != oftReceipt.amountReceivedLD) revert OftIncorrectAmountReceivedLD();
Expand Down
10 changes: 8 additions & 2 deletions deploy/004_deploy_arbitrum_adapter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CHAIN_IDs, MAINNET_CHAIN_IDs } from "@across-protocol/constants";
import { toWei } from "../utils/utils";
import { L1_ADDRESS_MAP, USDC } from "./consts";
import { DeployFunction } from "hardhat-deploy/types";
Expand All @@ -11,6 +12,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
// set to the Risk Labs relayer address. The deployer should change this if necessary.
const l2RefundAddress = "0x07aE8551Be970cB1cCa11Dd7a11F47Ae82e70E67";

// pick correct destination chain id to set based on deployment network
const dstChainId = chainId == MAINNET_CHAIN_IDs.MAINNET ? CHAIN_IDs.ARBITRUM : CHAIN_IDs.ARBITRUM_SEPOLIA;

// 1 ether is a good default for oftFeeCap for cross-chain OFT sends
const oftFeeCap = toWei("1");

Expand All @@ -20,7 +24,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
l2RefundAddress,
USDC[chainId],
L1_ADDRESS_MAP[chainId].cctpTokenMessenger,
L1_ADDRESS_MAP[chainId].oftAddressBook,
dstChainId,
L1_ADDRESS_MAP[chainId].adapterStore,
oftFeeCap,
];
const instance = await hre.deployments.deploy("Arbitrum_Adapter", {
Expand All @@ -33,7 +38,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
l2RefundAddress,
USDC[chainId],
L1_ADDRESS_MAP[chainId].cctpTokenMessenger,
L1_ADDRESS_MAP[chainId].oftAddressBook,
dstChainId,
L1_ADDRESS_MAP[chainId].adapterStore,
oftFeeCap,
],
});
Expand Down
4 changes: 2 additions & 2 deletions deploy/063_deploy_oft_addressbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import "hardhat-deploy";
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await hre.getNamedAccounts();

await hre.deployments.deploy("OFTAddressBook", {
await hre.deployments.deploy("AdapterStore", {
from: deployer,
log: true,
skipIfAlreadyDeployed: true,
});
};

module.exports = func;
func.tags = ["OFTAddressBook", "mainnet"];
func.tags = ["AdapterStore", "mainnet"];
1 change: 1 addition & 0 deletions deploy/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string }
l1AlephZeroInbox: "0x56D8EC76a421063e1907503aDd3794c395256AEb",
l1AlephZeroERC20GatewayRouter: "0xeBb17f398ed30d02F2e8733e7c1e5cf566e17812",
donationBox: "0x0d57392895Db5aF3280e9223323e20F3951E81B1",
adapterStore: "", // to be deployed
},
[CHAIN_IDs.SEPOLIA]: {
finder: "0xeF684C38F94F48775959ECf2012D7E864ffb9dd4",
Expand Down
15 changes: 9 additions & 6 deletions test/evm/hardhat/chain-adapters/Arbitrum_Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { IOFT__factory } from "../../../../typechain/factories/contracts/interfa
import { hubPoolFixture, enableTokensForLP } from "../fixtures/HubPool.Fixture";
import { constructSingleChainTree } from "../MerkleLib.utils";
import { CIRCLE_DOMAIN_IDs } from "../../../../deploy/consts";
import { OFTAddressBook, OFTAddressBook__factory } from "../../../../typechain";
import { AdapterStore, AdapterStore__factory } from "../../../../typechain";

let hubPool: Contract,
arbitrumAdapter: Contract,
Expand All @@ -49,7 +49,7 @@ let l1ERC20GatewayRouter: FakeContract,
cctpMessenger: FakeContract,
cctpTokenMinter: FakeContract,
oftMessenger: FakeContract<IOFT>,
oftAddressBook: FakeContract<OFTAddressBook>;
adapterStore: FakeContract<AdapterStore>;

const arbitrumChainId = 42161;

Expand All @@ -73,8 +73,7 @@ describe("Arbitrum Chain Adapter", function () {
cctpTokenMinter.burnLimitsPerMessage.returns(toWei("1000000"));

oftMessenger = await createTypedFakeFromABI([...IOFT__factory.abi]);
oftAddressBook = await createTypedFakeFromABI([...OFTAddressBook__factory.abi]);
await oftAddressBook.connect(owner).setOFTMessenger(usdt.address, oftMessenger.address);
adapterStore = await createTypedFakeFromABI([...AdapterStore__factory.abi]);

l1Inbox = await createFake("Inbox");
l1ERC20GatewayRouter = await createFake("ArbitrumMockErc20GatewayRouter");
Expand All @@ -91,7 +90,8 @@ describe("Arbitrum Chain Adapter", function () {
refundAddress.address,
usdc.address,
cctpMessenger.address,
oftAddressBook.address,
arbitrumChainId,
adapterStore.address,
oftFeeCap
);

Expand Down Expand Up @@ -260,14 +260,17 @@ describe("Arbitrum Chain Adapter", function () {
it("Correctly calls the OFT bridge adapter when attempting to bridge USDT", async function () {
const internalChainId = arbitrumChainId;

oftMessenger.token.returns(usdt.address);
await adapterStore.connect(owner).setOFTMessenger(arbitrumChainId, usdt.address, oftMessenger.address);

const { leaves, tree, tokensSendToL2 } = await constructSingleChainTree(usdt.address, 1, internalChainId, 6);
await hubPool
.connect(dataWorker)
.proposeRootBundle([3117], 1, tree.getHexRoot(), consts.mockRelayerRefundRoot, consts.mockSlowRelayRoot);
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness + 1);

// set up correct messenger to be returned on a proper `oftMessengers` call
oftAddressBook.oftMessengers.whenCalledWith(usdt.address).returns(oftMessenger.address);
adapterStore.oftMessengers.whenCalledWith(arbitrumChainId, usdt.address).returns(oftMessenger.address);

// set up `quoteSend` return val
const msgFeeStruct: MessagingFeeStructOutput = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ describe("Arbitrum Spoke Pool", function () {

await seedContract(arbitrumSpokePool, relayer, [dai], weth, amountHeldByPool);
await arbitrumSpokePool.connect(crossDomainAlias).whitelistToken(l2Dai, dai.address);
await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, l2OftMessenger.address);
});

it("Only cross domain owner upgrade logic contract", async function () {
Expand Down Expand Up @@ -162,6 +161,9 @@ describe("Arbitrum Spoke Pool", function () {
});

it("Bridge tokens to hub pool correctly using the OFT messaging for L2 USDT token", async function () {
l2OftMessenger.token.returns(l2UsdtContract.address);
await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, l2OftMessenger.address);

const l2UsdtSendAmount = BigNumber.from("1234567");
const { leaves, tree } = await constructSingleRelayerRefundTree(
l2UsdtContract.address,
Expand Down
Loading