diff --git a/ethereum/.env.template b/ethereum/.env.template index a17558cba..d08011a11 100644 --- a/ethereum/.env.template +++ b/ethereum/.env.template @@ -10,3 +10,9 @@ INIT_SIGNERS= # ["0x0000000000000000000000000000000000000000"] INIT_CHAIN_ID= # 0x2 INIT_GOV_CHAIN_ID= # 0x3 INIT_GOV_CONTRACT= # 0x000000000000000000000000000000000000000000000000000000000000000 + +# Bridge Migrations # Example Format +BRIDGE_INIT_CHAIN_ID= # 0x02 +BRIDGE_INIT_GOV_CHAIN_ID= # 0x3 +BRIDGE_INIT_GOV_CONTRACT= # 0x0000000000000000000000000000000000000000000000000000000000000004 +BRIDGE_INIT_WETH= # 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 diff --git a/ethereum/.env.test b/ethereum/.env.test index 5623b6565..67d5501d1 100644 --- a/ethereum/.env.test +++ b/ethereum/.env.test @@ -2,4 +2,10 @@ INIT_SIGNERS=["0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"] INIT_CHAIN_ID=0x2 INIT_GOV_CHAIN_ID=0x3 -INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004 \ No newline at end of file +INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004 + +# Bridge Migrations +BRIDGE_INIT_CHAIN_ID=0x02 +BRIDGE_INIT_GOV_CHAIN_ID=0x3 +BRIDGE_INIT_GOV_CONTRACT=0x0000000000000000000000000000000000000000000000000000000000000004 +BRIDGE_INIT_WETH=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \ No newline at end of file diff --git a/ethereum/contracts/bridge/Bridge.sol b/ethereum/contracts/bridge/Bridge.sol new file mode 100644 index 000000000..8dcfb520c --- /dev/null +++ b/ethereum/contracts/bridge/Bridge.sol @@ -0,0 +1,316 @@ +// contracts/Bridge.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +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 { + 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 fee, uint32 nonce) public payable returns (uint64 sequence) { + uint wormholeFee = wormhole().messageFee(); + + require(wormholeFee < msg.value, "value is smaller than wormhole fee"); + + WETH().deposit{ + value : msg.value - wormholeFee + }(); + + sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), msg.value, recipientChain, recipient, fee, wormholeFee, nonce); + } + + // Initiate a Transfer + function transferTokens(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint32 nonce) public payable returns (uint64 sequence) { + if(tokenChain == chainId()){ + SafeERC20.safeTransferFrom(IERC20(address(uint160(uint256(tokenAddress)))), msg.sender, address(this), amount); + } else { + address wrapped = wrappedAsset(tokenChain, tokenAddress); + require(wrapped != address(0), "no wrapper for this token created yet"); + + SafeERC20.safeTransferFrom(IERC20(wrapped), msg.sender, address(this), amount); + + TokenImplementation(wrapped).burn(address(this), amount); + } + + sequence = logTransfer(tokenChain, tokenAddress, amount, recipientChain, recipient, fee, msg.value, nonce); + } + + 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 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); + } + + // Creates a wrapped asset using AssetMeta + function _createWrapped(BridgeStructs.AssetMeta memory meta) 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, + + 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)))); + } else { + address wrapped = wrappedAsset(transfer.tokenChain, transfer.tokenAddress); + require(wrapped != address(0), "no wrapper for this token created yet"); + + TokenImplementation(wrapped).mint(address(this), transfer.amount); + + transferToken = IERC20(wrapped); + } + + if(transfer.fee > 0) { + require(transfer.fee <= transfer.amount, "fee higher than transferred amount"); + + if (unwrapWETH) { + require(address(transferToken) == address(WETH()), "invalid token, can only unwrap ETH"); + WETH().withdraw(transfer.fee); + payable(msg.sender).transfer(transfer.fee); + } else { + SafeERC20.safeTransfer(transferToken, msg.sender, transfer.fee); + } + } + + uint transferAmount = transfer.amount - transfer.fee; + address payable transferRecipient = payable(address(uint160(uint256(transfer.to)))); + + if (unwrapWETH) { + require(address(transferToken) == address(WETH()), "invalid token, can only unwrap ETH"); + WETH().withdraw(transferAmount); + transferRecipient.transfer(transferAmount); + } else { + SafeERC20.safeTransfer(transferToken, transferRecipient, transferAmount); + } + } + + 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 {} +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/BridgeGetters.sol b/ethereum/contracts/bridge/BridgeGetters.sol new file mode 100644 index 000000000..7c0284de9 --- /dev/null +++ b/ethereum/contracts/bridge/BridgeGetters.sol @@ -0,0 +1,61 @@ +// contracts/Getters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "../interfaces/IWormhole.sol"; + +import "./BridgeState.sol"; + +contract BridgeGetters is BridgeState { + function governanceActionIsConsumed(bytes32 hash) public view returns (bool) { + return _state.consumedGovernanceActions[hash]; + } + + function isInitialized(address impl) public view returns (bool) { + return _state.initializedImplementations[impl]; + } + + function isTransferCompleted(bytes32 hash) public view returns (bool) { + return _state.completedTransfers[hash]; + } + + function wormhole() public view returns (IWormhole) { + return IWormhole(_state.wormhole); + } + + function chainId() public view returns (uint16){ + return _state.provider.chainId; + } + + function governanceChainId() public view returns (uint16){ + return _state.provider.governanceChainId; + } + + function governanceContract() public view returns (bytes32){ + return _state.provider.governanceContract; + } + + function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress) public view returns (address){ + return _state.wrappedAssets[tokenChainId][tokenAddress]; + } + + function bridgeContracts(uint16 chainId_) public view returns (bytes32){ + return _state.bridgeImplementations[chainId_]; + } + + function tokenImplementation() public view returns (address){ + return _state.tokenImplementation; + } + + function WETH() public view returns (IWETH){ + return IWETH(_state.provider.WETH); + } +} + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint amount) external; +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/BridgeGovernance.sol b/ethereum/contracts/bridge/BridgeGovernance.sol new file mode 100644 index 000000000..28ac2a43b --- /dev/null +++ b/ethereum/contracts/bridge/BridgeGovernance.sol @@ -0,0 +1,118 @@ +// contracts/Bridge.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +import "../libraries/external/BytesLib.sol"; + +import "./BridgeGetters.sol"; +import "./BridgeSetters.sol"; +import "./BridgeStructs.sol"; + +import "./token/Token.sol"; +import "./token/TokenImplementation.sol"; + +import "../interfaces/IWormhole.sol"; + +contract BridgeGovernance is BridgeGetters, BridgeSetters, ERC1967Upgrade { + using BytesLib for bytes; + + // Execute a RegisterChain governance message + function registerChain(bytes memory encodedVM) public { + (IWormhole.VM memory vm, bool valid, string memory reason) = verifyGovernanceVM(encodedVM); + require(valid, reason); + + + setGovernanceActionConsumed(vm.hash); + + BridgeStructs.RegisterChain memory chain = parseRegisterChain(vm.payload); + + setBridgeImplementation(chain.chainID, chain.emitterAddress); + } + + // Execute a UpgradeContract governance message + function upgrade(bytes memory encodedVM) public { + (IWormhole.VM memory vm, bool valid, string memory reason) = verifyGovernanceVM(encodedVM); + require(valid, reason); + + setGovernanceActionConsumed(vm.hash); + + BridgeStructs.UpgradeContract memory implementation = parseUpgrade(vm.payload); + + require(implementation.chainID == chainId(), "wrong chain id"); + + upgradeImplementation(address(uint160(uint256(implementation.newContract)))); + } + + function verifyGovernanceVM(bytes memory encodedVM) internal view returns (IWormhole.VM memory parsedVM, bool isValid, string memory invalidReason){ + (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVM); + + if(!valid){ + return (vm, valid, reason); + } + + if (vm.emitterChainId != governanceChainId()) { + return (vm, false, "wrong governance chain"); + } + if (vm.emitterAddress != governanceContract()) { + return (vm, false, "wrong governance contract"); + } + + if(governanceActionIsConsumed(vm.hash)){ + return (vm, false, "governance action already consumed"); + } + + return (vm, true, ""); + } + + event ContractUpgraded(address indexed oldContract, address indexed newContract); + function upgradeImplementation(address newImplementation) internal { + address currentImplementation = _getImplementation(); + + _upgradeTo(newImplementation); + + // Call initialize function of the new implementation + (bool success, bytes memory reason) = newImplementation.delegatecall(abi.encodeWithSignature("initialize()")); + + require(success, string(reason)); + + emit ContractUpgraded(currentImplementation, newImplementation); + } + + function parseRegisterChain(bytes memory encoded) public pure returns(BridgeStructs.RegisterChain memory chain) { + uint index = 0; + + chain.payloadID = encoded.toUint8(index); + index += 1; + + require(chain.payloadID == 3, "invalid RegisterChain"); + + chain.chainID = encoded.toUint16(index); + index += 2; + + chain.emitterAddress = encoded.toBytes32(index); + index += 32; + + require(encoded.length == index, "invalid RegisterChain"); + } + + function parseUpgrade(bytes memory encoded) public pure returns(BridgeStructs.UpgradeContract memory chain) { + uint index = 0; + + chain.payloadID = encoded.toUint8(index); + index += 1; + + require(chain.payloadID == 4, "invalid UpgradeContract"); + + chain.chainID = encoded.toUint16(index); + index += 2; + + chain.newContract = encoded.toBytes32(index); + index += 32; + + require(encoded.length == index, "invalid UpgradeContract"); + } +} diff --git a/ethereum/contracts/bridge/BridgeImplementation.sol b/ethereum/contracts/bridge/BridgeImplementation.sol new file mode 100644 index 000000000..8f9313024 --- /dev/null +++ b/ethereum/contracts/bridge/BridgeImplementation.sol @@ -0,0 +1,51 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol"; + +import "./Bridge.sol"; + + +contract BridgeImplementation is Bridge { + + function initialize( + uint16 chainId, + address wormhole, + uint16 governanceChainId, + bytes32 governanceContract, + address tokenImplementation, + address WETH + ) initializer public { + setChainId(chainId); + + setWormhole(wormhole); + + setGovernanceChainId(governanceChainId); + setGovernanceContract(governanceContract); + + setTokenImplementation(tokenImplementation); + + setWETH(WETH); + } + + // Beacon getter for the token contracts + function implementation() public view returns (address) { + return tokenImplementation(); + } + + modifier initializer() { + address impl = ERC1967Upgrade._getImplementation(); + + require( + !isInitialized(impl), + "already initialized" + ); + + setInitialized(impl); + + _; + } +} diff --git a/ethereum/contracts/bridge/BridgeSetters.sol b/ethereum/contracts/bridge/BridgeSetters.sol new file mode 100644 index 000000000..bf90d3d49 --- /dev/null +++ b/ethereum/contracts/bridge/BridgeSetters.sol @@ -0,0 +1,52 @@ +// contracts/Setters.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./BridgeState.sol"; + +contract BridgeSetters is BridgeState { + function setInitialized(address implementatiom) internal { + _state.initializedImplementations[implementatiom] = true; + } + + function setGovernanceActionConsumed(bytes32 hash) internal { + _state.consumedGovernanceActions[hash] = true; + } + + function setTransferCompleted(bytes32 hash) internal { + _state.completedTransfers[hash] = true; + } + + function setChainId(uint16 chainId) internal { + _state.provider.chainId = chainId; + } + + function setGovernanceChainId(uint16 chainId) internal { + _state.provider.governanceChainId = chainId; + } + + function setGovernanceContract(bytes32 governanceContract) internal { + _state.provider.governanceContract = governanceContract; + } + + function setBridgeImplementation(uint16 chainId, bytes32 bridgeContract) internal { + _state.bridgeImplementations[chainId] = bridgeContract; + } + + function setTokenImplementation(address impl) internal { + _state.tokenImplementation = impl; + } + + function setWETH(address weth) internal { + _state.provider.WETH = weth; + } + + function setWormhole(address wh) internal { + _state.wormhole = payable(wh); + } + + function setWrappedAsset(uint16 tokenChainId, bytes32 tokenAddress, address wrapper) internal { + _state.wrappedAssets[tokenChainId][tokenAddress] = wrapper; + } +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/BridgeState.sol b/ethereum/contracts/bridge/BridgeState.sol new file mode 100644 index 000000000..dca503148 --- /dev/null +++ b/ethereum/contracts/bridge/BridgeState.sol @@ -0,0 +1,41 @@ +// contracts/State.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./BridgeStructs.sol"; + +contract BridgeStorage { + struct Provider { + uint16 chainId; + uint16 governanceChainId; + bytes32 governanceContract; + address WETH; + } + + struct State { + address payable wormhole; + address tokenImplementation; + + Provider provider; + + // Mapping of consumed governance actions + mapping(bytes32 => bool) consumedGovernanceActions; + + // Mapping of consumed token transfers + mapping(bytes32 => bool) completedTransfers; + + // Mapping of initialized implementations + mapping(address => bool) initializedImplementations; + + // Mapping of wrapped assets (chainID => nativeAddress => wrappedAddress) + mapping(uint16 => mapping(bytes32 => address)) wrappedAssets; + + // Mapping of bridge contracts on other chains + mapping(uint16 => bytes32) bridgeImplementations; + } +} + +contract BridgeState { + BridgeStorage.State _state; +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/BridgeStructs.sol b/ethereum/contracts/bridge/BridgeStructs.sol new file mode 100644 index 000000000..d9f25ce48 --- /dev/null +++ b/ethereum/contracts/bridge/BridgeStructs.sol @@ -0,0 +1,56 @@ +// contracts/Structs.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +contract BridgeStructs { + struct Transfer { + // PayloadID uint8 = 1 + uint8 payloadID; + // Amount being transferred (big-endian uint256) + uint256 amount; + // Address of the token. Left-zero-padded if shorter than 32 bytes + bytes32 tokenAddress; + // Chain ID of the token + uint16 tokenChain; + // Address of the recipient. Left-zero-padded if shorter than 32 bytes + bytes32 to; + // Chain ID of the recipient + uint16 toChain; + // Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount. + uint256 fee; + } + + struct AssetMeta { + // PayloadID uint8 = 2 + uint8 payloadID; + // Address of the token. Left-zero-padded if shorter than 32 bytes + bytes32 tokenAddress; + // Chain ID of the token + uint16 tokenChain; + // Number of decimals of the token (big-endian uint256) + uint8 decimals; + // Symbol of the token (UTF-8) + bytes32 symbol; + // Name of the token (UTF-8) + bytes32 name; + } + + struct RegisterChain { + // PayloadID uint8 = 3 + uint8 payloadID; + // Chain ID + uint16 chainID; + // Emitter address. Left-zero-padded if shorter than 32 bytes + bytes32 emitterAddress; + } + + struct UpgradeContract { + // PayloadID uint8 = 4 + uint8 payloadID; + // Chain ID + uint16 chainID; + // Address of the new contract + bytes32 newContract; + } +} diff --git a/ethereum/contracts/bridge/TokenBridge.sol b/ethereum/contracts/bridge/TokenBridge.sol new file mode 100644 index 000000000..dfa560dda --- /dev/null +++ b/ethereum/contracts/bridge/TokenBridge.sol @@ -0,0 +1,15 @@ +// contracts/Wormhole.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +contract TokenBridge is ERC1967Proxy { + constructor (address implementation, bytes memory initData) + ERC1967Proxy( + implementation, + initData + ) + {} +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol b/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol new file mode 100644 index 000000000..84bb1e2f2 --- /dev/null +++ b/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol @@ -0,0 +1,20 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../BridgeImplementation.sol"; + +contract MockBridgeImplementation is BridgeImplementation { + function initialize() initializer public { + // this function needs to be exposed for an upgrade to pass + } + + function testNewImplementationActive() external pure returns (bool) { + return true; + } + + function testUpdateWETHAddress(address WETH) external { + setWETH(WETH); + } +} diff --git a/ethereum/contracts/bridge/mock/MockTokenImplementation.sol b/ethereum/contracts/bridge/mock/MockTokenImplementation.sol new file mode 100644 index 000000000..d488d1b5a --- /dev/null +++ b/ethereum/contracts/bridge/mock/MockTokenImplementation.sol @@ -0,0 +1,12 @@ +// contracts/Implementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../token/TokenImplementation.sol"; + +contract MockTokenImplementation is TokenImplementation { + function testNewImplementationActive() external pure returns (bool) { + return true; + } +} diff --git a/ethereum/contracts/bridge/mock/MockWETH9.sol b/ethereum/contracts/bridge/mock/MockWETH9.sol new file mode 100644 index 000000000..bd4f2d3fc --- /dev/null +++ b/ethereum/contracts/bridge/mock/MockWETH9.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GNU + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +contract MockWETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping (address => uint) public balanceOf; + mapping (address => mapping (address => uint)) public allowance; + + fallback() external payable { + deposit(); + } + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) + public + returns (bool) + { + require(balanceOf[src] >= wad); + + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} diff --git a/ethereum/contracts/bridge/token/Token.sol b/ethereum/contracts/bridge/token/Token.sol new file mode 100644 index 000000000..8cdfba901 --- /dev/null +++ b/ethereum/contracts/bridge/token/Token.sol @@ -0,0 +1,11 @@ +// contracts/Structs.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +contract BridgeToken is BeaconProxy { + constructor(address beacon, bytes memory data) BeaconProxy(beacon, data) { + + } +} \ No newline at end of file diff --git a/ethereum/contracts/bridge/token/TokenImplementation.sol b/ethereum/contracts/bridge/token/TokenImplementation.sol new file mode 100644 index 000000000..fb5a59ee1 --- /dev/null +++ b/ethereum/contracts/bridge/token/TokenImplementation.sol @@ -0,0 +1,167 @@ +// contracts/TokenImplementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "./TokenState.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +// Based on the OpenZepplin ERC20 implementation, licensed under MIT +contract TokenImplementation is TokenState, Context { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function initialize( + string memory name_, + string memory symbol_, + uint8 decimals_, + + address owner_, + + uint16 chainId_, + bytes32 nativeContract_ + ) initializer public { + _state.name = name_; + _state.symbol = symbol_; + _state.decimals = decimals_; + + _state.owner = owner_; + + _state.chainId = chainId_; + _state.nativeContract = nativeContract_; + } + + function name() public view returns (string memory) { + return _state.name; + } + + function symbol() public view returns (string memory) { + return _state.symbol; + } + + function owner() public view returns (address) { + return _state.owner; + } + + function decimals() public view returns (uint8) { + return _state.decimals; + } + + function totalSupply() public view returns (uint256) { + return _state.totalSupply; + } + + function chainId() public view returns (uint16) { + return _state.chainId; + } + + function nativeContract() public view returns (bytes32) { + return _state.nativeContract; + } + + function balanceOf(address account_) public view returns (uint256) { + return _state.balances[account_]; + } + + function transfer(address recipient_, uint256 amount_) public returns (bool) { + _transfer(_msgSender(), recipient_, amount_); + return true; + } + + function allowance(address owner_, address spender_) public view returns (uint256) { + return _state.allowances[owner_][spender_]; + } + + function approve(address spender_, uint256 amount_) public returns (bool) { + _approve(_msgSender(), spender_, amount_); + return true; + } + + function transferFrom(address sender_, address recipient_, uint256 amount_) public returns (bool) { + _transfer(sender_, recipient_, amount_); + + uint256 currentAllowance = _state.allowances[sender_][_msgSender()]; + require(currentAllowance >= amount_, "ERC20: transfer amount exceeds allowance"); + _approve(sender_, _msgSender(), currentAllowance - amount_); + + return true; + } + + function increaseAllowance(address spender_, uint256 addedValue_) public returns (bool) { + _approve(_msgSender(), spender_, _state.allowances[_msgSender()][spender_] + addedValue_); + return true; + } + + function decreaseAllowance(address spender_, uint256 subtractedValue_) public returns (bool) { + uint256 currentAllowance = _state.allowances[_msgSender()][spender_]; + require(currentAllowance >= subtractedValue_, "ERC20: decreased allowance below zero"); + _approve(_msgSender(), spender_, currentAllowance - subtractedValue_); + + return true; + } + + function _transfer(address sender_, address recipient_, uint256 amount_) internal { + require(sender_ != address(0), "ERC20: transfer from the zero address"); + require(recipient_ != address(0), "ERC20: transfer to the zero address"); + + uint256 senderBalance = _state.balances[sender_]; + require(senderBalance >= amount_, "ERC20: transfer amount exceeds balance"); + _state.balances[sender_] = senderBalance - amount_; + _state.balances[recipient_] += amount_; + + emit Transfer(sender_, recipient_, amount_); + } + + function mint(address account_, uint256 amount_) public onlyOwner { + _mint(account_, amount_); + } + + function _mint(address account_, uint256 amount_) internal { + require(account_ != address(0), "ERC20: mint to the zero address"); + + _state.totalSupply += amount_; + _state.balances[account_] += amount_; + emit Transfer(address(0), account_, amount_); + } + + function burn(address account_, uint256 amount_) public onlyOwner { + _burn(account_, amount_); + } + + function _burn(address account_, uint256 amount_) internal { + require(account_ != address(0), "ERC20: burn from the zero address"); + + uint256 accountBalance = _state.balances[account_]; + require(accountBalance >= amount_, "ERC20: burn amount exceeds balance"); + _state.balances[account_] = accountBalance - amount_; + _state.totalSupply -= amount_; + + emit Transfer(account_, address(0), amount_); + } + + function _approve(address owner_, address spender_, uint256 amount_) internal virtual { + require(owner_ != address(0), "ERC20: approve from the zero address"); + require(spender_ != address(0), "ERC20: approve to the zero address"); + + _state.allowances[owner_][spender_] = amount_; + emit Approval(owner_, spender_, amount_); + } + + modifier onlyOwner() { + require(owner() == _msgSender(), "caller is not the owner"); + _; + } + + modifier initializer() { + require( + !_state.initialized, + "Already initialized" + ); + + _state.initialized = true; + + _; + } +} diff --git a/ethereum/contracts/bridge/token/TokenState.sol b/ethereum/contracts/bridge/token/TokenState.sol new file mode 100644 index 000000000..b11adcee2 --- /dev/null +++ b/ethereum/contracts/bridge/token/TokenState.sol @@ -0,0 +1,29 @@ +// contracts/State.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +contract TokenStorage { + struct State { + string name; + string symbol; + + uint256 totalSupply; + uint8 decimals; + + mapping (address => uint256) balances; + + mapping (address => mapping (address => uint256)) allowances; + + address owner; + + bool initialized; + + uint16 chainId; + bytes32 nativeContract; + } +} + +contract TokenState { + TokenStorage.State _state; +} \ No newline at end of file diff --git a/ethereum/migrations/3_deploy_bridge.js b/ethereum/migrations/3_deploy_bridge.js new file mode 100644 index 000000000..07d80efd2 --- /dev/null +++ b/ethereum/migrations/3_deploy_bridge.js @@ -0,0 +1,33 @@ +require('dotenv').config({ path: "../.env" }); + +const TokenBridge = artifacts.require("TokenBridge"); +const BridgeImplementation = artifacts.require("BridgeImplementation"); +const TokenImplementation = artifacts.require("TokenImplementation"); +const Wormhole = artifacts.require("Wormhole"); + +const chainId = process.env.BRIDGE_INIT_CHAIN_ID; +const governanceChainId = process.env.BRIDGE_INIT_GOV_CHAIN_ID; +const governanceContract = process.env.BRIDGE_INIT_GOV_CONTRACT; // bytes32 +const WETH = process.env.BRIDGE_INIT_WETH; + +module.exports = async function (deployer) { + // deploy token implementation + await deployer.deploy(TokenImplementation); + + // deploy implementation + await deployer.deploy(BridgeImplementation); + + // encode initialisation data + const impl = new web3.eth.Contract(BridgeImplementation.abi, BridgeImplementation.address); + const initData = impl.methods.initialize( + chainId, + (await Wormhole.deployed()).address, + governanceChainId, + governanceContract, + TokenImplementation.address, + WETH + ).encodeABI(); + + // deploy proxy + await deployer.deploy(TokenBridge, BridgeImplementation.address, initData); +}; diff --git a/ethereum/test/bridge.js b/ethereum/test/bridge.js new file mode 100644 index 000000000..68df629b6 --- /dev/null +++ b/ethereum/test/bridge.js @@ -0,0 +1,746 @@ +const jsonfile = require('jsonfile'); +const elliptic = require('elliptic'); +const BigNumber = require('bignumber.js'); + +const Wormhole = artifacts.require("Wormhole"); +const TokenBridge = artifacts.require("TokenBridge"); +const BridgeImplementation = artifacts.require("BridgeImplementation"); +const TokenImplementation = artifacts.require("TokenImplementation"); +const MockBridgeImplementation = artifacts.require("MockBridgeImplementation"); +const MockWETH9 = artifacts.require("MockWETH9"); + +const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; +const testSigner2PK = "892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe"; + +const WormholeImplementationFullABI = jsonfile.readFileSync("build/contracts/Implementation.json").abi +const BridgeImplementationFullABI = jsonfile.readFileSync("build/contracts/BridgeImplementation.json").abi +const TokenImplementationFullABI = jsonfile.readFileSync("build/contracts/TokenImplementation.json").abi + +contract("Bridge", function () { + const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK); + const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK); + const testChainId = "2"; + const testGovernanceChainId = "3"; + const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004"; + let WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + const testForeignChainId = "1"; + const testForeignBridgeContract = "0x000000000000000000000000000000000000000000000000000000000000ffff"; + const testBridgedAssetChain = "0001"; + const testBridgedAssetAddress = "000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e"; + + + it("should be initialized with the correct signers and values", async function(){ + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + const weth = await initialized.methods.WETH().call(); + assert.equal(weth, WETH); + + const tokenImplentation = await initialized.methods.tokenImplementation().call(); + assert.equal(tokenImplentation, TokenImplementation.address); + + // test beacon functionality + const beaconImplementation = await initialized.methods.implementation().call(); + assert.equal(beaconImplementation, TokenImplementation.address); + + // chain id + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, testChainId); + + // governance + const governanceChainId = await initialized.methods.governanceChainId().call(); + assert.equal(governanceChainId, testGovernanceChainId); + const governanceContract = await initialized.methods.governanceContract().call(); + assert.equal(governanceContract, testGovernanceContract); + }) + + it("initialize should be non-reentrant", async function(){ + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + try{ + await initialized.methods.initialize( + 1, + Wormhole.address, + 1, + testGovernanceContract, + TokenImplementation.address, + WETH + ).estimateGas(); + } catch (error) { + assert.equal(error.message, "Returned error: VM Exception while processing transaction: revert already initialized") + return + } + + assert.fail("did not fail") + }) + + + it("should register a foreign bridge implementation correctly", async function() { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const accounts = await web3.eth.getAccounts(); + + let data = [ + "0x03", + web3.eth.abi.encodeParameter("uint16", testForeignChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("bytes32", testForeignBridgeContract).substring(2), + ].join('') + + const vm = await signAndEncodeVM( + 1, + 1, + testGovernanceChainId, + testGovernanceContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + + let before = await initialized.methods.bridgeContracts(testForeignChainId).call(); + + assert.equal(before, "0x0000000000000000000000000000000000000000000000000000000000000000"); + + await initialized.methods.registerChain("0x" + vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + let after = await initialized.methods.bridgeContracts(testForeignChainId).call(); + + assert.equal(after, testForeignBridgeContract); + }) + + it("should accept a valid upgrade", async function() { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockBridgeImplementation.new(); + + let data = [ + "0x04", + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("address", mock.address).substring(2), + ].join('') + + const vm = await signAndEncodeVM( + 1, + 1, + testGovernanceChainId, + testGovernanceContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + let before = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), BridgeImplementation.address.toLowerCase()); + + await initialized.methods.upgrade("0x" + vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + let after = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + + const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address); + + let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); + + assert.ok(isUpgraded); + }) + + it("bridged tokens should only be mint- and burn-able by owner", async function() { + const accounts = await web3.eth.getAccounts(); + + // initialize our template token contract + const token = new web3.eth.Contract(TokenImplementation.abi, TokenImplementation.address); + + await token.methods.initialize( + "TestToken", + "TT", + 18, + + accounts[0], + + 0, + "0x0" + ).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + await token.methods.mint(accounts[0], 10).send({ + from : accounts[0], + gasLimit : 2000000 + }); + + await token.methods.burn(accounts[0], 5).send({ + from : accounts[0], + gasLimit : 2000000 + }); + + let failed = false + try { + await token.methods.mint(accounts[0], 10).send({ + from : accounts[1], + gasLimit : 2000000 + }); + } catch(e) { + failed = true + } + assert.ok(failed) + + failed = false + try { + await token.methods.burn(accounts[0], 5).send({ + from : accounts[1], + gasLimit : 2000000 + }); + } catch(e) { + failed = true + } + assert.ok(failed) + + await token.methods.burn(accounts[0], 5).send({ + from : accounts[0], + gasLimit : 2000000 + }); + }) + + it("should attest a token correctly", async function() { + const accounts = await web3.eth.getAccounts(); + + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + await initialized.methods.attestToken(TokenImplementation.address, "234").send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + const wormhole = new web3.eth.Contract(WormholeImplementationFullABI, Wormhole.address); + + const log = (await wormhole.getPastEvents('LogMessagePublished', { + fromBlock : 'latest' + }))[0].returnValues + + assert.equal(log.sender, TokenBridge.address) + + assert.equal(log.payload.length - 2, 200); + + // payload id + assert.equal(log.payload.substr(2, 2), "02"); + + // token address + assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("address", TokenImplementation.address).substring(2)); + + // chain id + assert.equal(log.payload.substr(68, 4), web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + 64 - 4)) + + // decimals + assert.equal(log.payload.substr(72, 2), web3.eth.abi.encodeParameter("uint8", 18).substring(2 + 64 - 2)) + + // symbol (TT) + assert.equal(log.payload.substr(74, 64), "5454000000000000000000000000000000000000000000000000000000000000") + + // name (TestToken) + assert.equal(log.payload.substr(138, 64), "54657374546f6b656e0000000000000000000000000000000000000000000000") + }) + + it("should correctly deploy a wrapped asset for a token attestation", async function() { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const accounts = await web3.eth.getAccounts(); + + const data = "0x02" + + // tokenAddress + testBridgedAssetAddress + + // tokenchain + testBridgedAssetChain + + // decimals + "12" + + // symbol + "5454000000000000000000000000000000000000000000000000000000000000" + + // name + "54657374546f6b656e0000000000000000000000000000000000000000000000"; + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await initialized.methods.createWrapped("0x" + vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + const wrappedAddress = await initialized.methods.wrappedAsset("0x0001", "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e").call(); + + const initializedWrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress); + + const symbol = await initializedWrappedAsset.methods.symbol().call(); + assert.equal(symbol, "TT"); + + const name = await initializedWrappedAsset.methods.name().call(); + assert.equal(name, "TestToken"); + + const decimals = await initializedWrappedAsset.methods.decimals().call(); + assert.equal(decimals, 18); + + const chainId = await initializedWrappedAsset.methods.chainId().call(); + assert.equal(chainId, 1); + + const nativeContract = await initializedWrappedAsset.methods.nativeContract().call(); + assert.equal(nativeContract, "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e"); + }) + + it("should deposit and log transfers correctly", async function() { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + + // mint and approve tokens + const token = new web3.eth.Contract(TokenImplementation.abi, TokenImplementation.address); + await token.methods.mint(accounts[0], amount).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + await token.methods.approve(TokenBridge.address, amount).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + // deposit tokens + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + const accountBalanceBefore = await token.methods.balanceOf(accounts[0]).call(); + const bridgeBalanceBefore = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(accountBalanceBefore.toString(10), amount); + assert.equal(bridgeBalanceBefore.toString(10), "0"); + + await initialized.methods.transferTokens( + testChainId, + web3.eth.abi.encodeParameter("address", TokenImplementation.address), + amount, + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + "123", + "234" + ).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + const accountBalanceAfter = await token.methods.balanceOf(accounts[0]).call(); + const bridgeBalanceAfter = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(accountBalanceAfter.toString(10), "0"); + assert.equal(bridgeBalanceAfter.toString(10), amount); + + // check transfer log + const wormhole = new web3.eth.Contract(WormholeImplementationFullABI, Wormhole.address); + const log = (await wormhole.getPastEvents('LogMessagePublished', { + fromBlock : 'latest' + }))[0].returnValues + + assert.equal(log.sender, TokenBridge.address) + + assert.equal(log.payload.length - 2, 266); + + // payload id + assert.equal(log.payload.substr(2, 2), "01"); + + // amount + assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", amount).substring(2)); + + // token + assert.equal(log.payload.substr(68, 64), web3.eth.abi.encodeParameter("address", TokenImplementation.address).substring(2)); + + // chain id + assert.equal(log.payload.substr(132, 4), web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + 64 - 4)) + + // to + assert.equal(log.payload.substr(136, 64), "000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e"); + + // to chain id + assert.equal(log.payload.substr(200, 4), web3.eth.abi.encodeParameter("uint16", 10).substring(2 + 64 - 4)) + + // fee + assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", 123).substring(2)) + }) + + it("should transfer out locked assets for a valid transfer vm", async function() { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + + const token = new web3.eth.Contract(TokenImplementation.abi, TokenImplementation.address); + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + + const accountBalanceBefore = await token.methods.balanceOf(accounts[0]).call(); + const bridgeBalanceBefore = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(accountBalanceBefore.toString(10), "0"); + assert.equal(bridgeBalanceBefore.toString(10), amount); + + const data = "0x" + + "01" + + // amount + "0000000000000000000000000000000000000000000000000de0b6b3a7640000" + + // tokenaddress + web3.eth.abi.encodeParameter("address", TokenImplementation.address).substr(2) + + // tokenchain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // receiver + web3.eth.abi.encodeParameter("address", accounts[0]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + "0000000000000000000000000000000000000000000000000000000000000000"; + + // console.log(data) + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await initialized.methods.completeTransfer("0x" + vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + const accountBalanceAfter = await token.methods.balanceOf(accounts[0]).call(); + const bridgeBalanceAfter = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(accountBalanceAfter.toString(10), amount); + assert.equal(bridgeBalanceAfter.toString(10), "0"); + }) + + it("should mint bridged assets wrappers on transfer from another chain", async function() { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + const wrappedAddress = await initialized.methods.wrappedAsset("0x"+testBridgedAssetChain, "0x"+testBridgedAssetAddress).call(); + const wrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress); + + const totalSupply = await wrappedAsset.methods.totalSupply().call(); + assert.equal(totalSupply.toString(10), "0"); + + // we are using the asset where we created a wrapper in the previous test + const data = "0x" + + "01" + + // amount + "0000000000000000000000000000000000000000000000000de0b6b3a7640000" + + // tokenaddress + testBridgedAssetAddress + + // tokenchain + testBridgedAssetChain + + // receiver + web3.eth.abi.encodeParameter("address", accounts[0]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + "0000000000000000000000000000000000000000000000000000000000000000"; + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await initialized.methods.completeTransfer("0x" + vm).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + const accountBalanceAfter = await wrappedAsset.methods.balanceOf(accounts[0]).call(); + const totalSupplyAfter = await wrappedAsset.methods.totalSupply().call(); + + assert.equal(accountBalanceAfter.toString(10), amount); + assert.equal(totalSupplyAfter.toString(10), amount); + }) + + it("should burn bridged assets wrappers on transfer to another chain", async function() { + + const accounts = await web3.eth.getAccounts(); + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const amount = "1000000000000000000"; + + const wrappedAddress = await initialized.methods.wrappedAsset("0x"+testBridgedAssetChain, "0x"+testBridgedAssetAddress).call(); + const wrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress); + + await wrappedAsset.methods.approve(TokenBridge.address, amount).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + // deposit tokens + + const accountBalanceBefore = await wrappedAsset.methods.balanceOf(accounts[0]).call(); + + assert.equal(accountBalanceBefore.toString(10), amount); + + await initialized.methods.transferTokens( + "0x"+testBridgedAssetChain, + "0x"+testBridgedAssetAddress, + amount, + "11", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + "0", + "234" + ).send({ + value : 0, + from : accounts[0], + gasLimit : 2000000 + }); + + const accountBalanceAfter = await wrappedAsset.methods.balanceOf(accounts[0]).call(); + assert.equal(accountBalanceAfter.toString(10), "0"); + + const bridgeBalanceAfter = await wrappedAsset.methods.balanceOf(TokenBridge.address).call(); + assert.equal(bridgeBalanceAfter.toString(10), "0"); + + const totalSupplyAfter = await wrappedAsset.methods.totalSupply().call(); + assert.equal(totalSupplyAfter.toString(10), "0"); + }) + + it("should handle ETH deposits correctly", async function() { + const accounts = await web3.eth.getAccounts(); + const amount = "100000000000000000"; + + // mint and approve tokens + WETH = (await MockWETH9.new()).address; + const token = new web3.eth.Contract(MockWETH9.abi, WETH); + + // set WETH contract + const mock = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address); + mock.methods.testUpdateWETHAddress(WETH).send({ + from : accounts[0], + gasLimit : 2000000 + }); + + // deposit tokens + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + const totalWETHSupply = await token.methods.totalSupply().call(); + const bridgeBalanceBefore = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(totalWETHSupply.toString(10), "0"); + assert.equal(bridgeBalanceBefore.toString(10), "0"); + + await initialized.methods.wrapAndTransferETH( + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + "123", + "234" + ).send({ + value : amount, + from : accounts[0], + gasLimit : 2000000 + }); + + const totalWETHSupplyAfter = await token.methods.totalSupply().call(); + const bridgeBalanceAfter = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(totalWETHSupplyAfter.toString(10), amount); + assert.equal(bridgeBalanceAfter.toString(10), amount); + + // check transfer log + const wormhole = new web3.eth.Contract(WormholeImplementationFullABI, Wormhole.address); + const log = (await wormhole.getPastEvents('LogMessagePublished', { + fromBlock : 'latest' + }))[0].returnValues + + assert.equal(log.sender, TokenBridge.address) + + assert.equal(log.payload.length - 2, 266); + + // payload id + assert.equal(log.payload.substr(2, 2), "01"); + + // amount + assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", amount).substring(2)); + + // token + assert.equal(log.payload.substr(68, 64), web3.eth.abi.encodeParameter("address", WETH).substring(2)); + + // chain id + assert.equal(log.payload.substr(132, 4), web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + 64 - 4)) + + // to + assert.equal(log.payload.substr(136, 64), "000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e"); + + // to chain id + assert.equal(log.payload.substr(200, 4), web3.eth.abi.encodeParameter("uint16", 10).substring(2 + 64 - 4)) + + // fee + assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", 123).substring(2)) + }) + + it("should handle ETH withdrawals and fees correctly", async function() { + const accounts = await web3.eth.getAccounts(); + const amount = "100000000000000000"; + const fee = "50000000000000000"; + + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + const token = new web3.eth.Contract(MockWETH9.abi, WETH); + + const totalSupply = await token.methods.totalSupply().call(); + assert.equal(totalSupply.toString(10), amount); + + const feeRecipientBalanceBefore = await web3.eth.getBalance(accounts[0]); + const accountBalanceBefore = await web3.eth.getBalance(accounts[1]); + + // we are using the asset where we created a wrapper in the previous test + const data = "0x" + + "01" + + // amount + web3.eth.abi.encodeParameter("uint256", amount).substr(2) + + // tokenaddress + web3.eth.abi.encodeParameter("address", WETH).substr(2) + + // tokenchain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // receiver + web3.eth.abi.encodeParameter("address", accounts[1]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + web3.eth.abi.encodeParameter("uint256", fee).substr(2); + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + const transferTX = await initialized.methods.completeTransferAndUnwrapETH("0x" + vm).send({ + from : accounts[0], + gasLimit : 2000000 + }); + + const totalSupplyAfter = await token.methods.totalSupply().call(); + assert.equal(totalSupplyAfter.toString(10), "0"); + + const accountBalanceAfter = await web3.eth.getBalance(accounts[1]); + const feeRecipientBalanceAfter = await web3.eth.getBalance(accounts[0]); + + assert.equal((new BigNumber(accountBalanceAfter)).minus(accountBalanceBefore).toString(10), (new BigNumber(amount)).minus(fee).toString(10)) + assert.ok((new BigNumber(feeRecipientBalanceAfter)).gt(feeRecipientBalanceBefore)) + }) +}); + +const signAndEncodeVM = async function ( + timestamp, + nonce, + emitterChainId, + emitterAddress, + sequence, + data, + signers, + guardianSetIndex, + consistencyLevel +) { + const body = [ + web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2), + web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)), + web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)), + data.substr(2) + ] + + const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join(""))) + + let signatures = ""; + + for (let i in signers) { + const ec = new elliptic.ec("secp256k1"); + const key = ec.keyFromPrivate(signers[i]); + const signature = key.sign(hash.substr(2), {canonical: true}); + + const packSig = [ + web3.eth.abi.encodeParameter("uint8", i).substring(2 + (64 - 2)), + zeroPadBytes(signature.r.toString(16), 32), + zeroPadBytes(signature.s.toString(16), 32), + web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)), + ] + + signatures += packSig.join("") + } + + const vm = [ + web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)), + web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)), + + signatures, + body.join("") + ].join(""); + + return vm +} + +function zeroPadBytes(value, length) { + while (value.length < 2 * length) { + value = "0" + value; + } + return value; +} \ No newline at end of file