-
Notifications
You must be signed in to change notification settings - Fork 412
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
13 changed files
with
868 additions
and
1 deletion.
There are no files selected for viewing
135 changes: 135 additions & 0 deletions
135
solidity/contracts/middleware/token-bridge/TokenBridgeRouter.sol
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 |
---|---|---|
@@ -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"); | ||
} | ||
} |
248 changes: 248 additions & 0 deletions
248
solidity/contracts/middleware/token-bridge/adapters/CircleBridgeAdapter.sol
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 |
---|---|---|
@@ -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))); | ||
} | ||
} |
Oops, something went wrong.