tokenbridge: decimal shifting & max outstanding

Change-Id: Ia9f27f317fe08c1d8dbb9eaa60e53633acfdd381
This commit is contained in:
valentin 2021-07-27 11:36:49 +02:00 committed by Valentin Von Albrecht
parent 14e892300c
commit 51e00dc1bf
5 changed files with 200 additions and 42 deletions

View File

@ -60,32 +60,77 @@ contract Bridge is BridgeGovernance {
}(nonce, encoded, 15);
}
function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 fee, uint32 nonce) public payable returns (uint64 sequence) {
function wrapAndTransferETH(uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable returns (uint64 sequence) {
uint wormholeFee = wormhole().messageFee();
require(wormholeFee < msg.value, "value is smaller than wormhole fee");
uint amount = msg.value - wormholeFee;
require(arbiterFee <= amount, "fee is bigger than amount minus wormhole fee");
uint normalizedAmount = amount / (10**10);
uint normalizedArbiterFee = arbiterFee / (10**10);
// refund dust
uint dust = amount - (normalizedAmount * (10**10));
if (dust > 0) {
payable(msg.sender).transfer(dust);
}
// deposit into WETH
WETH().deposit{
value : msg.value - wormholeFee
value : amount - dust
}();
sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), msg.value, recipientChain, recipient, fee, wormholeFee, nonce);
// track and check outstanding token amounts
bridgeOut(address(WETH()), normalizedAmount);
sequence = logTransfer(chainId(), bytes32(uint256(uint160(address(WETH())))), normalizedAmount, recipientChain, recipient, normalizedArbiterFee, wormholeFee, nonce);
}
// Initiate a Transfer
function transferTokens(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint32 nonce) public payable returns (uint64 sequence) {
if(tokenChain == chainId()){
SafeERC20.safeTransferFrom(IERC20(address(uint160(uint256(tokenAddress)))), msg.sender, address(this), amount);
} else {
address wrapped = wrappedAsset(tokenChain, tokenAddress);
require(wrapped != address(0), "no wrapper for this token created yet");
SafeERC20.safeTransferFrom(IERC20(wrapped), msg.sender, address(this), amount);
TokenImplementation(wrapped).burn(address(this), amount);
function transferTokens(address token, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 arbiterFee, uint32 nonce) public payable returns (uint64 sequence) {
// determine token parameters
uint16 tokenChain;
bytes32 tokenAddress;
if(isWrappedAsset(token)){
tokenChain = TokenImplementation(token).chainId();
tokenAddress = TokenImplementation(token).nativeContract();
}else{
tokenChain = chainId();
tokenAddress = bytes32(uint256(uint160(token)));
}
sequence = logTransfer(tokenChain, tokenAddress, amount, recipientChain, recipient, fee, msg.value, nonce);
// query tokens decimals
(,bytes memory queriedDecimals) = token.staticcall(abi.encodeWithSignature("decimals()"));
uint8 decimals = abi.decode(queriedDecimals, (uint8));
// adjust decimals
uint256 normalizedAmount = amount;
uint256 normalizedArbiterFee = arbiterFee;
if(decimals > 8) {
uint multiplier = 10**(decimals - 8);
normalizedAmount /= multiplier;
normalizedArbiterFee /= multiplier;
// don't deposit dust that can not be bridged due to the decimal shift
amount = normalizedAmount * multiplier;
}
if(tokenChain == chainId()){
SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);
// track and check outstanding token amounts
bridgeOut(token, normalizedAmount);
} else {
SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount);
TokenImplementation(token).burn(address(this), amount);
}
sequence = logTransfer(tokenChain, tokenAddress, normalizedAmount, recipientChain, recipient, normalizedArbiterFee, msg.value, nonce);
}
function logTransfer(uint16 tokenChain, bytes32 tokenAddress, uint256 amount, uint16 recipientChain, bytes32 recipient, uint256 fee, uint256 callValue, uint32 nonce) internal returns (uint64 sequence) {
@ -180,39 +225,72 @@ contract Bridge is BridgeGovernance {
IERC20 transferToken;
if(transfer.tokenChain == chainId()){
transferToken = IERC20(address(uint160(uint256(transfer.tokenAddress))));
// track outstanding token amounts
bridgedIn(address(transferToken), transfer.amount);
} else {
address wrapped = wrappedAsset(transfer.tokenChain, transfer.tokenAddress);
require(wrapped != address(0), "no wrapper for this token created yet");
TokenImplementation(wrapped).mint(address(this), transfer.amount);
transferToken = IERC20(wrapped);
}
if(transfer.fee > 0) {
require(transfer.fee <= transfer.amount, "fee higher than transferred amount");
require(unwrapWETH == false || address(transferToken) == address(WETH()), "invalid token, can only unwrap WETH");
// query decimals
(,bytes memory queriedDecimals) = address(transferToken).staticcall(abi.encodeWithSignature("decimals()"));
uint8 decimals = abi.decode(queriedDecimals, (uint8));
// adjust decimals
uint256 nativeAmount = transfer.amount;
uint256 nativeFee = transfer.fee;
if(decimals > 8) {
uint multiplier = 10**(decimals - 8);
nativeAmount *= multiplier;
nativeFee *= multiplier;
}
// mint wrapped asset
if(transfer.tokenChain != chainId()) {
TokenImplementation(address(transferToken)).mint(address(this), nativeAmount);
}
// transfer fee to arbiter
if(nativeFee > 0) {
require(nativeFee <= nativeAmount, "fee higher than transferred amount");
if (unwrapWETH) {
require(address(transferToken) == address(WETH()), "invalid token, can only unwrap ETH");
WETH().withdraw(transfer.fee);
payable(msg.sender).transfer(transfer.fee);
WETH().withdraw(nativeFee);
payable(msg.sender).transfer(nativeFee);
} else {
SafeERC20.safeTransfer(transferToken, msg.sender, transfer.fee);
SafeERC20.safeTransfer(transferToken, msg.sender, nativeFee);
}
}
uint transferAmount = transfer.amount - transfer.fee;
address payable transferRecipient = payable(address(uint160(uint256(transfer.to))));
// transfer bridged amount to recipient
uint transferAmount = nativeAmount - nativeFee;
address transferRecipient = address(uint160(uint256(transfer.to)));
if (unwrapWETH) {
require(address(transferToken) == address(WETH()), "invalid token, can only unwrap ETH");
WETH().withdraw(transferAmount);
transferRecipient.transfer(transferAmount);
payable(transferRecipient).transfer(transferAmount);
} else {
SafeERC20.safeTransfer(transferToken, transferRecipient, transferAmount);
}
}
function bridgeOut(address token, uint normalizedAmount) internal {
uint outstanding = outstandingBridged(token);
require(outstanding + normalizedAmount <= type(uint64).max, "transfer exceeds max outstanding bridged token amount");
setOutstandingBridged(token, outstanding + normalizedAmount);
}
function bridgedIn(address token, uint normalizedAmount) internal {
setOutstandingBridged(token, outstandingBridged(token) - normalizedAmount);
}
function verifyBridgeVM(IWormhole.VM memory vm) internal view returns (bool){
if (bridgeContracts(vm.emitterChainId) == vm.emitterAddress) {
return true;

View File

@ -53,6 +53,14 @@ contract BridgeGetters is BridgeState {
function WETH() public view returns (IWETH){
return IWETH(_state.provider.WETH);
}
function outstandingBridged(address token) public view returns (uint256){
return _state.outstandingBridged[token];
}
function isWrappedAsset(address token) public view returns (bool){
return _state.isWrappedAsset[token];
}
}
interface IWETH is IERC20 {

View File

@ -48,5 +48,10 @@ contract BridgeSetters is BridgeState {
function setWrappedAsset(uint16 tokenChainId, bytes32 tokenAddress, address wrapper) internal {
_state.wrappedAssets[tokenChainId][tokenAddress] = wrapper;
_state.isWrappedAsset[wrapper] = true;
}
function setOutstandingBridged(address token, uint256 outstanding) internal {
_state.outstandingBridged[token] = outstanding;
}
}

View File

@ -13,6 +13,11 @@ contract BridgeStorage {
address WETH;
}
struct Asset {
uint16 chainId;
bytes32 assetAddress;
}
struct State {
address payable wormhole;
address tokenImplementation;
@ -31,6 +36,12 @@ contract BridgeStorage {
// Mapping of wrapped assets (chainID => nativeAddress => wrappedAddress)
mapping(uint16 => mapping(bytes32 => address)) wrappedAssets;
// Mapping to safely identify wrapped assets
mapping(address => bool) isWrappedAsset;
// Mapping of native assets to amount outstanding on other chains
mapping(address => uint256) outstandingBridged;
// Mapping of bridge contracts on other chains
mapping(uint16 => bytes32) bridgeImplementations;
}

View File

@ -303,6 +303,8 @@ contract("Bridge", function () {
const wrappedAddress = await initialized.methods.wrappedAsset("0x0001", "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e").call();
assert.ok(await initialized.methods.isWrappedAsset(wrappedAddress).call())
const initializedWrappedAsset = new web3.eth.Contract(TokenImplementation.abi, wrappedAddress);
const symbol = await initializedWrappedAsset.methods.symbol().call();
@ -324,6 +326,7 @@ contract("Bridge", function () {
it("should deposit and log transfers 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);
@ -348,12 +351,11 @@ contract("Bridge", function () {
assert.equal(bridgeBalanceBefore.toString(10), "0");
await initialized.methods.transferTokens(
testChainId,
web3.eth.abi.encodeParameter("address", TokenImplementation.address),
TokenImplementation.address,
amount,
"10",
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
"123",
fee,
"234"
).send({
value : 0,
@ -381,7 +383,7 @@ contract("Bridge", function () {
assert.equal(log.payload.substr(2, 2), "01");
// amount
assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", amount).substring(2));
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));
@ -396,7 +398,7 @@ contract("Bridge", function () {
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", 123).substring(2))
assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2))
})
it("should transfer out locked assets for a valid transfer vm", async function() {
@ -416,7 +418,7 @@ contract("Bridge", function () {
const data = "0x" +
"01" +
// amount
"0000000000000000000000000000000000000000000000000de0b6b3a7640000" +
web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) +
// tokenaddress
web3.eth.abi.encodeParameter("address", TokenImplementation.address).substr(2) +
// tokenchain
@ -428,8 +430,6 @@ contract("Bridge", function () {
// fee
"0000000000000000000000000000000000000000000000000000000000000000";
// console.log(data)
const vm = await signAndEncodeVM(
0,
0,
@ -473,7 +473,7 @@ contract("Bridge", function () {
const data = "0x" +
"01" +
// amount
"0000000000000000000000000000000000000000000000000de0b6b3a7640000" +
web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) +
// tokenaddress
testBridgedAssetAddress +
// tokenchain
@ -534,8 +534,7 @@ contract("Bridge", function () {
assert.equal(accountBalanceBefore.toString(10), amount);
await initialized.methods.transferTokens(
"0x"+testBridgedAssetChain,
"0x"+testBridgedAssetAddress,
wrappedAddress,
amount,
"11",
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
@ -560,6 +559,7 @@ contract("Bridge", function () {
it("should handle ETH deposits correctly", async function() {
const accounts = await web3.eth.getAccounts();
const amount = "100000000000000000";
const fee = "10000000000000000";
// mint and approve tokens
WETH = (await MockWETH9.new()).address;
@ -584,7 +584,7 @@ contract("Bridge", function () {
await initialized.methods.wrapAndTransferETH(
"10",
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
"123",
fee,
"234"
).send({
value : amount,
@ -612,7 +612,7 @@ contract("Bridge", function () {
assert.equal(log.payload.substr(2, 2), "01");
// amount
assert.equal(log.payload.substr(4, 64), web3.eth.abi.encodeParameter("uint256", amount).substring(2));
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));
@ -627,7 +627,7 @@ contract("Bridge", function () {
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", 123).substring(2))
assert.equal(log.payload.substr(204, 64), web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2))
})
it("should handle ETH withdrawals and fees correctly", async function() {
@ -649,7 +649,7 @@ contract("Bridge", function () {
const data = "0x" +
"01" +
// amount
web3.eth.abi.encodeParameter("uint256", amount).substr(2) +
web3.eth.abi.encodeParameter("uint256", new BigNumber(amount).div(1e10).toString()).substring(2) +
// tokenaddress
web3.eth.abi.encodeParameter("address", WETH).substr(2) +
// tokenchain
@ -659,7 +659,7 @@ contract("Bridge", function () {
// receiving chain
web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)) +
// fee
web3.eth.abi.encodeParameter("uint256", fee).substr(2);
web3.eth.abi.encodeParameter("uint256", new BigNumber(fee).div(1e10).toString()).substring(2);
const vm = await signAndEncodeVM(
0,
@ -689,6 +689,62 @@ contract("Bridge", function () {
assert.equal((new BigNumber(accountBalanceAfter)).minus(accountBalanceBefore).toString(10), (new BigNumber(amount)).minus(fee).toString(10))
assert.ok((new BigNumber(feeRecipientBalanceAfter)).gt(feeRecipientBalanceBefore))
})
it("should revert on transfer out of a total of > max(uint64) tokens", async function() {
const accounts = await web3.eth.getAccounts();
const supply = "184467440737095516160000000000";
const firstTransfer = "1000000000000";
// mint and approve tokens
const token = new web3.eth.Contract(TokenImplementation.abi, TokenImplementation.address);
await token.methods.mint(accounts[0], supply).send({
value : 0,
from : accounts[0],
gasLimit : 2000000
});
await token.methods.approve(TokenBridge.address, supply).send({
value : 0,
from : accounts[0],
gasLimit : 2000000
});
// deposit tokens
const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address);
await initialized.methods.transferTokens(
TokenImplementation.address,
firstTransfer,
"10",
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
"0",
"0"
).send({
value : 0,
from : accounts[0],
gasLimit : 2000000
});
let failed = false;
try {
await initialized.methods.transferTokens(
TokenImplementation.address,
new BigNumber(supply).minus(firstTransfer).toString(10),
"10",
"0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e",
"0",
"0"
).send({
value : 0,
from : accounts[0],
gasLimit : 2000000
});
} catch(error) {
assert.equal(error.message, "Returned error: VM Exception while processing transaction: revert transfer exceeds max outstanding bridged token amount")
failed = true
}
assert.ok(failed)
})
});
const signAndEncodeVM = async function (