Skip to content

feat: support OFT's and SpokePoolPeriphery #993

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

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
name: evm-artifacts-${{ runner.os }}-node-${{ env.NODE_VERSION }}
- name: Test evm-hardhat
shell: bash
run: yarn test-evm
run: yarn test-evm-hardhat
test-svm-verified:
name: Test verified SVM build
needs: upload-svm-artifacts
Expand Down Expand Up @@ -208,4 +208,4 @@ jobs:
- name: Inspect storage layouts
run: ./scripts/checkStorageLayout.sh
- name: Test evm-foundry
run: forge test --match-path test/evm/foundry/local/**/*.t.sol
run: yarn test-evm-foundry
65 changes: 65 additions & 0 deletions contracts/AdapterStore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

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

library MessengerTypes {
bytes32 public constant OFT_MESSENGER = bytes32("OFT_MESSENGER");
}

/**
* @dev A helper contract for chain adapters on the hub chain that support OFT or xERC20(via Hyperlane) messaging.
* @dev Handles token => messenger/router mapping storage. Adapters can't store this themselves as they're called
* @dev via `delegateCall` and their storage is not part of available context.
*/
contract AdapterStore is Ownable {
// (messengerType, dstDomainId, srcChainToken) => messenger address
mapping(bytes32 => mapping(uint256 => mapping(address => address))) public crossChainMessengers;

event MessengerSet(
bytes32 indexed messengerType,
uint256 indexed dstDomainId,
address indexed srcChainToken,
address srcChainMessenger
);

error ArrayLengthMismatch();

function setMessenger(
bytes32 messengerType,
uint256 dstDomainId,
address srcChainToken,
address srcChainMessenger
) external onlyOwner {
_setMessenger(messengerType, dstDomainId, srcChainToken, srcChainMessenger);
}

function batchSetMessengers(
bytes32[] calldata messengerTypes,
uint256[] calldata dstDomainIds,
address[] calldata srcChainTokens,
address[] calldata srcChainMessengers
) external onlyOwner {
if (
messengerTypes.length != dstDomainIds.length ||
messengerTypes.length != srcChainTokens.length ||
messengerTypes.length != srcChainMessengers.length
) {
revert ArrayLengthMismatch();
}

for (uint256 i = 0; i < dstDomainIds.length; i++) {
_setMessenger(messengerTypes[i], dstDomainIds[i], srcChainTokens[i], srcChainMessengers[i]);
}
}

function _setMessenger(
bytes32 _messengerType,
uint256 _dstDomainId,
address _srcChainToken,
address _srcChainMessenger
) internal {
crossChainMessengers[_messengerType][_dstDomainId][_srcChainToken] = _srcChainMessenger;
emit MessengerSet(_messengerType, _dstDomainId, _srcChainToken, _srcChainMessenger);
}
}
8 changes: 6 additions & 2 deletions contracts/AlephZero_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ contract AlephZero_SpokePool is Arbitrum_SpokePool {
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer,
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
ITokenMessenger _cctpTokenMessenger,
uint32 _oftDstEid,
uint256 _oftFeeCap
)
Arbitrum_SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
_l2Usdc,
_cctpTokenMessenger
_cctpTokenMessenger,
_oftDstEid,
_oftFeeCap
)
{} // solhint-disable-line no-empty-blocks
}
15 changes: 13 additions & 2 deletions contracts/Arbitrum_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer,
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
ITokenMessenger _cctpTokenMessenger,
uint32 _oftDstEid,
uint256 _oftFeeCap
)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer, _oftDstEid, _oftFeeCap)
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
{} // solhint-disable-line no-empty-blocks

