diff --git a/ethereum/contracts/bridge/Bridge.sol b/ethereum/contracts/bridge/Bridge.sol index 8dcfb520c..2466180f6 100644 --- a/ethereum/contracts/bridge/Bridge.sol +++ b/ethereum/contracts/bridge/Bridge.sol @@ -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; diff --git a/ethereum/contracts/bridge/BridgeGetters.sol b/ethereum/contracts/bridge/BridgeGetters.sol index 7c0284de9..afc67ec77 100644 --- a/ethereum/contracts/bridge/BridgeGetters.sol +++ b/ethereum/contracts/bridge/BridgeGetters.sol @@ -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 { diff --git a/ethereum/contracts/bridge/BridgeSetters.sol b/ethereum/contracts/bridge/BridgeSetters.sol index bf90d3d49..0f8e84b74 100644 --- a/ethereum/contracts/bridge/BridgeSetters.sol +++ b/ethereum/contracts/bridge/BridgeSetters.sol @@ -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; } } \ No newline at end of file diff --git a/ethereum/contracts/bridge/BridgeState.sol b/ethereum/contracts/bridge/BridgeState.sol index dca503148..a73cc56ef 100644 --- a/ethereum/contracts/bridge/BridgeState.sol +++ b/ethereum/contracts/bridge/BridgeState.sol @@ -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; } diff --git a/ethereum/test/bridge.js b/ethereum/test/bridge.js index 555934815..10fc1bc4f 100644 --- a/ethereum/test/bridge.js +++ b/ethereum/test/bridge.js @@ -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 (