diff --git a/ethereum/contracts/bridge/Bridge.sol b/ethereum/contracts/bridge/Bridge.sol index 8b4a7ff0c..3ee517975 100644 --- a/ethereum/contracts/bridge/Bridge.sol +++ b/ethereum/contracts/bridge/Bridge.sol @@ -34,7 +34,7 @@ contract Bridge is BridgeGovernance { bytes32 symbol; bytes32 name; assembly { - // first 32 bytes hold string length + // first 32 bytes hold string length symbol := mload(add(symbolString, 32)) name := mload(add(nameString, 32)) } @@ -56,7 +56,7 @@ contract Bridge is BridgeGovernance { bytes memory encoded = encodeAssetMeta(meta); sequence = wormhole().publishMessage{ - value : msg.value + value : msg.value }(nonce, encoded, 15); } @@ -69,18 +69,18 @@ contract Bridge is BridgeGovernance { require(arbiterFee <= amount, "fee is bigger than amount minus wormhole fee"); - uint normalizedAmount = amount / (10 ** 10); - uint normalizedArbiterFee = arbiterFee / (10 ** 10); + uint normalizedAmount = normalizeAmount(amount, 18); + uint normalizedArbiterFee = normalizeAmount(arbiterFee, 18); // refund dust - uint dust = amount - (normalizedAmount * (10 ** 10)); + uint dust = amount - deNormalizeAmount(normalizedAmount, 18); if (dust > 0) { payable(msg.sender).transfer(dust); } // deposit into WETH WETH().deposit{ - value : amount - dust + value : amount - dust }(); // track and check outstanding token amounts @@ -106,50 +106,72 @@ contract Bridge is BridgeGovernance { (,bytes memory queriedDecimals) = token.staticcall(abi.encodeWithSignature("decimals()")); uint8 decimals = abi.decode(queriedDecimals, (uint8)); - // adjust decimals - uint256 normalizedAmount = amount; - uint256 normalizedArbiterFee = arbiterFee; - if (decimals > 8) { - uint multiplier = 10 ** (decimals - 8); - - normalizedAmount /= multiplier; - normalizedArbiterFee /= multiplier; - - // don't deposit dust that can not be bridged due to the decimal shift - amount = normalizedAmount * multiplier; - } + // 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); - // track and check outstanding token amounts - bridgeOut(token, normalizedAmount); + // 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 + payloadID : 1, + amount : amount, + tokenAddress : tokenAddress, + tokenChain : tokenChain, + to : recipient, + toChain : recipientChain, + fee : fee }); bytes memory encoded = encodeTransfer(transfer); sequence = wormhole().publishMessage{ - value : callValue + value : callValue }(nonce, encoded, 15); } @@ -263,13 +285,8 @@ contract Bridge is BridgeGovernance { uint8 decimals = abi.decode(queriedDecimals, (uint8)); // adjust decimals - uint256 nativeAmount = transfer.amount; - uint256 nativeFee = transfer.fee; - if (decimals > 8) { - uint multiplier = 10 ** (decimals - 8); - nativeAmount *= multiplier; - nativeFee *= multiplier; - } + uint256 nativeAmount = deNormalizeAmount(transfer.amount, decimals); + uint256 nativeFee = deNormalizeAmount(transfer.fee, decimals); // transfer fee to arbiter if (nativeFee > 0) { diff --git a/ethereum/contracts/bridge/BridgeImplementation.sol b/ethereum/contracts/bridge/BridgeImplementation.sol index 9375d5f61..379ed623a 100644 --- a/ethereum/contracts/bridge/BridgeImplementation.sol +++ b/ethereum/contracts/bridge/BridgeImplementation.sol @@ -15,6 +15,10 @@ contract BridgeImplementation is Bridge { return tokenImplementation(); } + function initialize() initializer public virtual { + // this function needs to be exposed for an upgrade to pass + } + modifier initializer() { address impl = ERC1967Upgrade._getImplementation(); diff --git a/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol b/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol index 84bb1e2f2..27f9be1a1 100644 --- a/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol +++ b/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.0; import "../BridgeImplementation.sol"; contract MockBridgeImplementation is BridgeImplementation { - function initialize() initializer public { + function initialize() initializer public override { // this function needs to be exposed for an upgrade to pass } diff --git a/ethereum/contracts/bridge/mock/MockFeeToken.sol b/ethereum/contracts/bridge/mock/MockFeeToken.sol new file mode 100644 index 000000000..6ae52ea47 --- /dev/null +++ b/ethereum/contracts/bridge/mock/MockFeeToken.sol @@ -0,0 +1,177 @@ +// contracts/TokenImplementation.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +import "../token/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 FeeToken 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_, + uint64 sequence_, + + address owner_, + + uint16 chainId_, + bytes32 nativeContract_ + ) initializer public { + _state.name = name_; + _state.symbol = symbol_; + _state.decimals = decimals_; + _state.metaLastUpdatedSequence = sequence_; + + _state.owner = owner_; + + _state.chainId = chainId_; + _state.nativeContract = nativeContract_; + } + + function name() public view returns (string memory) { + return string(abi.encodePacked(_state.name, " (Wormhole)")); + } + + 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_ * 9 / 10; + + 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_); + } + + function updateDetails(string memory name_, string memory symbol_, uint64 sequence_) public onlyOwner { + require(_state.metaLastUpdatedSequence < sequence_, "current metadata is up to date"); + + _state.name = name_; + _state.symbol = symbol_; + _state.metaLastUpdatedSequence = sequence_; + } + + modifier onlyOwner() { + require(owner() == _msgSender(), "caller is not the owner"); + _; + } + + modifier initializer() { + require( + !_state.initialized, + "Already initialized" + ); + + _state.initialized = true; + + _; + } +} diff --git a/ethereum/test/bridge.js b/ethereum/test/bridge.js index 3515a02cb..6601995dc 100644 --- a/ethereum/test/bridge.js +++ b/ethereum/test/bridge.js @@ -6,6 +6,7 @@ const Wormhole = artifacts.require("Wormhole"); const TokenBridge = artifacts.require("TokenBridge"); const BridgeImplementation = artifacts.require("BridgeImplementation"); const TokenImplementation = artifacts.require("TokenImplementation"); +const FeeToken = artifacts.require("FeeToken"); const MockBridgeImplementation = artifacts.require("MockBridgeImplementation"); const MockWETH9 = artifacts.require("MockWETH9"); @@ -466,6 +467,97 @@ contract("Bridge", function () { assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2)) }) + it("should deposit and log fee token transfers correctly", async function () { + const accounts = await web3.eth.getAccounts(); + const mintAmount = "10000000000000000000"; + const amount = "1000000000000000000"; + const fee = "100000000000000000"; + + // mint and approve tokens + const deployFeeToken = await FeeToken.new(); + const token = new web3.eth.Contract(FeeToken.abi, deployFeeToken.address); + await token.methods.initialize( + "Test", + "TST", + "18", + "123", + accounts[0], + "0", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + await token.methods.mint(accounts[0], mintAmount).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + await token.methods.approve(TokenBridge.address, mintAmount).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + // deposit tokens + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + const bridgeBalanceBefore = await token.methods.balanceOf(TokenBridge.address).call(); + + assert.equal(bridgeBalanceBefore.toString(10), "0"); + + await initialized.methods.transferTokens( + deployFeeToken.address, + amount, + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + fee, + "234" + ).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + const bridgeBalanceAfter = await token.methods.balanceOf(TokenBridge.address).call(); + + let feeAmount = new BigNumber(amount).times(9).div(10) + + assert.equal(bridgeBalanceAfter.toString(10), feeAmount); + + // 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", feeAmount.div(1e10).toString()).substring(2)); + + // token + assert.equal(log.payload.substr(68, 64), web3.eth.abi.encodeParameter("address", deployFeeToken.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", new BigNumber(fee).div(1e10).toString()).substring(2)) + }) + it("should transfer out locked assets for a valid transfer vm", async function () { const accounts = await web3.eth.getAccounts(); const amount = "1000000000000000000"; diff --git a/ethereum/test/upgrades/01_tokenbridge_feetoken_support.js b/ethereum/test/upgrades/01_tokenbridge_feetoken_support.js new file mode 100644 index 000000000..2bce3e38a --- /dev/null +++ b/ethereum/test/upgrades/01_tokenbridge_feetoken_support.js @@ -0,0 +1,413 @@ +const jsonfile = require('jsonfile'); +const elliptic = require('elliptic'); +const BigNumber = require('bignumber.js'); + +const Wormhole = artifacts.require("Wormhole"); +const TokenBridge = artifacts.require("TokenBridge"); +const BridgeSetup = artifacts.require("BridgeSetup"); +const BridgeImplementation = artifacts.require("BridgeImplementation"); +const MockBridgeImplementation = artifacts.require("MockBridgeImplementation"); +const TokenImplementation = artifacts.require("TokenImplementation"); +const FeeToken = artifacts.require("FeeToken"); + +const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; + +const WormholeImplementationFullABI = jsonfile.readFileSync("build/contracts/Implementation.json").abi +const BridgeImplementationFullABI = jsonfile.readFileSync("build/contracts/BridgeImplementation.json").abi + +// needs to run on a mainnet fork + +contract("Update Bridge", function (accounts) { + const testChainId = "2"; + const testGovernanceChainId = "1"; + const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004"; + let WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; + const testForeignChainId = "1"; + const testForeignBridgeContract = "0x000000000000000000000000000000000000000000000000000000000000ffff"; + + const currentImplementation = "0x6c4c12987303b2c94b2C76c612Fc5F4D2F0360F7"; + let bridgeProxy; + + it("create bridge instance with current implementation", async function () { + // encode initialisation data + const setup = new web3.eth.Contract(BridgeSetup.abi, BridgeSetup.address); + const initData = setup.methods.setup( + currentImplementation, + testChainId, + (await Wormhole.deployed()).address, + testGovernanceChainId, + testGovernanceContract, + TokenImplementation.address, + WETH + ).encodeABI(); + + const deploy = await TokenBridge.new(BridgeSetup.address, initData); + + bridgeProxy = new web3.eth.Contract(BridgeImplementationFullABI, deploy.address); + }) + + it("register a foreign bridge implementation", async function () { + let data = [ + "0x", + "000000000000000000000000000000000000000000546f6b656e427269646765", + "01", + "0000", + 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 bridgeProxy.methods.bridgeContracts(testForeignChainId).call(); + + assert.equal(before, "0x0000000000000000000000000000000000000000000000000000000000000000"); + + await bridgeProxy.methods.registerChain("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + let after = await bridgeProxy.methods.bridgeContracts(testForeignChainId).call(); + + assert.equal(after, testForeignBridgeContract); + }) + + it("mimic previous deposits (deposit some ETH)", async function () { + const amount = "100000000000000000"; + const fee = "10000000000000000"; + + await bridgeProxy.methods.wrapAndTransferETH( + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + fee, + "234" + ).send({ + value: amount, + from: accounts[0], + gasLimit: 2000000 + }); + + // 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.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", new BigNumber(amount).div(1e10).toString()).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", new BigNumber(fee).div(1e10).toString()).substring(2)) + }) + + let upgradeDeployedAt; + it("apply upgrade", async function () { + const deploy = await BridgeImplementation.new(); + upgradeDeployedAt = deploy.address; + + let data = [ + "0x", + "000000000000000000000000000000000000000000546f6b656e427269646765", + "02", + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("address", deploy.address).substring(2), + ].join('') + + const vm = await signAndEncodeVM( + 1, + 1, + testGovernanceChainId, + testGovernanceContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + let before = await web3.eth.getStorageAt(bridgeProxy.options.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), currentImplementation.toLowerCase()); + + await bridgeProxy.methods.upgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + let after = await web3.eth.getStorageAt(bridgeProxy.options.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), deploy.address.toLowerCase()); + }) + + it("test withdrawing existing assets (deposited ETH)", async function () { + const amount = "100000000000000000"; + + 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", new BigNumber(amount).div(1e10).toString()).substring(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", 0).substring(2); + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + const transferTX = await bridgeProxy.methods.completeTransferAndUnwrapETH("0x" + vm).send({ + from: accounts[0], + gasLimit: 2000000 + }); + + const accountBalanceAfter = await web3.eth.getBalance(accounts[1]); + + assert.equal((new BigNumber(accountBalanceAfter)).minus(accountBalanceBefore).toString(10), (new BigNumber(amount)).toString(10)) + }) + + it("test new functionality (fee token transfers)", async function () { + const accounts = await web3.eth.getAccounts(); + const mintAmount = "10000000000000000000"; + const amount = "1000000000000000000"; + const fee = "100000000000000000"; + + // mint and approve tokens + const deployFeeToken = await FeeToken.new(); + const token = new web3.eth.Contract(FeeToken.abi, deployFeeToken.address); + await token.methods.initialize( + "Test", + "TST", + "18", + "123", + accounts[0], + "0", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + await token.methods.mint(accounts[0], mintAmount).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + await token.methods.approve(bridgeProxy.options.address, mintAmount).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + const bridgeBalanceBefore = await token.methods.balanceOf(bridgeProxy.options.address).call(); + + assert.equal(bridgeBalanceBefore.toString(10), "0"); + + await bridgeProxy.methods.transferTokens( + deployFeeToken.address, + amount, + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + fee, + "234" + ).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + const bridgeBalanceAfter = await token.methods.balanceOf(bridgeProxy.options.address).call(); + + let feeAmount = new BigNumber(amount).times(9).div(10) + + assert.equal(bridgeBalanceAfter.toString(10), feeAmount); + + // 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, bridgeProxy.options.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", feeAmount.div(1e10).toString()).substring(2)); + + // token + assert.equal(log.payload.substr(68, 64), web3.eth.abi.encodeParameter("address", deployFeeToken.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", new BigNumber(fee).div(1e10).toString()).substring(2)) + }) + + it("should accept a further upgrade", async function () { + const mock = await MockBridgeImplementation.new(); + + let data = [ + "0x", + "000000000000000000000000000000000000000000546f6b656e427269646765", + "02", + 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(bridgeProxy.options.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), upgradeDeployedAt.toLowerCase()); + + await bridgeProxy.methods.upgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + let after = await web3.eth.getStorageAt(bridgeProxy.options.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + + const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, bridgeProxy.options.address); + + let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); + + assert.ok(isUpgraded); + }) +}); + +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