Skip to content

Commit

Permalink
TokenBridgeRouter and CircleBridgeAdapter (#1212)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
nambrot and tkporter authored Nov 2, 2022
1 parent 216f579 commit bb01859
Show file tree
Hide file tree
Showing 13 changed files with 868 additions and 1 deletion.
135 changes: 135 additions & 0 deletions solidity/contracts/middleware/token-bridge/TokenBridgeRouter.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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)));
}
}
Loading

0 comments on commit bb01859

Please sign in to comment.