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
Nam Chu Hoai 2 years ago committed by GitHub
parent 216f579975
commit bb01859bad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 135
      solidity/contracts/middleware/token-bridge/TokenBridgeRouter.sol
  2. 248
      solidity/contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol
  3. 18
      solidity/contracts/middleware/token-bridge/interfaces/ITokenBridgeAdapter.sol
  4. 12
      solidity/contracts/middleware/token-bridge/interfaces/ITokenBridgeMessageRecipient.sol
  5. 59
      solidity/contracts/middleware/token-bridge/interfaces/circle/ICircleBridge.sol
  6. 38
      solidity/contracts/middleware/token-bridge/interfaces/circle/ICircleMessageTransmitter.sol
  7. 14
      solidity/contracts/mock/MockToken.sol
  8. 46
      solidity/contracts/mock/MockTokenBridgeAdapter.sol
  9. 24
      solidity/contracts/test/TestTokenBridgeMessageRecipient.sol
  10. 44
      solidity/contracts/test/TestTokenRecipient.sol
  11. 2
      solidity/test/InterchainAccountRouter.t.sol
  12. 39
      solidity/test/MockHyperlaneEnvironment.sol
  13. 190
      solidity/test/TokenBridgeRouter.t.sol

@ -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;
}
}

@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;
import "forge-std/Test.sol";

@ -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…
Cancel
Save