Skip to content

Commit

Permalink
polish OFTTransportAdapter, add OFT support to Arbitrum_Adapter on L1…
Browse files Browse the repository at this point in the history
…, and Arbitrum_SpokePool on L2

Signed-off-by: Ihor Farion <[email protected]>
  • Loading branch information
grasphoper committed Feb 26, 2025
1 parent dc3da61 commit 6898995
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 58 deletions.
10 changes: 8 additions & 2 deletions contracts/AlephZero_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ contract AlephZero_SpokePool is Arbitrum_SpokePool {
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer,
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
ITokenMessenger _cctpTokenMessenger,
IERC20 _l2Usdt,
IOFT _oftMessenger,
uint32 _ethereumUsdtDstEid
)
Arbitrum_SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
_l2Usdc,
_cctpTokenMessenger
_cctpTokenMessenger,
_l2Usdt,
_oftMessenger,
_ethereumUsdtDstEid
)
{} // solhint-disable-line no-empty-blocks
}
13 changes: 11 additions & 2 deletions contracts/Arbitrum_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ pragma solidity ^0.8.0;

import "./SpokePool.sol";
import "./libraries/CircleCCTPAdapter.sol";
import "./libraries/OFTTransportAdapter.sol";
import { CrossDomainAddressUtils } from "./libraries/CrossDomainAddressUtils.sol";
import { ArbitrumL2ERC20GatewayLike } from "./interfaces/ArbitrumBridge.sol";

/**
* @notice AVM specific SpokePool. Uses AVM cross-domain-enabled logic to implement admin only access to functions.
* @custom:security-contact [email protected]
*/
contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter, OFTTransportAdapter {
// Address of the Arbitrum L2 token gateway to send funds to L1.
address public l2GatewayRouter;

Expand All @@ -27,10 +28,14 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer,
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
ITokenMessenger _cctpTokenMessenger,
IERC20 _l2Usdt,
IOFT _oftMessenger,
uint32 _ethereumUsdtDstEid
)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
OFTTransportAdapter(_l2Usdt, _oftMessenger, _ethereumUsdtDstEid)
{} // solhint-disable-line no-empty-blocks

/**
Expand Down Expand Up @@ -86,6 +91,10 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
// If the l2TokenAddress is UDSC, we need to use the CCTP bridge.
if (_isCCTPEnabled() && l2TokenAddress == address(usdcToken)) {
_transferUsdc(withdrawalRecipient, amountToReturn);
}
// If the l2TokenAddress is USDT, we need to use the OFT bridge.
else if (_isOFTEnabled(l2TokenAddress)) {
_transferUsdt(withdrawalRecipient, amountToReturn);
} else {
// Check that the Ethereum counterpart of the L2 token is stored on this contract.
address ethereumTokenToBridge = whitelistedTokens[l2TokenAddress];
Expand Down
18 changes: 15 additions & 3 deletions contracts/chain-adapters/Arbitrum_Adapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../external/interfaces/CCTPInterfaces.sol";
import "../libraries/CircleCCTPAdapter.sol";
import "../libraries/OFTTransportAdapter.sol";
import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike } from "../interfaces/ArbitrumBridge.sol";

/**
Expand All @@ -18,7 +19,7 @@ import { ArbitrumInboxLike as ArbitrumL1InboxLike, ArbitrumL1ERC20GatewayLike }
*/