Expand Down Expand Up @@ -83,9 +85,13 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {
**************************************/

function _bridgeTokensToHubPool(uint256 amountToReturn, address l2TokenAddress) internal override {
address oftMessenger = _getOftMessenger(l2TokenAddress);

// If the l2TokenAddress is UDSC, we need to use the CCTP bridge.
if (_isCCTPEnabled() && l2TokenAddress == address(usdcToken)) {
_transferUsdc(withdrawalRecipient, amountToReturn);
} else if (oftMessenger != address(0)) {
_transferViaOFT(IERC20(l2TokenAddress), IOFT(oftMessenger), withdrawalRecipient, amountToReturn);
} else {
// Check that the Ethereum counterpart of the L2 token is stored on this contract.
address ethereumTokenToBridge = whitelistedTokens[l2TokenAddress];
Expand All @@ -112,4 +118,9 @@ contract Arbitrum_SpokePool is SpokePool, CircleCCTPAdapter {

// Apply AVM-specific transformation to cross domain admin address on L1.
function _requireAdminSender() internal override onlyFromCrossDomainAdmin {}

// Reserve storage slots for future versions of this base contract to add state variables without
// affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables
// are added. This is at bottom of contract to make sure it's always at the end of storage.
uint256[1000] private __gap;
}
36 changes: 36 additions & 0 deletions contracts/Create2Factory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol";
import { Lockable } from "./Lockable.sol";

/**
* @title Create2Factory
* @notice Deploys a new contract via create2 at a deterministic address and then atomically initializes the contract
* @dev Contracts designed to be deployed at deterministic addresses should initialize via a non-constructor
* initializer to maintain bytecode across different chains.
* @custom:security-contact [email protected]
*/
contract Create2Factory is Lockable {
/// @notice Emitted when the initialization to a newly deployed contract fails
error InitializationFailed();

/**
* @notice Deploys a new contract via create2 at a deterministic address and then atomically initializes the contract
* @param amount The amount of ETH to send with the deployment. If this is not zero then the contract must have a payable constructor
* @param salt The salt to use for the create2 deployment. Must not have been used before for the bytecode
* @param bytecode The bytecode of the contract to deploy
* @param initializationCode The initialization code to call on the deployed contract
*/
function deploy(
uint256 amount,
bytes32 salt,
bytes calldata bytecode,
bytes calldata initializationCode
) external nonReentrant returns (address) {
address deployedAddress = Create2.deploy(amount, salt, bytecode);
(bool success, ) = deployedAddress.call(initializationCode);
if (!success) revert InitializationFailed();
return deployedAddress;
}
}
11 changes: 10 additions & 1 deletion contracts/Ethereum_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ contract Ethereum_SpokePool is SpokePool, OwnableUpgradeable {
address _wrappedNativeTokenAddress,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
)
SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
// Ethereum_SpokePool does not use OFT messaging; setting destination eid and fee cap to 0
0,
0
)
{} // solhint-disable-line no-empty-blocks

/**
* @notice Construct the Ethereum SpokePool.
Expand Down
9 changes: 8 additions & 1 deletion contracts/Linea_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ contract Linea_SpokePool is SpokePool, CircleCCTPAdapter {
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
// Linea_SpokePool does not use OFT messaging; setting destination eid and fee cap to 0
0,
0
)
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
{} // solhint-disable-line no-empty-blocks

Expand Down
10 changes: 9 additions & 1 deletion contracts/Ovm_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface IL2ERC20Bridge {
*/
contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
using SafeERC20 for IERC20;

// "l1Gas" parameter used in call to bridge tokens from this contract back to L1 via IL2ERC20Bridge. Currently
// unused by bridge but included for future compatibility.
uint32 public l1Gas;
Expand Down Expand Up @@ -71,7 +72,14 @@ contract Ovm_SpokePool is SpokePool, CircleCCTPAdapter {
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
// Ovm_SpokePool does not use OFT messaging; setting destination eid and fee cap to 0
0,
0
)
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
{} // solhint-disable-line no-empty-blocks

Expand Down
11 changes: 10 additions & 1 deletion contracts/PolygonZkEVM_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,16 @@ contract PolygonZkEVM_SpokePool is SpokePool, IBridgeMessageReceiver {
address _wrappedNativeTokenAddress,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
)
SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
// PolygonZkEVM_SpokePool does not use OFT messaging; setting destination eid and fee cap to 0
0,
0
)
{} // solhint-disable-line no-empty-blocks

/**
* @notice Construct the Polygon zkEVM SpokePool.
Expand Down
9 changes: 8 additions & 1 deletion contracts/Polygon_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ contract Polygon_SpokePool is IFxMessageProcessor, SpokePool, CircleCCTPAdapter
IERC20 _l2Usdc,
ITokenMessenger _cctpTokenMessenger
)
SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer)
SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
// Polygon_SpokePool does not use OFT messaging; setting destination eid and fee cap to 0
0,
0
)
CircleCCTPAdapter(_l2Usdc, _cctpTokenMessenger, CircleDomainIds.Ethereum)
{} // solhint-disable-line no-empty-blocks

Expand Down
11 changes: 10 additions & 1 deletion contracts/Scroll_SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ contract Scroll_SpokePool is SpokePool {
address _wrappedNativeTokenAddress,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) SpokePool(_wrappedNativeTokenAddress, _depositQuoteTimeBuffer, _fillDeadlineBuffer) {} // solhint-disable-line no-empty-blocks
)
SpokePool(
_wrappedNativeTokenAddress,
_depositQuoteTimeBuffer,
_fillDeadlineBuffer,
// Scroll_SpokePool does not use OFT messaging, setting destination id and fee cap to 0
0,
0
)
{} // solhint-disable-line no-empty-blocks

/**
* @notice Construct the Scroll SpokePool.
Expand Down
41 changes: 37 additions & 4 deletions contracts/SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "./upgradeable/MultiCallerUpgradeable.sol";
import "./upgradeable/EIP712CrossChainUpgradeable.sol";
import "./upgradeable/AddressLibUpgradeable.sol";
import "./libraries/AddressConverters.sol";
import "./libraries/OFTTransportAdapter.sol";

import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
Expand All @@ -37,7 +38,8 @@ abstract contract SpokePool is
ReentrancyGuardUpgradeable,
MultiCallerUpgradeable,
EIP712CrossChainUpgradeable,
IDestinationSettler
IDestinationSettler,
OFTTransportAdapter
{
using SafeERC20Upgradeable for IERC20Upgradeable;
using AddressLibUpgradeable for address;
Expand Down Expand Up @@ -111,6 +113,9 @@ abstract contract SpokePool is
// reason (eg blacklist) to track their outstanding liability, thereby letting them claim it later.
mapping(address => mapping(address => uint256)) public relayerRefund;

// Mapping of L2 token address to L2 IOFT messenger address. Required to support bridging via OFT standard
mapping(address => address) public oftMessengers;

/**************************************************************
* CONSTANT/IMMUTABLE VARIABLES *
**************************************************************/
Expand Down Expand Up @@ -190,6 +195,9 @@ abstract contract SpokePool is
event EmergencyDeletedRootBundle(uint256 indexed rootBundleId);
event PausedDeposits(bool isPaused);
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
Expand All @@ -205,13 +213,17 @@ abstract contract SpokePool is
* into the past from the block time of the deposit.
* @param _fillDeadlineBuffer fillDeadlineBuffer to set. Fill deadlines can't be set more than this amount
* into the future from the block time of the deposit.
* @param _oftDstEid destination endpoint id for OFT messaging
* @param _oftFeeCap fee cap in native token when paying for cross-chain OFT transfers
*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
address _wrappedNativeTokenAddress,
uint32 _depositQuoteTimeBuffer,
uint32 _fillDeadlineBuffer
) {
uint32 _fillDeadlineBuffer,
uint32 _oftDstEid,
uint256 _oftFeeCap
) OFTTransportAdapter(_oftDstEid, _oftFeeCap) {
wrappedNativeToken = WETH9Interface(_wrappedNativeTokenAddress);
depositQuoteTimeBuffer = _depositQuoteTimeBuffer;
fillDeadlineBuffer = _fillDeadlineBuffer;
Expand Down Expand Up @@ -342,6 +354,15 @@ abstract contract SpokePool is
emit EmergencyDeletedRootBundle(rootBundleId);
}

