Bläddra i källkod

tokenbridge: decimal shifting & max outstanding

Change-Id: Ia9f27f317fe08c1d8dbb9eaa60e53633acfdd381
valentin 4 år sedan
förälder
incheckning
51e00dc1bf

+ 101 - 23
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) {
+    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)));
+        }
+
+        // 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(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(token), msg.sender, address(this), amount);
 
-            SafeERC20.safeTransferFrom(IERC20(wrapped), 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(wrapped).burn(address(this), amount);
+            TokenImplementation(token).burn(address(this), amount);
         }
 
-        sequence = logTransfer(tokenChain, tokenAddress, amount, recipientChain, recipient, fee, msg.value, nonce);
+        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;

+ 8 - 0
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 {

+ 5 - 0
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;
     }
 }

+ 11 - 0
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;
     }

+ 72 - 16
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 (