TokenBridgeRouter and CircleBridgeAdapter (#1212)
* Compiles * nit * Add initializer * Add some setters * Got it working * Don't track nonces in contract state * Nit * Starting tests * Tests * Use actual token behavior in mocktoken * Complete MockTokenBridgeAdapter * Fix up * fix * Fix test * Remove relayer * PR review * PR review Co-authored-by: Trevor Porter <trkporter@ucdavis.edu>0.5.3
parent
216f579975
commit
bb01859bad
@ -0,0 +1,135 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {Router} from "../../Router.sol"; |
||||
|
||||
import {IMessageRecipient} from "../../../interfaces/IMessageRecipient.sol"; |
||||
import {ICircleBridge} from "./interfaces/circle/ICircleBridge.sol"; |
||||
import {ICircleMessageTransmitter} from "./interfaces/circle/ICircleMessageTransmitter.sol"; |
||||
import {ITokenBridgeAdapter} from "./interfaces/ITokenBridgeAdapter.sol"; |
||||
import {ITokenBridgeMessageRecipient} from "./interfaces/ITokenBridgeMessageRecipient.sol"; |
||||
|
||||
import {TypeCasts} from "../../libs/TypeCasts.sol"; |
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
||||
|
||||
contract TokenBridgeRouter is Router { |
||||
// Token bridge => adapter address |
||||
mapping(string => address) public tokenBridgeAdapters; |
||||
|
||||
event TokenBridgeAdapterSet(string indexed bridge, address adapter); |
||||
|
||||
function initialize( |
||||
address _owner, |
||||
address _abacusConnectionManager, |
||||
address _interchainGasPaymaster |
||||
) public initializer { |
||||
// Transfer ownership of the contract to deployer |
||||
_transferOwnership(_owner); |
||||
// Set the addresses for the ACM and IGP |
||||
// Alternatively, this could be done later in an initialize method |
||||
_setAbacusConnectionManager(_abacusConnectionManager); |
||||
_setInterchainGasPaymaster(_interchainGasPaymaster); |
||||
} |
||||
|
||||
function dispatchWithTokens( |
||||
uint32 _destinationDomain, |
||||
bytes32 _recipientAddress, |
||||
bytes calldata _messageBody, |
||||
address _token, |
||||
uint256 _amount, |
||||
string calldata _bridge |
||||
) external payable { |
||||
ITokenBridgeAdapter _adapter = _getAdapter(_bridge); |
||||
|
||||
// Transfer the tokens to the adapter |
||||
// TODO: use safeTransferFrom |
||||
// TODO: Are there scenarios where a transferFrom fails and it doesn't revert? |
||||
require( |
||||
IERC20(_token).transferFrom(msg.sender, address(_adapter), _amount), |
||||
"!transfer in" |
||||
); |
||||
|
||||
// Reverts if the bridge was unsuccessful. |
||||
// Gets adapter-specific data that is encoded into the message |
||||
// ultimately sent via Hyperlane. |
||||
bytes memory _adapterData = _adapter.sendTokens( |
||||
_destinationDomain, |
||||
_recipientAddress, |
||||
_token, |
||||
_amount |
||||
); |
||||
|
||||
// The user's message "wrapped" with metadata required by this middleware |
||||
bytes memory _messageWithMetadata = abi.encode( |
||||
TypeCasts.addressToBytes32(msg.sender), |
||||
_recipientAddress, // The "user" recipient |
||||
_amount, // The amount of the tokens sent over the bridge |
||||
_bridge, // The destination token bridge ID |
||||
_adapterData, // The adapter-specific data |
||||
_messageBody // The "user" message |
||||
); |
||||
|
||||
// Dispatch the _messageWithMetadata to the destination's TokenBridgeRouter. |
||||
_dispatchWithGas(_destinationDomain, _messageWithMetadata, msg.value); |
||||
} |
||||
|
||||
// Handles a message from an enrolled remote TokenBridgeRouter |
||||
function _handle( |
||||
uint32 _origin, |
||||
bytes32, // _sender, unused |
||||
bytes calldata _message |
||||
) internal override { |
||||
// Decode the message with metadata, "unwrapping" the user's message body |
||||
( |
||||
bytes32 _originalSender, |
||||
bytes32 _userRecipientAddress, |
||||
uint256 _amount, |
||||
string memory _bridge, |
||||
bytes memory _adapterData, |
||||
bytes memory _userMessageBody |
||||
) = abi.decode( |
||||
_message, |
||||
(bytes32, bytes32, uint256, string, bytes, bytes) |
||||
); |
||||
|
||||
ITokenBridgeMessageRecipient _userRecipient = ITokenBridgeMessageRecipient( |
||||
TypeCasts.bytes32ToAddress(_userRecipientAddress) |
||||
); |
||||
|
||||
// Reverts if the adapter hasn't received the bridged tokens yet |
||||
(address _token, uint256 _receivedAmount) = _getAdapter(_bridge) |
||||
.receiveTokens( |
||||
_origin, |
||||
address(_userRecipient), |
||||
_amount, |
||||
_adapterData |
||||
); |
||||
|
||||
_userRecipient.handleWithTokens( |
||||
_origin, |
||||
_originalSender, |
||||
_userMessageBody, |
||||
_token, |
||||
_receivedAmount |
||||
); |
||||
} |
||||
|
||||
function setTokenBridgeAdapter(string calldata _bridge, address _adapter) |
||||
external |
||||
onlyOwner |
||||
{ |
||||
tokenBridgeAdapters[_bridge] = _adapter; |
||||
emit TokenBridgeAdapterSet(_bridge, _adapter); |
||||
} |
||||
|
||||
function _getAdapter(string memory _bridge) |
||||
internal |
||||
view |
||||
returns (ITokenBridgeAdapter _adapter) |
||||
{ |
||||
_adapter = ITokenBridgeAdapter(tokenBridgeAdapters[_bridge]); |
||||
// Require the adapter to have been set |
||||
require(address(_adapter) != address(0), "No adapter found for bridge"); |
||||
} |
||||
} |
@ -0,0 +1,248 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {Router} from "../../../Router.sol"; |
||||
|
||||
import {ICircleBridge} from "../interfaces/circle/ICircleBridge.sol"; |
||||
import {ICircleMessageTransmitter} from "../interfaces/circle/ICircleMessageTransmitter.sol"; |
||||
import {ITokenBridgeAdapter} from "../interfaces/ITokenBridgeAdapter.sol"; |
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
||||
|
||||
contract CircleBridgeAdapter is ITokenBridgeAdapter, Router { |
||||
/// @notice The CircleBridge contract. |
||||
ICircleBridge public circleBridge; |
||||
|
||||
/// @notice The Circle MessageTransmitter contract. |
||||
ICircleMessageTransmitter public circleMessageTransmitter; |
||||
|
||||
/// @notice The TokenBridgeRouter contract. |
||||
address public tokenBridgeRouter; |
||||
|
||||
/// @notice Hyperlane domain => Circle domain. |
||||
/// ATM, known Circle domains are Ethereum = 0 and Avalanche = 1. |
||||
/// Note this could result in ambiguity between the Circle domain being |
||||
/// Ethereum or unknown. TODO fix? |
||||
mapping(uint32 => uint32) public hyperlaneDomainToCircleDomain; |
||||
|
||||
/// @notice Token symbol => address of token on local chain. |
||||
mapping(string => IERC20) public tokenSymbolToAddress; |
||||
|
||||
/// @notice Local chain token address => token symbol. |
||||
mapping(address => string) public tokenAddressToSymbol; |
||||
|
||||
/** |
||||
* @notice Emits the nonce of the Circle message when a token is bridged. |
||||
* @param nonce The nonce of the Circle message. |
||||
*/ |
||||
event BridgedToken(uint64 nonce); |
||||
|
||||
/** |
||||
* @notice Emitted when the Hyperlane domain to Circle domain mapping is updated. |
||||
* @param hyperlaneDomain The Hyperlane domain. |
||||
* @param circleDomain The Circle domain. |
||||
*/ |
||||
event DomainAdded(uint32 indexed hyperlaneDomain, uint32 circleDomain); |
||||
|
||||
/** |
||||
* @notice Emitted when a local token and its token symbol have been added. |
||||
*/ |
||||
event TokenAdded(address indexed token, string indexed symbol); |
||||
|
||||
/** |
||||
* @notice Emitted when a local token and its token symbol have been removed. |
||||
*/ |
||||
event TokenRemoved(address indexed token, string indexed symbol); |
||||
|
||||
modifier onlyTokenBridgeRouter() { |
||||
require(msg.sender == tokenBridgeRouter, "!tokenBridgeRouter"); |
||||
_; |
||||
} |
||||
|
||||
/** |
||||
* @param _owner The new owner. |
||||
* @param _circleBridge The CircleBridge contract. |
||||
* @param _circleMessageTransmitter The Circle MessageTransmitter contract. |
||||
* @param _tokenBridgeRouter The TokenBridgeRouter contract. |
||||
*/ |
||||
function initialize( |
||||
address _owner, |
||||
address _circleBridge, |
||||
address _circleMessageTransmitter, |
||||
address _tokenBridgeRouter |
||||
) public initializer { |
||||
// Transfer ownership of the contract to deployer |
||||
_transferOwnership(_owner); |
||||
|
||||
// Set the addresses for the ACM and IGP to address(0) - they aren't used. |
||||
_setAbacusConnectionManager(address(0)); |
||||
_setInterchainGasPaymaster(address(0)); |
||||
|
||||
circleBridge = ICircleBridge(_circleBridge); |
||||
circleMessageTransmitter = ICircleMessageTransmitter( |
||||
_circleMessageTransmitter |
||||
); |
||||
tokenBridgeRouter = _tokenBridgeRouter; |
||||
} |
||||
|
||||
function sendTokens( |
||||
uint32 _destinationDomain, |
||||
bytes32, // _recipientAddress, unused |
||||
address _token, |
||||
uint256 _amount |
||||
) external onlyTokenBridgeRouter returns (bytes memory) { |
||||
string memory _tokenSymbol = tokenAddressToSymbol[_token]; |
||||
require( |
||||
bytes(_tokenSymbol).length > 0, |
||||
"CircleBridgeAdapter: Unknown token" |
||||
); |
||||
|
||||
uint32 _circleDomain = hyperlaneDomainToCircleDomain[ |
||||
_destinationDomain |
||||
]; |
||||
bytes32 _remoteRouter = routers[_destinationDomain]; |
||||
require( |
||||
_remoteRouter != bytes32(0), |
||||
"CircleBridgeAdapter: No router for domain" |
||||
); |
||||
|
||||
// Approve the token to Circle. We assume that the TokenBridgeRouter |
||||
// has already transferred the token to this contract. |
||||
require( |
||||
IERC20(_token).approve(address(circleBridge), _amount), |
||||
"!approval" |
||||
); |
||||
|
||||
uint64 _nonce = circleBridge.depositForBurn( |
||||
_amount, |
||||
_circleDomain, |
||||
_remoteRouter, // Mint to the remote router |
||||
_token |
||||
); |
||||
|
||||
emit BridgedToken(_nonce); |
||||
|
||||
return abi.encodePacked(_nonce, _tokenSymbol); |
||||
} |
||||
|
||||
// Returns the token and amount sent |
||||
function receiveTokens( |
||||
uint32 _originDomain, // Hyperlane domain |
||||
address _recipient, |
||||
uint256 _amount, |
||||
bytes calldata _adapterData // The adapter data from the message |
||||
) external onlyTokenBridgeRouter returns (address, uint256) { |
||||
// The origin Circle domain |
||||
uint32 _originCircleDomain = hyperlaneDomainToCircleDomain[ |
||||
_originDomain |
||||
]; |
||||
// Get the token symbol and nonce of the transfer from the _adapterData |
||||
(uint64 _nonce, string memory _tokenSymbol) = abi.decode( |
||||
_adapterData, |
||||
(uint64, string) |
||||
); |
||||
|
||||
// Require the circle message to have been processed |
||||
bytes32 _nonceId = _circleNonceId(_originCircleDomain, _nonce); |
||||
require( |
||||
circleMessageTransmitter.usedNonces(_nonceId), |
||||
"Circle message not processed yet" |
||||
); |
||||
|
||||
IERC20 _token = tokenSymbolToAddress[_tokenSymbol]; |
||||
require( |
||||
address(_token) != address(0), |
||||
"CircleBridgeAdapter: Unknown token" |
||||
); |
||||
|
||||
// Transfer the token out to the recipient |
||||
// TODO: use safeTransfer |
||||
// Circle doesn't charge any fee, so we can safely transfer out the |
||||
// exact amount that was bridged over. |
||||
require(_token.transfer(_recipient, _amount), "!transfer out"); |
||||
|
||||
return (address(_token), _amount); |
||||
} |
||||
|
||||
// This contract is only a Router to be aware of remote router addresses, |
||||
// and doesn't actually send/handle Hyperlane messages directly |
||||
function _handle( |
||||
uint32, // origin |
||||
bytes32, // sender |
||||
bytes calldata // message |
||||
) internal pure override { |
||||
revert("No messages expected"); |
||||
} |
||||
|
||||
function addDomain(uint32 _hyperlaneDomain, uint32 _circleDomain) |
||||
external |
||||
onlyOwner |
||||
{ |
||||
hyperlaneDomainToCircleDomain[_hyperlaneDomain] = _circleDomain; |
||||
|
||||
emit DomainAdded(_hyperlaneDomain, _circleDomain); |
||||
} |
||||
|
||||
function addToken(address _token, string calldata _tokenSymbol) |
||||
external |
||||
onlyOwner |
||||
{ |
||||
require( |
||||
_token != address(0) && bytes(_tokenSymbol).length > 0, |
||||
"Cannot add default values" |
||||
); |
||||
|
||||
// Require the token and token symbol to be unset. |
||||
address _existingToken = address(tokenSymbolToAddress[_tokenSymbol]); |
||||
require(_existingToken == address(0), "token symbol already has token"); |
||||
|
||||
string memory _existingSymbol = tokenAddressToSymbol[_token]; |
||||
require( |
||||
bytes(_existingSymbol).length == 0, |
||||
"token already has token symbol" |
||||
); |
||||
|
||||
tokenAddressToSymbol[_token] = _tokenSymbol; |
||||
tokenSymbolToAddress[_tokenSymbol] = IERC20(_token); |
||||
|
||||
emit TokenAdded(_token, _tokenSymbol); |
||||
} |
||||
|
||||
function removeToken(address _token, string calldata _tokenSymbol) |
||||
external |
||||
onlyOwner |
||||
{ |
||||
// Require the provided token and token symbols match what's in storage. |
||||
address _existingToken = address(tokenSymbolToAddress[_tokenSymbol]); |
||||
require(_existingToken == _token, "Token mismatch"); |
||||
|
||||
string memory _existingSymbol = tokenAddressToSymbol[_token]; |
||||
require( |
||||
keccak256(bytes(_existingSymbol)) == keccak256(bytes(_tokenSymbol)), |
||||
"Token symbol mismatch" |
||||
); |
||||
|
||||
// Delete them from storage. |
||||
delete tokenSymbolToAddress[_tokenSymbol]; |
||||
delete tokenAddressToSymbol[_token]; |
||||
|
||||
emit TokenRemoved(_token, _tokenSymbol); |
||||
} |
||||
|
||||
/** |
||||
* @notice Gets the Circle nonce ID by hashing _originCircleDomain and _nonce. |
||||
* @param _originCircleDomain Domain of chain where the transfer originated |
||||
* @param _nonce The unique identifier for the message from source to |
||||
destination |
||||
* @return hash of source and nonce |
||||
*/ |
||||
function _circleNonceId(uint32 _originCircleDomain, uint64 _nonce) |
||||
internal |
||||
pure |
||||
returns (bytes32) |
||||
{ |
||||
// The hash is of a uint256 nonce, not a uint64 one. |
||||
return |
||||
keccak256(abi.encodePacked(_originCircleDomain, uint256(_nonce))); |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
interface ITokenBridgeAdapter { |
||||
function sendTokens( |
||||
uint32 _destinationDomain, |
||||
bytes32 _recipientAddress, |
||||
address _token, |
||||
uint256 _amount |
||||
) external returns (bytes memory _adapterData); |
||||
|
||||
function receiveTokens( |
||||
uint32 _originDomain, // Hyperlane domain |
||||
address _recipientAddress, |
||||
uint256 _amount, |
||||
bytes calldata _adapterData // The adapter data from the message |
||||
) external returns (address, uint256); |
||||
} |
@ -0,0 +1,12 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
interface ITokenBridgeMessageRecipient { |
||||
function handleWithTokens( |
||||
uint32 _origin, |
||||
bytes32 _sender, |
||||
bytes calldata _message, |
||||
address _token, |
||||
uint256 _amount |
||||
) external; |
||||
} |
@ -0,0 +1,59 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
interface ICircleBridge { |
||||
event MessageSent(bytes message); |
||||
|
||||
/** |
||||
* @notice Deposits and burns tokens from sender to be minted on destination domain. |
||||
* Emits a `DepositForBurn` event. |
||||
* @dev reverts if: |
||||
* - given burnToken is not supported |
||||
* - given destinationDomain has no CircleBridge registered |
||||
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance |
||||
* to this contract is less than `amount`. |
||||
* - burn() reverts. For example, if `amount` is 0. |
||||
* - MessageTransmitter returns false or reverts. |
||||
* @param _amount amount of tokens to burn |
||||
* @param _destinationDomain destination domain (ETH = 0, AVAX = 1) |
||||
* @param _mintRecipient address of mint recipient on destination domain |
||||
* @param _burnToken address of contract to burn deposited tokens, on local domain |
||||
* @return _nonce unique nonce reserved by message |
||||
*/ |
||||
function depositForBurn( |
||||
uint256 _amount, |
||||
uint32 _destinationDomain, |
||||
bytes32 _mintRecipient, |
||||
address _burnToken |
||||
) external returns (uint64 _nonce); |
||||
|
||||
/** |
||||
* @notice Deposits and burns tokens from sender to be minted on destination domain. The mint |
||||
* on the destination domain must be called by `_destinationCaller`. |
||||
* WARNING: if the `_destinationCaller` does not represent a valid address as bytes32, then it will not be possible |
||||
* to broadcast the message on the destination domain. This is an advanced feature, and the standard |
||||
* depositForBurn() should be preferred for use cases where a specific destination caller is not required. |
||||
* Emits a `DepositForBurn` event. |
||||
* @dev reverts if: |
||||
* - given destinationCaller is zero address |
||||
* - given burnToken is not supported |
||||
* - given destinationDomain has no CircleBridge registered |
||||
* - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance |
||||
* to this contract is less than `amount`. |
||||
* - burn() reverts. For example, if `amount` is 0. |
||||
* - MessageTransmitter returns false or reverts. |
||||
* @param _amount amount of tokens to burn |
||||
* @param _destinationDomain destination domain |
||||
* @param _mintRecipient address of mint recipient on destination domain |
||||
* @param _burnToken address of contract to burn deposited tokens, on local domain |
||||
* @param _destinationCaller caller on the destination domain, as bytes32 |
||||
* @return _nonce unique nonce reserved by message |
||||
*/ |
||||
function depositForBurnWithCaller( |
||||
uint256 _amount, |
||||
uint32 _destinationDomain, |
||||
bytes32 _mintRecipient, |
||||
address _burnToken, |
||||
bytes32 _destinationCaller |
||||
) external returns (uint64 _nonce); |
||||
} |
@ -0,0 +1,38 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
interface ICircleMessageTransmitter { |
||||
/** |
||||
* @notice Receive a message. Messages with a given nonce |
||||
* can only be broadcast once for a (sourceDomain, destinationDomain) |
||||
* pair. The message body of a valid message is passed to the |
||||
* specified recipient for further processing. |
||||
* |
||||
* @dev Attestation format: |
||||
* A valid attestation is the concatenated 65-byte signature(s) of exactly |
||||
* `thresholdSignature` signatures, in increasing order of attester address. |
||||
* ***If the attester addresses recovered from signatures are not in |
||||
* increasing order, signature verification will fail.*** |
||||
* If incorrect number of signatures or duplicate signatures are supplied, |
||||
* signature verification will fail. |
||||
* |
||||
* Message format: |
||||
* Field Bytes Type Index |
||||
* version 4 uint32 0 |
||||
* sourceDomain 4 uint32 4 |
||||
* destinationDomain 4 uint32 8 |
||||
* nonce 8 uint64 12 |
||||
* sender 32 bytes32 20 |
||||
* recipient 32 bytes32 52 |
||||
* messageBody dynamic bytes 84 |
||||
* @param _message Message bytes |
||||
* @param _attestation Concatenated 65-byte signature(s) of `_message`, in increasing order |
||||
* of the attester address recovered from signatures. |
||||
* @return success bool, true if successful |
||||
*/ |
||||
function receiveMessage(bytes memory _message, bytes calldata _attestation) |
||||
external |
||||
returns (bool success); |
||||
|
||||
function usedNonces(bytes32 _nonceId) external view returns (bool); |
||||
} |
@ -0,0 +1,14 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; |
||||
|
||||
contract MockToken is ERC20Upgradeable { |
||||
function mint(address account, uint256 amount) external { |
||||
_mint(account, amount); |
||||
} |
||||
|
||||
function burn(uint256 _amount) external { |
||||
_burn(msg.sender, _amount); |
||||
} |
||||
} |
@ -0,0 +1,46 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {ITokenBridgeAdapter} from "../middleware/token-bridge/interfaces/ITokenBridgeAdapter.sol"; |
||||
import {MockToken} from "./MockToken.sol"; |
||||
|
||||
contract MockTokenBridgeAdapter is ITokenBridgeAdapter { |
||||
uint256 public nonce = 0; |
||||
MockToken token; |
||||
|
||||
mapping(uint256 => bool) public isProcessed; |
||||
|
||||
constructor(MockToken _token) { |
||||
token = _token; |
||||
} |
||||
|
||||
function sendTokens( |
||||
uint32, |
||||
bytes32, |
||||
address _token, |
||||
uint256 _amount |
||||
) external override returns (bytes memory _adapterData) { |
||||
require(_token == address(token), "cant bridge this token"); |
||||
token.burn(_amount); |
||||
nonce = nonce + 1; |
||||
return abi.encode(nonce); |
||||
} |
||||
|
||||
function process(uint256 _nonce) public { |
||||
isProcessed[_nonce] = true; |
||||
} |
||||
|
||||
function receiveTokens( |
||||
uint32 _originDomain, // Hyperlane domain |
||||
address _recipientAddress, |
||||
uint256 _amount, |
||||
bytes calldata _adapterData // The adapter data from the message |
||||
) external override returns (address, uint256) { |
||||
_originDomain; |
||||
uint256 _nonce = abi.decode(_adapterData, (uint256)); |
||||
// Check if the transfer was processed first |
||||
require(isProcessed[_nonce], "Transfer has not been processed yet"); |
||||
token.mint(_recipientAddress, _amount); |
||||
return (address(0), 0); |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import {ITokenBridgeMessageRecipient} from "../middleware/token-bridge/interfaces/ITokenBridgeMessageRecipient.sol"; |
||||
|
||||
contract TestTokenBridgeMessageRecipient is ITokenBridgeMessageRecipient { |
||||
event HandledWithTokens( |
||||
uint32 origin, |
||||
bytes32 sender, |
||||
bytes message, |
||||
address token, |
||||
uint256 amount |
||||
); |
||||
|
||||
function handleWithTokens( |
||||
uint32 _origin, |
||||
bytes32 _sender, |
||||
bytes calldata _message, |
||||
address _token, |
||||
uint256 _amount |
||||
) external { |
||||
emit HandledWithTokens(_origin, _sender, _message, _token, _amount); |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.8.0; |
||||
|
||||
import {ITokenBridgeMessageRecipient} from "../middleware/token-bridge/interfaces/ITokenBridgeMessageRecipient.sol"; |
||||
|
||||
contract TestTokenRecipient is ITokenBridgeMessageRecipient { |
||||
bytes32 public lastSender; |
||||
bytes public lastData; |
||||
address public lastToken; |
||||
uint256 public lastAmount; |
||||
|
||||
address public lastCaller; |
||||
string public lastCallMessage; |
||||
|
||||
event ReceivedMessage( |
||||
uint32 indexed origin, |
||||
bytes32 indexed sender, |
||||
string message, |
||||
address token, |
||||
uint256 amount |
||||
); |
||||
|
||||
event ReceivedCall(address indexed caller, uint256 amount, string message); |
||||
|
||||
function handleWithTokens( |
||||
uint32 _origin, |
||||
bytes32 _sender, |
||||
bytes calldata _data, |
||||
address _token, |
||||
uint256 _amount |
||||
) external override { |
||||
emit ReceivedMessage(_origin, _sender, string(_data), _token, _amount); |
||||
lastSender = _sender; |
||||
lastData = _data; |
||||
lastToken = _token; |
||||
lastAmount = _amount; |
||||
} |
||||
|
||||
function fooBar(uint256 amount, string calldata message) external { |
||||
emit ReceivedCall(msg.sender, amount, message); |
||||
lastCaller = msg.sender; |
||||
lastCallMessage = message; |
||||
} |
||||
} |
@ -0,0 +1,39 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import "../contracts/mock/MockOutbox.sol"; |
||||
import "../contracts/mock/MockInbox.sol"; |
||||
import "../contracts/AbacusConnectionManager.sol"; |
||||
|
||||
contract MockHyperlaneEnvironment { |
||||
MockOutbox public outbox; |
||||
MockInbox public inbox; |
||||
|
||||
mapping(uint32 => AbacusConnectionManager) connectionManagers; |
||||
|
||||
constructor(uint32 _originDomain, uint32 _destinationDomain) { |
||||
inbox = new MockInbox(); |
||||
outbox = new MockOutbox(_originDomain, address(inbox)); |
||||
|
||||
AbacusConnectionManager originManager = new AbacusConnectionManager(); |
||||
AbacusConnectionManager destinationManager = new AbacusConnectionManager(); |
||||
|
||||
originManager.setOutbox(address(outbox)); |
||||
destinationManager.enrollInbox(_destinationDomain, address(inbox)); |
||||
|
||||
connectionManagers[_originDomain] = originManager; |
||||
connectionManagers[_destinationDomain] = destinationManager; |
||||
} |
||||
|
||||
function connectionManager(uint32 _domain) |
||||
public |
||||
view |
||||
returns (AbacusConnectionManager) |
||||
{ |
||||
return connectionManagers[_domain]; |
||||
} |
||||
|
||||
function processNextPendingMessage() public { |
||||
inbox.processNextPendingMessage(); |
||||
} |
||||
} |
@ -0,0 +1,190 @@ |
||||
// SPDX-License-Identifier: Apache-2.0 |
||||
pragma solidity ^0.8.13; |
||||
|
||||
import "forge-std/Test.sol"; |
||||
import {TokenBridgeRouter} from "../contracts/middleware/token-bridge/TokenBridgeRouter.sol"; |
||||
import {MockToken} from "../contracts/mock/MockToken.sol"; |
||||
import {TestTokenRecipient} from "../contracts/test/TestTokenRecipient.sol"; |
||||
import {MockTokenBridgeAdapter} from "../contracts/mock/MockTokenBridgeAdapter.sol"; |
||||
import {MockHyperlaneEnvironment} from "./MockHyperlaneEnvironment.sol"; |
||||
|
||||
import {TypeCasts} from "../contracts/libs/TypeCasts.sol"; |
||||
|
||||
contract TokenBridgeRouterTest is Test { |
||||
MockHyperlaneEnvironment testEnvironment; |
||||
|
||||
TokenBridgeRouter originTokenBridgeRouter; |
||||
TokenBridgeRouter destinationTokenBridgeRouter; |
||||
|
||||
// Origin bridge adapter |
||||
MockTokenBridgeAdapter bridgeAdapter; |
||||
string bridge = "FooBridge"; |
||||
|
||||
uint32 originDomain = 123; |
||||
uint32 destinationDomain = 321; |
||||
|
||||
TestTokenRecipient recipient; |
||||
MockToken token; |
||||
bytes messageBody = hex"beefdead"; |
||||
uint256 amount = 420000; |
||||
|
||||
event TokenBridgeAdapterSet(string indexed bridge, address adapter); |
||||
|
||||
function setUp() public { |
||||
token = new MockToken(); |
||||
bridgeAdapter = new MockTokenBridgeAdapter(token); |
||||
recipient = new TestTokenRecipient(); |
||||
|
||||
originTokenBridgeRouter = new TokenBridgeRouter(); |
||||
destinationTokenBridgeRouter = new TokenBridgeRouter(); |
||||
|
||||
testEnvironment = new MockHyperlaneEnvironment( |
||||
originDomain, |
||||
destinationDomain |
||||
); |
||||
|
||||
// TODO: set IGP? |
||||
originTokenBridgeRouter.initialize( |
||||
address(this), |
||||
address(testEnvironment.connectionManager(originDomain)), |
||||
address(0) |
||||
); |
||||
destinationTokenBridgeRouter.initialize( |
||||
address(this), |
||||
address(testEnvironment.connectionManager(destinationDomain)), |
||||
address(0) |
||||
); |
||||
|
||||
originTokenBridgeRouter.enrollRemoteRouter( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(destinationTokenBridgeRouter)) |
||||
); |
||||
destinationTokenBridgeRouter.enrollRemoteRouter( |
||||
originDomain, |
||||
TypeCasts.addressToBytes32(address(originTokenBridgeRouter)) |
||||
); |
||||
|
||||
originTokenBridgeRouter.setTokenBridgeAdapter( |
||||
bridge, |
||||
address(bridgeAdapter) |
||||
); |
||||
|
||||
destinationTokenBridgeRouter.setTokenBridgeAdapter( |
||||
bridge, |
||||
address(bridgeAdapter) |
||||
); |
||||
|
||||
token.mint(address(this), amount); |
||||
} |
||||
|
||||
function testSetTokenBridgeAdapter() public { |
||||
// Expect the TokenBridgeAdapterSet event. |
||||
// Expect topic0 & data to match |
||||
vm.expectEmit(true, false, false, true); |
||||
emit TokenBridgeAdapterSet(bridge, address(bridgeAdapter)); |
||||
|
||||
// Set the token bridge adapter |
||||
originTokenBridgeRouter.setTokenBridgeAdapter( |
||||
bridge, |
||||
address(bridgeAdapter) |
||||
); |
||||
|
||||
// Expect the bridge adapter to have been set |
||||
assertEq( |
||||
originTokenBridgeRouter.tokenBridgeAdapters(bridge), |
||||
address(bridgeAdapter) |
||||
); |
||||
} |
||||
|
||||
// ==== dispatchWithTokens ==== |
||||
|
||||
function testDispatchWithTokensRevertsWithUnkownBridgeAdapter() public { |
||||
vm.expectRevert("No adapter found for bridge"); |
||||
originTokenBridgeRouter.dispatchWithTokens( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
messageBody, |
||||
address(token), |
||||
amount, |
||||
"BazBridge" // some unknown bridge name |
||||
); |
||||
} |
||||
|
||||
function testDispatchWithTokensRevertsWithFailedTransferIn() public { |
||||
vm.expectRevert("ERC20: insufficient allowance"); |
||||
originTokenBridgeRouter.dispatchWithTokens( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
messageBody, |
||||
address(token), |
||||
amount, |
||||
bridge |
||||
); |
||||
} |
||||
|
||||
function testDispatchWithTokenTransfersMovesTokens() public { |
||||
token.approve(address(originTokenBridgeRouter), amount); |
||||
originTokenBridgeRouter.dispatchWithTokens( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
messageBody, |
||||
address(token), |
||||
amount, |
||||
bridge |
||||
); |
||||
} |
||||
|
||||
function testDispatchWithTokensCallsAdapter() public { |
||||
vm.expectCall( |
||||
address(bridgeAdapter), |
||||
abi.encodeWithSelector( |
||||
bridgeAdapter.sendTokens.selector, |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
address(token), |
||||
amount |
||||
) |
||||
); |
||||
token.approve(address(originTokenBridgeRouter), amount); |
||||
originTokenBridgeRouter.dispatchWithTokens( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
messageBody, |
||||
address(token), |
||||
amount, |
||||
bridge |
||||
); |
||||
} |
||||
|
||||
function testProcessingRevertsIfBridgeAdapterReverts() public { |
||||
token.approve(address(originTokenBridgeRouter), amount); |
||||
originTokenBridgeRouter.dispatchWithTokens( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
messageBody, |
||||
address(token), |
||||
amount, |
||||
bridge |
||||
); |
||||
|
||||
vm.expectRevert("Transfer has not been processed yet"); |
||||
testEnvironment.processNextPendingMessage(); |
||||
} |
||||
|
||||
function testDispatchWithTokensTransfersOnDestination() public { |
||||
token.approve(address(originTokenBridgeRouter), amount); |
||||
originTokenBridgeRouter.dispatchWithTokens( |
||||
destinationDomain, |
||||
TypeCasts.addressToBytes32(address(recipient)), |
||||
messageBody, |
||||
address(token), |
||||
amount, |
||||
bridge |
||||
); |
||||
|
||||
bridgeAdapter.process(bridgeAdapter.nonce()); |
||||
testEnvironment.processNextPendingMessage(); |
||||
assertEq(recipient.lastData(), messageBody); |
||||
assertEq(token.balanceOf(address(recipient)), amount); |
||||
} |
||||
} |
Loading…
Reference in new issue