/**
* @notice Add token -> OFTMessenger relationship. Callable only by admin.
* @param token token address on the current chain
* @param messenger IOFT contract address on the current chain for the specified token. Acts as a 'mailbox'
*/
function setOftMessenger(address token, address messenger) public onlyAdmin nonReentrant {
_setOftMessenger(token, messenger);
}

/**************************************
* LEGACY DEPOSITOR FUNCTIONS *
**************************************/
Expand Down Expand Up @@ -1715,6 +1736,18 @@ abstract contract SpokePool is
else return keccak256(message);
}

function _setOftMessenger(address _token, address _messenger) internal {
if (IOFT(_messenger).token() != _token) {
revert OFTTokenMismatch();
}
oftMessengers[_token] = _messenger;
emit SetOFTMessenger(_token, _messenger);
}

function _getOftMessenger(address _token) internal view returns (address) {
return oftMessengers[_token];
}

// Implementing contract needs to override this to ensure that only the appropriate cross chain admin can execute
// certain admin functions. For L2 contracts, the cross chain admin refers to some L1 address or contract, and for
// L1, this would just be the same admin of the HubPool.
Expand All @@ -1726,5 +1759,5 @@ abstract contract SpokePool is
// Reserve storage slots for future versions of this base contract to add state variables without
// affecting the storage layout of child contracts. Decrement the size of __gap whenever state variables
// are added. This is at bottom of contract to make sure it's always at the end of storage.
uint256[998] private __gap;
uint256[997] private __gap;
}
Loading
Loading