From 73e15db86656950e6cc11d476071f04c847a6c8b Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Thu, 23 Dec 2021 03:48:52 +0000 Subject: [PATCH] eth: token bridge transfer with payload --- ethereum/contracts/bridge/Bridge.sol | 113 +++- ethereum/contracts/bridge/BridgeStructs.sol | 32 ++ .../mock/MockTokenBridgeIntegration.sol | 52 ++ ethereum/test/bridge.js | 481 +++++++++++++++++- 4 files changed, 659 insertions(+), 19 deletions(-) create mode 100644 ethereum/contracts/bridge/mock/MockTokenBridgeIntegration.sol diff --git a/ethereum/contracts/bridge/Bridge.sol b/ethereum/contracts/bridge/Bridge.sol index 3f06757f8..45e55292b 100644 --- a/ethereum/contracts/bridge/Bridge.sol +++ b/ethereum/contracts/bridge/Bridge.sol @@ -62,6 +62,16 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { } function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable returns (uint64 sequence) { + BridgeStructs.TransferResult memory transferResult = _wrapAndTransferETH(arbiterFee); + sequence = logTransfer(transferResult.tokenChain, transferResult.tokenAddress, transferResult.normalizedAmount, recipientChain, recipient, transferResult.normalizedArbiterFee, transferResult.wormholeFee, nonce); + } + + function wrapAndTransferETHWithPayload(uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce, bytes memory payload) public payable returns (uint64 sequence) { + BridgeStructs.TransferResult memory transferResult = _wrapAndTransferETH(arbiterFee); + sequence = logTransferWithPayload(transferResult.tokenChain, transferResult.tokenAddress, transferResult.normalizedAmount, recipientChain, recipient, transferResult.normalizedArbiterFee, transferResult.wormholeFee, nonce, payload); + } + + function _wrapAndTransferETH(uint256 arbiterFee) internal returns (BridgeStructs.TransferResult memory transferResult) { uint wormholeFee = wormhole().messageFee(); require(wormholeFee < msg.value, "value is smaller than wormhole fee"); @@ -87,11 +97,27 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { // track and check outstanding token amounts bridgeOut(address(WETH()), normalizedAmount); - sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), normalizedAmount, recipientChain, recipient, normalizedArbiterFee, wormholeFee, nonce); + transferResult = BridgeStructs.TransferResult({ + tokenChain : chainId(), + tokenAddress : bytes32(uint256(uint160(address(WETH())))), + normalizedAmount : normalizedAmount, + normalizedArbiterFee : normalizedArbiterFee, + wormholeFee : wormholeFee + }); + } + + function transferTokens(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable nonReentrant returns (uint64 sequence) { + BridgeStructs.TransferResult memory transferResult = _transferTokens(token, amount, arbiterFee); + sequence = logTransfer(transferResult.tokenChain, transferResult.tokenAddress, transferResult.normalizedAmount, recipientChain, recipient, transferResult.normalizedArbiterFee, transferResult.wormholeFee, nonce); + } + + function transferTokensWithPayload(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce, bytes memory payload) public payable nonReentrant returns (uint64 sequence) { + BridgeStructs.TransferResult memory transferResult = _transferTokens(token, amount, arbiterFee); + sequence = logTransferWithPayload(transferResult.tokenChain, transferResult.tokenAddress, transferResult.normalizedAmount, recipientChain, recipient, transferResult.normalizedArbiterFee, transferResult.wormholeFee, nonce, payload); } // Initiate a Transfer - function transferTokens(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable nonReentrant returns (uint64 sequence) { + function _transferTokens(address token, uint256 amount, uint256 arbiterFee) internal returns (BridgeStructs.TransferResult memory transferResult) { // determine token parameters uint16 tokenChain; bytes32 tokenAddress; @@ -139,7 +165,13 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { bridgeOut(token, normalizedAmount); } - sequence = logTransfer(tokenChain, tokenAddress, normalizedAmount, recipientChain, recipient, normalizedArbiterFee, msg.value, nonce); + transferResult = BridgeStructs.TransferResult({ + tokenChain : tokenChain, + tokenAddress : tokenAddress, + normalizedAmount : normalizedAmount, + normalizedArbiterFee : normalizedArbiterFee, + wormholeFee : msg.value + }); } function normalizeAmount(uint256 amount, uint8 decimals) internal pure returns(uint256){ @@ -176,6 +208,27 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { }(nonce, encoded, 15); } + function logTransferWithPayload(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint256 callValue, uint32 nonce, bytes memory payload) internal returns (uint64 sequence) { + require(fee <= amount, "fee exceeds amount"); + + BridgeStructs.TransferWithPayload memory transfer = BridgeStructs.TransferWithPayload({ + payloadID : 3, + amount : amount, + tokenAddress : tokenAddress, + tokenChain : tokenChain, + to : recipient, + toChain : recipientChain, + fee : fee, + payload : payload + }); + + bytes memory encoded = encodeTransferWithPayload(transfer); + + sequence = wormhole().publishMessage{ + value : callValue + }(nonce, encoded, 15); + } + function updateWrapped(bytes memory encodedVm) external returns (address token) { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm); @@ -244,16 +297,24 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { setWrappedAsset(meta.tokenChain, meta.tokenAddress, token); } + function completeTransferWithPayload(bytes memory encodedVm, address feeRecipient) public returns (bytes memory) { + return _completeTransfer(encodedVm, false, feeRecipient); + } + + function completeTransferAndUnwrapETHWithPayload(bytes memory encodedVm, address feeRecipient) public returns (bytes memory) { + return _completeTransfer(encodedVm, true, feeRecipient); + } + function completeTransfer(bytes memory encodedVm) public { - _completeTransfer(encodedVm, false); + _completeTransfer(encodedVm, false, msg.sender); } function completeTransferAndUnwrapETH(bytes memory encodedVm) public { - _completeTransfer(encodedVm, true); + _completeTransfer(encodedVm, true, msg.sender); } // Execute a Transfer message - function _completeTransfer(bytes memory encodedVm, bool unwrapWETH) internal { + function _completeTransfer(bytes memory encodedVm, bool unwrapWETH, address feeRecipient) internal returns (bytes memory) { (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm); require(valid, reason); @@ -261,6 +322,12 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { BridgeStructs.Transfer memory transfer = parseTransfer(vm.payload); + // payload 3 must be redeemed by the designated proxy contract + address transferRecipient = address(uint160(uint256(transfer.to))); + if (transfer.payloadID == 3) { + require(msg.sender == transferRecipient, "invalid sender"); + } + require(!isTransferCompleted(vm.hash), "transfer already completed"); setTransferCompleted(vm.hash); @@ -290,26 +357,28 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { uint256 nativeFee = deNormalizeAmount(transfer.fee, decimals); // transfer fee to arbiter - if (nativeFee > 0) { + if (nativeFee > 0 && transferRecipient != feeRecipient) { require(nativeFee <= nativeAmount, "fee higher than transferred amount"); if (unwrapWETH) { WETH().withdraw(nativeFee); - payable(msg.sender).transfer(nativeFee); + payable(feeRecipient).transfer(nativeFee); } else { if (transfer.tokenChain != chainId()) { // mint wrapped asset - TokenImplementation(address(transferToken)).mint(msg.sender, nativeFee); + TokenImplementation(address(transferToken)).mint(feeRecipient, nativeFee); } else { - SafeERC20.safeTransfer(transferToken, msg.sender, nativeFee); + SafeERC20.safeTransfer(transferToken, feeRecipient, nativeFee); } } + } else { + // set fee to zero in case transferRecipient == feeRecipient + nativeFee = 0; } // transfer bridged amount to recipient uint transferAmount = nativeAmount - nativeFee; - address transferRecipient = address(uint160(uint256(transfer.to))); if (unwrapWETH) { WETH().withdraw(transferAmount); @@ -323,6 +392,8 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { SafeERC20.safeTransfer(transferToken, transferRecipient, transferAmount); } } + + return vm.payload; } function bridgeOut(address token, uint normalizedAmount) internal { @@ -366,6 +437,19 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { ); } + function encodeTransferWithPayload(BridgeStructs.TransferWithPayload 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, + transfer.payload + ); + } + function parseAssetMeta(bytes memory encoded) public pure returns (BridgeStructs.AssetMeta memory meta) { uint index = 0; @@ -398,7 +482,7 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { transfer.payloadID = encoded.toUint8(index); index += 1; - require(transfer.payloadID == 1, "invalid Transfer"); + require(transfer.payloadID == 1 || transfer.payloadID == 3, "invalid Transfer"); transfer.amount = encoded.toUint256(index); index += 32; @@ -418,7 +502,8 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { transfer.fee = encoded.toUint256(index); index += 32; - require(encoded.length == index, "invalid Transfer"); + // payload 3 allows for an arbitrary additional payload + require(encoded.length == index || transfer.payloadID == 3, "invalid Transfer"); } function bytes32ToString(bytes32 input) internal pure returns (string memory) { @@ -435,4 +520,4 @@ contract Bridge is BridgeGovernance, ReentrancyGuard { // we need to accept ETH sends to unwrap WETH receive() external payable {} -} \ No newline at end of file +} diff --git a/ethereum/contracts/bridge/BridgeStructs.sol b/ethereum/contracts/bridge/BridgeStructs.sol index 821838570..29ce6c8f6 100644 --- a/ethereum/contracts/bridge/BridgeStructs.sol +++ b/ethereum/contracts/bridge/BridgeStructs.sol @@ -21,6 +21,38 @@ contract BridgeStructs { uint256 fee; } + struct TransferWithPayload { + // PayloadID uint8 = 3 + 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; + // An arbitrary payload + bytes payload; + } + + struct TransferResult { + // Chain ID of the token + uint16 tokenChain; + // Address of the token. Left-zero-padded if shorter than 32 bytes + bytes32 tokenAddress; + // Amount being transferred (big-endian uint256) + uint256 normalizedAmount; + // Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount. + uint256 normalizedArbiterFee; + // Portion of msg.value to be paid as the core bridge fee + uint wormholeFee; + } + struct AssetMeta { // PayloadID uint8 = 2 uint8 payloadID; diff --git a/ethereum/contracts/bridge/mock/MockTokenBridgeIntegration.sol b/ethereum/contracts/bridge/mock/MockTokenBridgeIntegration.sol new file mode 100644 index 000000000..e47544eb3 --- /dev/null +++ b/ethereum/contracts/bridge/mock/MockTokenBridgeIntegration.sol @@ -0,0 +1,52 @@ +// contracts/Implementation.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 "../../interfaces/IWormhole.sol"; + +interface ITokenBridge { + function completeTransferWithPayload(bytes memory encodedVm, address feeRecipient) external returns (bytes memory); + function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress) external view returns (address); +} + +contract MockTokenBridgeIntegration { + using BytesLib for bytes; + using SafeERC20 for IERC20; + address tokenBridgeAddress; + function completeTransferAndSwap(bytes memory encodedVm) public { + // token bridge transfers are 133 bytes, our additional payload is 32 bytes = 165 + // len - 165 + 33 = len - 132 + bytes32 tokenAddress = encodedVm.toBytes32(encodedVm.length-132); + // len - 165 + 65 = len - 100 + uint16 tokenChainId = encodedVm.toUint16(encodedVm.length-100); + address wrappedAddress = tokenBridge().wrappedAsset(tokenChainId, tokenAddress); + IERC20 transferToken = IERC20(wrappedAddress); + uint256 balanceBefore = transferToken.balanceOf(address(this)); + bytes memory payload = tokenBridge().completeTransferWithPayload(encodedVm, msg.sender); + // make sure this vm is a payload 3 + uint8 payloadType = payload.toUint8(0); + require(payloadType == 3, "invalid payload type"); + bytes32 vmTokenAddress = payload.toBytes32(33); + require(tokenAddress == vmTokenAddress, 'Address parsed from VAA and payload do not match'); + uint16 vmTokenChainId = payload.toUint16(65); + require(tokenChainId == vmTokenChainId, 'ChainId parsed from VAA and payload do not match'); + uint256 balanceAfter = transferToken.balanceOf(address(this)); + uint256 amount = balanceAfter - balanceBefore; + // additional field(s) + bytes32 receiver = payload.toBytes32(133); + address receiverAddress = address(uint160(uint256(receiver))); + transferToken.safeTransfer(receiverAddress, amount); + } + function tokenBridge() private view returns (ITokenBridge) { + return ITokenBridge(tokenBridgeAddress); + } + function setup(address _tokenBridge) public { + tokenBridgeAddress = _tokenBridge; + } +} diff --git a/ethereum/test/bridge.js b/ethereum/test/bridge.js index da2303c0d..f791d91e6 100644 --- a/ethereum/test/bridge.js +++ b/ethereum/test/bridge.js @@ -8,6 +8,7 @@ const BridgeImplementation = artifacts.require("BridgeImplementation"); const TokenImplementation = artifacts.require("TokenImplementation"); const FeeToken = artifacts.require("FeeToken"); const MockBridgeImplementation = artifacts.require("MockBridgeImplementation"); +const MockTokenBridgeIntegration = artifacts.require("MockTokenBridgeIntegration"); const MockWETH9 = artifacts.require("MockWETH9"); const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; @@ -23,7 +24,7 @@ contract("Bridge", function () { const testChainId = "2"; const testGovernanceChainId = "1"; const testGovernanceContract = "0x0000000000000000000000000000000000000000000000000000000000000004"; - let WETH = process.env.BRIDGE_INIT_WETH;; + let WETH = process.env.BRIDGE_INIT_WETH; const testForeignChainId = "1"; const testForeignBridgeContract = "0x000000000000000000000000000000000000000000000000000000000000ffff"; const testBridgedAssetChain = "0001"; @@ -614,6 +615,147 @@ contract("Bridge", function () { assert.equal(bridgeBalanceAfter.toString(10), "0"); }) + it("should deposit and log transfer with payload correctly", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + const fee = "100000000000000000"; + + // 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(bridgeBalanceBefore.toString(10), "0"); + + const additionalPayload = "abc123" + + await initialized.methods.transferTokensWithPayload( + TokenImplementation.address, + amount, + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + fee, + "234", + "0x"+additionalPayload + ).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), new BigNumber(accountBalanceBefore).minus(amount).toString(10)); + 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 - additionalPayload.length, 266); + + // payload id + assert.equal(log.payload.substr(2, 2), "03"); + + // 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", 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", new BigNumber(fee).div(1e10).toString()).substring(2)) + + // payload + assert.equal(log.payload.substr(268), additionalPayload) + }) + + it("should transfer out locked assets for a valid transfer with payload vm", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + const feeRecipient = accounts[1]; + + 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(bridgeBalanceBefore.toString(10), amount); + + const data = "0x" + + "03" + + // amount + web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) + + // 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" + + // additional payload + "abc123"; + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await initialized.methods.completeTransferWithPayload("0x" + vm, feeRecipient).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), new BigNumber(accountBalanceBefore).plus(amount).toString(10)); + assert.equal(bridgeBalanceAfter.toString(10), "0"); + }) + it("should mint bridged assets wrappers on transfer from another chain and handle fees correctly", async function () { const accounts = await web3.eth.getAccounts(); const amount = "1000000000000000000"; @@ -678,11 +820,200 @@ contract("Bridge", function () { }); }) - it("should burn bridged assets wrappers on transfer to another chain", async function () { + it("should handle additional data on token bridge transfer with payload in single transaction when feeRecipient == transferRecipient", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + const fee = "1000000000000000"; + const feeRecipient = accounts[0]; // same account as sender to check feeRecipient != transferRecipient condition + 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 accountBalanceBefore = await wrappedAsset.methods.balanceOf(accounts[0]).call(); + const totalSupplyBefore = await wrappedAsset.methods.totalSupply().call(); + + // we are using the asset where we created a wrapper in the previous test + const data = "0x" + + "03" + + // amount + web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) + + // tokenaddress + testBridgedAssetAddress + + // tokenchain + testBridgedAssetChain + + // receiver (must be self msg.sender) + web3.eth.abi.encodeParameter("address", accounts[0]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2) + + // additional payload + web3.eth.abi.encodeParameter("address", accounts[1]).substr(2); + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 1, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await initialized.methods.completeTransferWithPayload("0x" + vm, feeRecipient).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), new BigNumber(accountBalanceBefore).plus(amount).toString(10)); + assert.equal(totalSupplyAfter.toString(10), new BigNumber(totalSupplyBefore).plus(amount).toString(10)); + }) + + it("should not allow a redemption from msg.sender other than 'to' on token bridge transfer with payload", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + const fee = "1000000000000000"; + const feeRecipient = accounts[1]; + + 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); + + // we are using the asset where we created a wrapper in the previous test + const data = "0x" + + "03" + + // amount + web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) + + // tokenaddress + testBridgedAssetAddress + + // tokenchain + testBridgedAssetChain + + // receiver (must be self msg.sender) + web3.eth.abi.encodeParameter("address", accounts[0]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2) + + // additional payload + web3.eth.abi.encodeParameter("address", accounts[1]).substr(2); + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 1, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + let hadSenderError = false + try { + await initialized.methods.completeTransferWithPayload("0x" + vm, feeRecipient).send({ + value: 0, + from: accounts[1], + gasLimit: 2000000 + }); + } catch(e) { + hadSenderError = e.message.includes('revert invalid sender') + } + assert.equal(hadSenderError, true) + }) + + it("should allow a redemption from msg.sender == 'to' on token bridge transfer with payload and check that sender recieves fee", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "1000000000000000000"; + const fee = "1000000000000000"; + const feeRecipient = accounts[1]; + + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + + mock = (await MockTokenBridgeIntegration.new()).address; + const MockIntegration = new web3.eth.Contract(MockTokenBridgeIntegration.abi, mock); + await MockIntegration.methods.setup(TokenBridge.address).send({ + value: 0, + from: accounts[1], + gasLimit: 2000000 + }); + + const wrappedAddress = await initialized.methods.wrappedAsset("0x" + testBridgedAssetChain, "0x" + testBridgedAssetAddress).call(); + const wrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress); + + const accountBalanceBefore = await wrappedAsset.methods.balanceOf(accounts[0]).call(); + const senderBalanceBefore = await wrappedAsset.methods.balanceOf(accounts[1]).call(); + const totalSupplyBefore = await wrappedAsset.methods.totalSupply().call(); + + // we are using the asset where we created a wrapper in the previous test + const data = "0x" + + "03" + + // amount + web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) + + // tokenaddress + testBridgedAssetAddress + + // tokenchain + testBridgedAssetChain + + // receiver + web3.eth.abi.encodeParameter("address", mock).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2) + + // additional payload + web3.eth.abi.encodeParameter("address", accounts[0]).substr(2); + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 2, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await MockIntegration.methods.completeTransferAndSwap("0x" + vm).send({ + value: 0, + from: feeRecipient, + gasLimit: 2000000 + }); + + const accountBalanceAfter = await wrappedAsset.methods.balanceOf(accounts[0]).call(); + const senderBalanceAfter = await wrappedAsset.methods.balanceOf(accounts[1]).call(); + const totalSupplyAfter = await wrappedAsset.methods.totalSupply().call(); + + // account for fees + const amountLessFees = new BigNumber(amount).minus(fee); + + // sender should receive fees + assert.equal(accountBalanceAfter.toString(10), new BigNumber(accountBalanceBefore).plus(amountLessFees).toString(10)); + assert.equal(senderBalanceAfter.toString(10), new BigNumber(senderBalanceBefore).plus(fee)); + assert.equal(totalSupplyAfter.toString(10), new BigNumber(totalSupplyBefore).plus(amount).toString(10)); + }) + + 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 amount = "2999000000000000000"; + const wrappedFeesPaid = "1000000000000000"; const wrappedAddress = await initialized.methods.wrappedAsset("0x" + testBridgedAssetChain, "0x" + testBridgedAssetAddress).call(); const wrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress); @@ -719,7 +1050,7 @@ contract("Bridge", function () { assert.equal(bridgeBalanceAfter.toString(10), "0"); const totalSupplyAfter = await wrappedAsset.methods.totalSupply().call(); - assert.equal(totalSupplyAfter.toString(10), "0"); + assert.equal(totalSupplyAfter.toString(10), wrappedFeesPaid); }) it("should handle ETH deposits correctly", async function () { @@ -856,6 +1187,146 @@ contract("Bridge", function () { assert.ok((new BigNumber(feeRecipientBalanceAfter)).gt(feeRecipientBalanceBefore)) }) + it("should handle ETH deposits with payload correctly", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "100000000000000000"; + const fee = "10000000000000000"; + + // 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"); + + const additionalPayload = "abc123" + + await initialized.methods.wrapAndTransferETHWithPayload( + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + fee, + "234", + "0x"+additionalPayload + ).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 - additionalPayload.length, 266); + + // payload id + assert.equal(log.payload.substr(2, 2), "03"); + + // 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)) + + // payload + assert.equal(log.payload.substr(268), additionalPayload) + }) + + it("should handle ETH withdrawals with payload correctly", async function () { + const accounts = await web3.eth.getAccounts(); + const amount = "100000000000000000"; + const fee = "0"; + const feeRecipient = accounts[1]; + + 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 accountBalanceBefore = await web3.eth.getBalance(accounts[0]); + + // we are using the asset where we created a wrapper in the previous test + const data = "0x" + + "03" + + // 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[0]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) + + // fee + web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).toString()).substring(2) + + // additional payload + "abc123" + + const vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + const transferTX = await initialized.methods.completeTransferAndUnwrapETHWithPayload("0x" + vm, feeRecipient).send({ + from: accounts[0], //must be same as receiver + gasLimit: 2000000 + }); + + const totalSupplyAfter = await token.methods.totalSupply().call(); + assert.equal(totalSupplyAfter.toString(10), "0"); + + const accountBalanceAfter = await web3.eth.getBalance(accounts[0]); + + assert.ok((new BigNumber(accountBalanceAfter)).gt(accountBalanceBefore)) + }) + it("should revert on transfer out of a total of > max(uint64) tokens", async function () { const accounts = await web3.eth.getAccounts(); const supply = "184467440737095516160000000000"; @@ -970,4 +1441,4 @@ function zeroPadBytes(value, length) { value = "0" + value; } return value; -} \ No newline at end of file +}