| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- // contracts/Bridge.sol
- // SPDX-License-Identifier: Apache 2
- pragma solidity ^0.8.0;
- import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
- import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
- import "../libraries/external/BytesLib.sol";
- import "./BridgeGetters.sol";
- import "./BridgeSetters.sol";
- import "./BridgeStructs.sol";
- import "./BridgeGovernance.sol";
- import "./token/Token.sol";
- import "./token/TokenImplementation.sol";
- contract Bridge is BridgeGovernance, ReentrancyGuard {
- using BytesLib for bytes;
- // Produce a AssetMeta message for a given token
- function attestToken(address tokenAddress, uint32 nonce) public payable returns (uint64 sequence){
- // decimals, symbol & token are not part of the core ERC20 token standard, so we need to support contracts that dont implement them
- (,bytes memory queriedDecimals) = tokenAddress.staticcall(abi.encodeWithSignature("decimals()"));
- (,bytes memory queriedSymbol) = tokenAddress.staticcall(abi.encodeWithSignature("symbol()"));
- (,bytes memory queriedName) = tokenAddress.staticcall(abi.encodeWithSignature("name()"));
- uint8 decimals = abi.decode(queriedDecimals, (uint8));
- string memory symbolString = abi.decode(queriedSymbol, (string));
- string memory nameString = abi.decode(queriedName, (string));
- bytes32 symbol;
- bytes32 name;
- assembly {
- // first 32 bytes hold string length
- symbol := mload(add(symbolString, 32))
- name := mload(add(nameString, 32))
- }
- BridgeStructs.AssetMeta memory meta = BridgeStructs.AssetMeta({
- payloadID : 2,
- // Address of the token. Left-zero-padded if shorter than 32 bytes
- tokenAddress : bytes32(uint256(uint160(tokenAddress))),
- // Chain ID of the token
- tokenChain : chainId(),
- // Number of decimals of the token (big-endian uint8)
- decimals : decimals,
- // Symbol of the token (UTF-8)
- symbol : symbol,
- // Name of the token (UTF-8)
- name : name
- });
- bytes memory encoded = encodeAssetMeta(meta);
- sequence = wormhole().publishMessage{
- value : msg.value
- }(nonce, encoded, 15);
- }
- function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable returns (uint64 sequence) {
- uint wormholeFee = wormhole().messageFee();
- require(wormholeFee < msg.value, "value is smaller than wormhole fee");
- uint amount = msg.value - wormholeFee;
- require(arbiterFee <= amount, "fee is bigger than amount minus wormhole fee");
- uint normalizedAmount = normalizeAmount(amount, 18);
- uint normalizedArbiterFee = normalizeAmount(arbiterFee, 18);
- // refund dust
- uint dust = amount - deNormalizeAmount(normalizedAmount, 18);
- if (dust > 0) {
- payable(msg.sender).transfer(dust);
- }
- // deposit into WETH
- WETH().deposit{
- value : amount - dust
- }();
- // track and check outstanding token amounts
- bridgeOut(address(WETH()), normalizedAmount);
- sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), normalizedAmount, recipientChain, recipient, normalizedArbiterFee, wormholeFee, nonce);
- }
- // Initiate a Transfer
- function transferTokens(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable nonReentrant returns (uint64 sequence) {
- // determine token parameters
- uint16 tokenChain;
- bytes32 tokenAddress;
- if (isWrappedAsset(token)) {
- tokenChain = TokenImplementation(token).chainId();
- tokenAddress = TokenImplementation(token).nativeContract();
- } else {
- tokenChain = chainId();
- tokenAddress = bytes32(uint256(uint160(token)));
- }
- // query tokens decimals
- (,bytes memory queriedDecimals) = token.staticcall(abi.encodeWithSignature("decimals()"));
- uint8 decimals = abi.decode(queriedDecimals, (uint8));
- // don't deposit dust that can not be bridged due to the decimal shift
- amount = deNormalizeAmount(normalizeAmount(amount, decimals), decimals);
- if (tokenChain == chainId()) {
- // query own token balance before transfer
- (,bytes memory queriedBalanceBefore) = token.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
- uint256 balanceBefore = abi.decode(queriedBalanceBefore, (uint256));
- // transfer tokens
- SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);
- // query own token balance after transfer
- (,bytes memory queriedBalanceAfter) = token.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector, address(this)));
- uint256 balanceAfter = abi.decode(queriedBalanceAfter, (uint256));
- // correct amount for potential transfer fees
- amount = balanceAfter - balanceBefore;
- } else {
- SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);
- TokenImplementation(token).burn(address(this), amount);
- }
- // normalize amounts decimals
- uint256 normalizedAmount = normalizeAmount(amount, decimals);
- uint256 normalizedArbiterFee = normalizeAmount(arbiterFee, decimals);
- // track and check outstanding token amounts
- if (tokenChain == chainId()) {
- bridgeOut(token, normalizedAmount);
- }
- sequence = logTransfer(tokenChain, tokenAddress, normalizedAmount, recipientChain, recipient, normalizedArbiterFee, msg.value, nonce);
- }
- function normalizeAmount(uint256 amount, uint8 decimals) internal pure returns(uint256){
- if (decimals > 8) {
- amount /= 10 ** (decimals - 8);
- }
- return amount;
- }
- function deNormalizeAmount(uint256 amount, uint8 decimals) internal pure returns(uint256){
- if (decimals > 8) {
- amount *= 10 ** (decimals - 8);
- }
- return amount;
- }
- function logTransfer(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint256 callValue, uint32 nonce) internal returns (uint64 sequence) {
- require(fee <= amount, "fee exceeds amount");
- BridgeStructs.Transfer memory transfer = BridgeStructs.Transfer({
- payloadID : 1,
- amount : amount,
- tokenAddress : tokenAddress,
- tokenChain : tokenChain,
- to : recipient,
- toChain : recipientChain,
- fee : fee
- });
- bytes memory encoded = encodeTransfer(transfer);
- sequence = wormhole().publishMessage{
- value : callValue
- }(nonce, encoded, 15);
- }
- function updateWrapped(bytes memory encodedVm) external returns (address token) {
- (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
- require(valid, reason);
- require(verifyBridgeVM(vm), "invalid emitter");
- BridgeStructs.AssetMeta memory meta = parseAssetMeta(vm.payload);
- return _updateWrapped(meta, vm.sequence);
- }
- function _updateWrapped(BridgeStructs.AssetMeta memory meta, uint64 sequence) internal returns (address token) {
- address wrapped = wrappedAsset(meta.tokenChain, meta.tokenAddress);
- require(wrapped != address(0), "wrapped asset does not exists");
- // Update metadata
- TokenImplementation(wrapped).updateDetails(bytes32ToString(meta.name), bytes32ToString(meta.symbol), sequence);
- return wrapped;
- }
- function createWrapped(bytes memory encodedVm) external returns (address token) {
- (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
- require(valid, reason);
- require(verifyBridgeVM(vm), "invalid emitter");
- BridgeStructs.AssetMeta memory meta = parseAssetMeta(vm.payload);
- return _createWrapped(meta, vm.sequence);
- }
- // Creates a wrapped asset using AssetMeta
- function _createWrapped(BridgeStructs.AssetMeta memory meta, uint64 sequence) internal returns (address token) {
- require(meta.tokenChain != chainId(), "can only wrap tokens from foreign chains");
- require(wrappedAsset(meta.tokenChain, meta.tokenAddress) == address(0), "wrapped asset already exists");
- // initialize the TokenImplementation
- bytes memory initialisationArgs = abi.encodeWithSelector(
- TokenImplementation.initialize.selector,
- bytes32ToString(meta.name),
- bytes32ToString(meta.symbol),
- meta.decimals,
- sequence,
- address(this),
- meta.tokenChain,
- meta.tokenAddress
- );
- // initialize the BeaconProxy
- bytes memory constructorArgs = abi.encode(address(this), initialisationArgs);
- // deployment code
- bytes memory bytecode = abi.encodePacked(type(BridgeToken).creationCode, constructorArgs);
- bytes32 salt = keccak256(abi.encodePacked(meta.tokenChain, meta.tokenAddress));
- assembly {
- token := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
- if iszero(extcodesize(token)) {
- revert(0, 0)
- }
- }
- setWrappedAsset(meta.tokenChain, meta.tokenAddress, token);
- }
- function completeTransfer(bytes memory encodedVm) public {
- _completeTransfer(encodedVm, false);
- }
- function completeTransferAndUnwrapETH(bytes memory encodedVm) public {
- _completeTransfer(encodedVm, true);
- }
- // Execute a Transfer message
- function _completeTransfer(bytes memory encodedVm, bool unwrapWETH) internal {
- (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
- require(valid, reason);
- require(verifyBridgeVM(vm), "invalid emitter");
- BridgeStructs.Transfer memory transfer = parseTransfer(vm.payload);
- require(!isTransferCompleted(vm.hash), "transfer already completed");
- setTransferCompleted(vm.hash);
- require(transfer.toChain == chainId(), "invalid target chain");
- IERC20 transferToken;
- if (transfer.tokenChain == chainId()) {
- transferToken = IERC20(address(uint160(uint256(transfer.tokenAddress))));
- // track outstanding token amounts
- bridgedIn(address(transferToken), transfer.amount);
- } else {
- address wrapped = wrappedAsset(transfer.tokenChain, transfer.tokenAddress);
- require(wrapped != address(0), "no wrapper for this token created yet");
- transferToken = IERC20(wrapped);
- }
- require(unwrapWETH == false || address(transferToken) == address(WETH()), "invalid token, can only unwrap WETH");
- // query decimals
- (,bytes memory queriedDecimals) = address(transferToken).staticcall(abi.encodeWithSignature("decimals()"));
- uint8 decimals = abi.decode(queriedDecimals, (uint8));
- // adjust decimals
- uint256 nativeAmount = deNormalizeAmount(transfer.amount, decimals);
- uint256 nativeFee = deNormalizeAmount(transfer.fee, decimals);
- // transfer fee to arbiter
- if (nativeFee > 0) {
- require(nativeFee <= nativeAmount, "fee higher than transferred amount");
- if (unwrapWETH) {
- WETH().withdraw(nativeFee);
- payable(msg.sender).transfer(nativeFee);
- } else {
- if (transfer.tokenChain != chainId()) {
- // mint wrapped asset
- TokenImplementation(address(transferToken)).mint(msg.sender, nativeFee);
- } else {
- SafeERC20.safeTransfer(transferToken, msg.sender, nativeFee);
- }
- }
- }
- // transfer bridged amount to recipient
- uint transferAmount = nativeAmount - nativeFee;
- address transferRecipient = address(uint160(uint256(transfer.to)));
- if (unwrapWETH) {
- WETH().withdraw(transferAmount);
- payable(transferRecipient).transfer(transferAmount);
- } else {
- if (transfer.tokenChain != chainId()) {
- // mint wrapped asset
- TokenImplementation(address(transferToken)).mint(transferRecipient, transferAmount);
- } else {
- SafeERC20.safeTransfer(transferToken, transferRecipient, transferAmount);
- }
- }
- }
- function bridgeOut(address token, uint normalizedAmount) internal {
- uint outstanding = outstandingBridged(token);
- require(outstanding + normalizedAmount <= type(uint64).max, "transfer exceeds max outstanding bridged token amount");
- setOutstandingBridged(token, outstanding + normalizedAmount);
- }
- function bridgedIn(address token, uint normalizedAmount) internal {
- setOutstandingBridged(token, outstandingBridged(token) - normalizedAmount);
- }
- function verifyBridgeVM(IWormhole.VM memory vm) internal view returns (bool){
- if (bridgeContracts(vm.emitterChainId) == vm.emitterAddress) {
- return true;
- }
- return false;
- }
- function encodeAssetMeta(BridgeStructs.AssetMeta memory meta) public pure returns (bytes memory encoded) {
- encoded = abi.encodePacked(
- meta.payloadID,
- meta.tokenAddress,
- meta.tokenChain,
- meta.decimals,
- meta.symbol,
- meta.name
- );
- }
- function encodeTransfer(BridgeStructs.Transfer memory transfer) public pure returns (bytes memory encoded) {
- encoded = abi.encodePacked(
- transfer.payloadID,
- transfer.amount,
- transfer.tokenAddress,
- transfer.tokenChain,
- transfer.to,
- transfer.toChain,
- transfer.fee
- );
- }
- function parseAssetMeta(bytes memory encoded) public pure returns (BridgeStructs.AssetMeta memory meta) {
- uint index = 0;
- meta.payloadID = encoded.toUint8(index);
- index += 1;
- require(meta.payloadID == 2, "invalid AssetMeta");
- meta.tokenAddress = encoded.toBytes32(index);
- index += 32;
- meta.tokenChain = encoded.toUint16(index);
- index += 2;
- meta.decimals = encoded.toUint8(index);
- index += 1;
- meta.symbol = encoded.toBytes32(index);
- index += 32;
- meta.name = encoded.toBytes32(index);
- index += 32;
- require(encoded.length == index, "invalid AssetMeta");
- }
- function parseTransfer(bytes memory encoded) public pure returns (BridgeStructs.Transfer memory transfer) {
- uint index = 0;
- transfer.payloadID = encoded.toUint8(index);
- index += 1;
- require(transfer.payloadID == 1, "invalid Transfer");
- transfer.amount = encoded.toUint256(index);
- index += 32;
- transfer.tokenAddress = encoded.toBytes32(index);
- index += 32;
- transfer.tokenChain = encoded.toUint16(index);
- index += 2;
- transfer.to = encoded.toBytes32(index);
- index += 32;
- transfer.toChain = encoded.toUint16(index);
- index += 2;
- transfer.fee = encoded.toUint256(index);
- index += 32;
- require(encoded.length == index, "invalid Transfer");
- }
- function bytes32ToString(bytes32 input) internal pure returns (string memory) {
- uint256 i;
- while (i < 32 && input[i] != 0) {
- i++;
- }
- bytes memory array = new bytes(i);
- for (uint c = 0; c < i; c++) {
- array[c] = input[c];
- }
- return string(array);
- }
- // we need to accept ETH sends to unwrap WETH
- receive() external payable {}
- }
|