-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
polish OFTTransportAdapter, add OFT support to Arbitrum_Adapter on L1…
…, and Arbitrum_SpokePool on L2 Signed-off-by: Ihor Farion <[email protected]>
- Loading branch information
1 parent
dc3da61
commit 6898995
Showing
4 changed files
with
88 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -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 | ||
|
||
/** | ||
|
@@ -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]; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
} |