eth: token bridge transfer with payload
This commit is contained in:
parent
08ea624f52
commit
73e15db866
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue