ソースを参照

support tokens with transfer fees

Change-Id: Ib6ef2f1680ac845ef0f05c51047846c2633b0d4b
valentin 4 年 前
コミット
1226f85461

+ 52 - 35
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) {

+ 4 - 0
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();
 

+ 1 - 1
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
     }
 

+ 177 - 0
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;
+
+        _;
+    }
+}

+ 92 - 0
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";

+ 413 - 0
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;
+}