eth: token bridge transfer with payload

This commit is contained in:
Evan Gray 2021-12-23 03:48:52 +00:00 committed by Evan Gray
parent 08ea624f52
commit 73e15db866
4 changed files with 659 additions and 19 deletions

View File

@ -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 {}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}