// solhint-disable-next-line contract-name-camelcase
contract Arbitrum_Adapter is AdapterInterface, CircleCCTPAdapter {
contract Arbitrum_Adapter is AdapterInterface, CircleCCTPAdapter, OFTTransportAdapter {
using SafeERC20 for IERC20;

// Amount of ETH allocated to pay for the base submission fee. The base submission fee is a parameter unique to
Expand Down Expand Up @@ -61,14 +62,23 @@ contract Arbitrum_Adapter is AdapterInterface, CircleCCTPAdapter {
* @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 _l1Usdt USDT address on L1.
* @param _oftTransport OFTAdapter proxy address on L1 to bridge via OFT.
* @param _arbitrumUsdtDstEid destination endpoint id of USDT OFTAdater on Arbitrum.
*/
constructor(
ArbitrumL1InboxLike _l1ArbitrumInbox,
ArbitrumL1ERC20GatewayLike _l1ERC20GatewayRouter,
address _l2RefundL2Address,
IERC20 _l1Usdc,
ITokenMessenger _cctpTokenMessenger
) CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Arbitrum) {
ITokenMessenger _cctpTokenMessenger,
IERC20 _l1Usdt,
IOFT _oftTransport,
uint32 _arbitrumUsdtDstEid
)
CircleCCTPAdapter(_l1Usdc, _cctpTokenMessenger, CircleDomainIds.Arbitrum)
OFTTransportAdapter(_l1Usdt, _oftTransport, _arbitrumUsdtDstEid)
{
L1_INBOX = _l1ArbitrumInbox;
L1_ERC20_GATEWAY_ROUTER = _l1ERC20GatewayRouter;
L2_REFUND_L2_ADDRESS = _l2RefundL2Address;
Expand Down Expand Up @@ -116,6 +126,8 @@ contract Arbitrum_Adapter is AdapterInterface, CircleCCTPAdapter {
// Check if this token is USDC, which requires a custom bridge via CCTP.
if (_isCCTPEnabled() && l1Token == address(usdcToken)) {
_transferUsdc(to, amount);
} else if (_isOFTEnabled(l1Token)) {
_transferUsdt(to, amount);
}
// If not, we can use the Arbitrum gateway
else {
Expand Down
105 changes: 54 additions & 51 deletions contracts/libraries/OFTTransportAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,82 +12,85 @@ interface IERC20Decimals {
function decimals() external view returns (uint8);
}

// @note: this contract is built for a 1-to-1 relationship with `usdt` for now
// however, in theory, OFT can support any token. We can think about it when we
// need to support more OFT tokens
/**
* @notice Facilitate bridging USDT via LayerZero's OFT.
* @dev This contract is intended to be inherited by other chain-specific adapters and spoke pools. This contract is built for 1-to-1 relationship with USDT to facilitate USDT0. When adding other contracts, we'd need to add a token mapping instead of the immutable token address.
* @custom:security-contact [email protected]
*/
contract OFTTransportAdapter {
using AddressToBytes32 for address;

bytes public constant EMPTY_MSG_BYTES = new bytes(0);
address public constant ZERO_ADDRESS = address(0);

// USDT address on current chain
IERC20 public immutable usdt;
IOFT public immutable oftTransport;
// OFTAdapter address on current chain. Mailbox for OFT cross-chain transfers
IOFT public immutable oftMessenger;

// todo: make sure that this can't change under us
// todo: can this somehow be queriable from the contract on our own chain if it ever changes?
uint32 public immutable dstEid; // destination endpoint id in the OFT messaging protocol
// todo ihor: arbitrum one is 30101. It should be found through `OFTAdapter -> endpoint() -> eid()` on the destination chain
// todo: make sure it can't change under us
/**
* @notice The destination endpoint id in the OFT messaging protocol
* @dev Can be found on target chain OFTAdapter -> endpoint() -> eid()
*/
uint32 public immutable dstEid;

// todo: has to already contain the receiving domain's smth like(oft address + chain id)
constructor(IERC20 _usdt, IOFT _oftTransport, uint32 _dstEid) {
/**
* @notice intiailizes the OFTTransportAdapter contract.
* @param _usdt USDT address on the current chain.
* @param _oftMessenger OFTAdapter contract to bridge to the other chain. If address is set to zero, OFT bridging will be disabled.
* @param _dstEid The endpoint ID that OFT will transfer funds to.
*/
constructor(IERC20 _usdt, IOFT _oftMessenger, uint32 _dstEid) {
usdt = _usdt;
// todo: do we need this check? amountLD == minAmountLD in _transferUSDT protects us from the same thing.
// @dev: this contract assumes this decimal parity later in amount calculation.
// todo ihor: well, in `_transferUsdt`, if we set `amountLD == minAmountLD`,
// todo ihor: `oftTransport` should protect us from any rounding that happens due to decimals anyway. So perhaps this check can be removed
require(IERC20Decimals(address(_usdt)).decimals() == _oftTransport.sharedDecimals(), "decimals don't match"); // todo: ugh, this is ugly and perhaps not required
oftTransport = _oftTransport;
require(IERC20Decimals(address(_usdt)).decimals() == _oftMessenger.sharedDecimals(), "decimals don't match"); // todo: ugh, this is ugly and perhaps not required
oftMessenger = _oftMessenger;
dstEid = _dstEid;
}

// todo: seems cleaner if this contract decides the full oft branch in the adapter
// e.g. if (_isOFTEnabled(token)) { ... }, rahter than adding extra token conditions in
// the adapter itself
/**
* @notice Returns whether or not the OFT bridge is enabled.
* @dev If the IOFT is the zero address, OFT bridging is disabled.
*/
function _isOFTEnabled(address _token) internal view returns (bool) {
// we have to have a oftTransport set and the token has to be USDT
return address(oftTransport) != address(0) && address(usdt) == _token;
return address(oftMessenger) != address(0) && address(usdt) == _token;
}

function _transferUsdt(address _to, uint256 amount) internal {
// @dev: building `SendParam` struct
// receiver address converted to bytes32
/**
* @notice transfers usdt to destination endpoint
* @param _to address to receive a trasnfer on the destination chain
* @param _amount amount to send
*/
function _transferUsdt(address _to, uint256 _amount) internal {
bytes32 to = _to.toBytes32();

// `amountLD` and `minAmountLD` have a subtle relationship
// `minAmountLD` is used to check against when "the dust"
// is removed from `amountLD`. `amountLD` will be the sent amount for tokens with 6 decimals
// For tokens with other decimal params, please look at `_debitView` function in `OFTCore.sol`
// For usdt with 6 decimals, `amountLD == minAmountLD` should never fail
uint256 amountLD = amount;
uint256 minAmountLD = amount;

// empty options with version identifier. In current version, looks like `0x0003`
// todo: as per TG chat, can set this to empty bytes instead and should be fine
// bytes memory extraOptions = OptionsBuilder.newOptions(); // todo: this requires installing an extra lib `solidity-bytes-utils`
bytes memory extraOptions = new bytes(0);

// todo: can make these an immutable storage var, ZERO_BYTES? Idk
bytes memory composeMsg = new bytes(0);
bytes memory oftCmd = new bytes(0);

SendParam memory sendParam = SendParam(dstEid, to, amountLD, minAmountLD, extraOptions, composeMsg, oftCmd);
// todo: should probably just comment these 2 vars and use _amount in send call below
// @dev these 2 amounts have a subtle relationship. OFT "removes dust" on the send, which should not affect USDT transfer
// @dev setting these two equal protects us from dust subtraction on the OFT side. If any dust is subtracted, the later .send should revert. Should be cautious with this logic
uint256 amountLD = _amount;
uint256 minAmountLD = _amount;

SendParam memory sendParam = SendParam(
dstEid,
to,
amountLD,
minAmountLD,
EMPTY_MSG_BYTES,
EMPTY_MSG_BYTES,
EMPTY_MSG_BYTES
);

MessagingFee memory fee = oftTransport.quoteSend(sendParam, false);
MessagingFee memory fee = oftMessenger.quoteSend(sendParam, false);

// @dev setting refundAddress to zero addr here, because we calculate the fees precicely and we can save gas this way
// todo: not really sure if we should blindly trust the fee.nativeFee here and just send it away. Should we check it against some sane cap?
(MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = oftTransport.send{ value: fee.nativeFee }(
sendParam,
fee,
address(0)
);
// @dev setting refundAddress to zero addr here, because we calculate the fees precicely and we can save gas this way
oftMessenger.send{ value: fee.nativeFee }(sendParam, fee, ZERO_ADDRESS);

// todo: possible further actions:
// 1.
// todo: OFTAdapter enforces this, but should we check anyway?
// return vals from .send: (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt)
// require(amount == oftReceipt.amountSentLD);
// require(amount == oftReceipt.amountReceivedLD);
// 2.
// emit some event, but OFTAdapter already does emit `event OFTSent`, so probably no
}
}

0 comments on commit 6898995

Please sign in to comment.