ソースを参照

Add smart contract tests

Hendrik Hofstadt 5 年 前
コミット
6e0fe64937

+ 0 - 24
ethereum/.openzeppelin/project.json

@@ -1,24 +0,0 @@
-{
-  "manifestVersion": "2.2",
-  "contracts": {
-    "Wormhole": "Wormhole"
-  },
-  "dependencies": {},
-  "name": "wormhole",
-  "version": "0.1.0",
-  "compiler": {
-    "compilerSettings": {
-      "optimizer": {
-        "enabled": false,
-        "runs": "200"
-      }
-    },
-    "typechain": {
-      "enabled": false
-    },
-    "manager": "openzeppelin",
-    "solcVersion": "0.6.12",
-    "artifactsDir": "build/contracts",
-    "contractsDir": "contracts"
-  }
-}

+ 9 - 1
ethereum/README.md

@@ -11,7 +11,7 @@ wrapped non-ETH assets that are currently held on ETH.
 ### Deploying
 
 To deploy the bridge on Ethereum you first need to compile all smart contracts:
-`npx oz compile`
+`npx truffle compile`
 
 To deploy you can either use the bytecode from the `build/contracts` folder or the oz cli `oz deploy <Contract>` 
 ([Documentation](https://docs.openzeppelin.com/learn/deploying-and-interacting)).
@@ -22,6 +22,14 @@ Then deploy the `Wormhole` using the initial guardian key (`key_x,y_parity,0`) a
 `WrappedAsset`. The wrapped asset contract will be used as proxy library to all the creation of cheap proxy wrapped 
 assets.
 
+### Testing
+
+For each test run:
+
+Run `npx ganache-cli --deterministic --time "1970-01-01T00:00:00+00:00"` to start a chain.
+
+Run the tests using `npm test`
+
 ### User methods
 
 `submitVAA(bytes vaa)` can be used to execute a VAA.

+ 10 - 9
ethereum/contracts/Wormhole.sol

@@ -53,7 +53,7 @@ contract Wormhole {
     );
 
     // Mapping of guardian_set_index => guardian set
-    mapping(uint32 => GuardianSet)  private guardian_sets;
+    mapping(uint32 => GuardianSet) public guardian_sets;
     // Current active guardian set
     uint32 public guardian_set_index;
 
@@ -67,10 +67,11 @@ contract Wormhole {
     mapping(bytes32 => address) wrappedAssets;
     mapping(address => bool) isWrappedAsset;
 
-    constructor(GuardianSet memory initial_guardian_set, address wrapped_asset_master) public {
+    constructor(GuardianSet memory initial_guardian_set, address wrapped_asset_master, uint32 _vaa_expiry) public {
         guardian_sets[0] = initial_guardian_set;
         // Explicitly set for doc purposes
         guardian_set_index = 0;
+        vaa_expiry = _vaa_expiry;
 
         wrappedAssetMaster = wrapped_asset_master;
     }
@@ -91,8 +92,7 @@ contract Wormhole {
         uint32 timestamp = vaa.toUint32(57);
 
         // Verify that the VAA is still valid
-        // TODO: the clock on Solana can't be trusted
-        require(timestamp + vaa_expiry < block.timestamp, "VAA has expired");
+        require(timestamp + vaa_expiry > block.timestamp, "VAA has expired");
 
         // Hash the body
         bytes32 hash = keccak256(vaa.slice(57, vaa.length - 57));
@@ -129,16 +129,17 @@ contract Wormhole {
 
     function vaaUpdateGuardianSet(bytes memory data) private {
         uint256 new_key_x = data.toUint256(0);
-        uint256 new_key_y = data.toUint256(32);
-        uint32 new_guardian_set_index = data.toUint32(64);
+        uint256 y_parity = data.toUint8(32);
+        uint32 new_guardian_set_index = data.toUint32(33);
 
         require(new_guardian_set_index > guardian_set_index, "index of new guardian set must be > current");
         require(new_key_x < Schnorr.HALF_Q, "invalid key for fast Schnorr verification");
+        require(y_parity <= 1, "invalid y parity");
 
         uint32 old_guardian_set_index = guardian_set_index;
         guardian_set_index = new_guardian_set_index;
 
-        GuardianSet memory new_guardian_set = GuardianSet(new_key_x, uint8(new_key_y % 2), 0);
+        GuardianSet memory new_guardian_set = GuardianSet(new_key_x, uint8(y_parity), 0);
         guardian_sets[guardian_set_index] = new_guardian_set;
         guardian_sets[old_guardian_set_index].expiration_time = uint32(block.timestamp) + vaa_expiry;
 
@@ -226,7 +227,7 @@ contract Wormhole {
             asset_address = bytes32(uint256(asset));
         }
 
-        emit LogTokensLocked(target_chain, asset_chain, asset_address, recipient, bytes32(uint256(msg.sender)), amount);
+        emit LogTokensLocked(target_chain, asset_chain, asset_address, bytes32(uint256(msg.sender)), recipient, amount);
     }
 
     function lockETH(
@@ -239,7 +240,7 @@ contract Wormhole {
         WETH(WETHAddress).deposit{value : msg.value}();
 
         // Log deposit of WETH
-        emit LogTokensLocked(target_chain, CHAIN_ID, bytes32(uint256(WETHAddress)), recipient, bytes32(uint256(msg.sender)), msg.value);
+        emit LogTokensLocked(target_chain, CHAIN_ID, bytes32(uint256(WETHAddress)), bytes32(uint256(msg.sender)), recipient, msg.value);
     }
 
 

+ 14 - 0
ethereum/migrations/1_initial_migration.js

@@ -0,0 +1,14 @@
+const Schnorr = artifacts.require("Schnorr");
+const WrappedAsset = artifacts.require("WrappedAsset");
+const Wormhole = artifacts.require("Wormhole");
+
+module.exports = async function (deployer) {
+    await deployer.deploy(Schnorr);
+    await deployer.deploy(WrappedAsset);
+    await deployer.link(Schnorr, Wormhole);
+    await deployer.deploy(Wormhole, {
+        x: "15420174358166353706216094226583628565375637765325964030087969534155416299009",
+        parity: 1,
+        expiration_time: 0
+    }, WrappedAsset.address, 1000);
+};

+ 0 - 12
ethereum/networks.js

@@ -1,12 +0,0 @@
-module.exports = {
-  networks: {
-    development: {
-      protocol: 'http',
-      host: 'localhost',
-      port: 8545,
-      gas: 5000000,
-      gasPrice: 5e9,
-      networkId: '*',
-    },
-  },
-};

ファイルの差分が大きいため隠しています
+ 13625 - 1680
ethereum/package-lock.json


+ 13 - 4
ethereum/package.json

@@ -5,12 +5,21 @@
   "main": "networks.js",
   "devDependencies": {
     "@openzeppelin/cli": "^2.8.2",
-    "@openzeppelin/contracts": "^3.1.0"
+    "@openzeppelin/contracts": "^3.1.0",
+    "@openzeppelin/test-environment": "^0.1.4",
+    "@openzeppelin/test-helpers": "^0.5.6",
+    "chai": "^4.2.0",
+    "mocha": "^8.1.1",
+    "truffle-assertions": "^0.9.2",
+    "truffle": "^5.1.37"
   },
   "scripts": {
-    "build": "openzeppelin compile",
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "build": "truffle compile",
+    "test": "cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle test"
   },
   "author": "",
-  "license": "ISC"
+  "license": "ISC",
+  "dependencies": {
+    "truffle": "^5.1.37"
+  }
 }

+ 179 - 0
ethereum/test/wormhole.js

@@ -0,0 +1,179 @@
+const Schnorr = artifacts.require("Schnorr");
+const Wormhole = artifacts.require("Wormhole");
+const WrappedAsset = artifacts.require("WrappedAsset");
+const ERC20 = artifacts.require("ERC20PresetMinterPauser");
+
+advanceTimeAndBlock = async (time) => {
+    await advanceTime(time);
+    await advanceBlock();
+
+    return Promise.resolve(web3.eth.getBlock('latest'));
+}
+
+advanceTime = (time) => {
+    return new Promise((resolve, reject) => {
+        web3.currentProvider.send({
+            jsonrpc: "2.0",
+            method: "evm_increaseTime",
+            params: [time],
+            id: new Date().getTime()
+        }, (err, result) => {
+            if (err) {
+                return reject(err);
+            }
+            return resolve(result);
+        });
+    });
+}
+
+advanceBlock = () => {
+    return new Promise((resolve, reject) => {
+        web3.currentProvider.send({
+            jsonrpc: "2.0",
+            method: "evm_mine",
+            id: new Date().getTime()
+        }, (err, result) => {
+            if (err) {
+                return reject(err);
+            }
+            const newBlockHash = web3.eth.getBlock('latest').hash;
+
+            return resolve(newBlockHash)
+        });
+    });
+}
+
+contract("Wormhole", function () {
+    it("should use master wrapped asset", async function () {
+        let bridge = await Wormhole.deployed();
+        let wa = await bridge.wrappedAssetMaster.call();
+        assert.equal(wa, WrappedAsset.address)
+    });
+
+    it("should transfer tokens in on valid VAA", async function () {
+        let bridge = await Wormhole.deployed();
+
+        await bridge.submitVAA("0x0100000000008df1ef2b367213cf591e6f6a8de37dd5a4ca771590f6f964a2c4a63b44c1e8532c0e595f4e6e0e784314724c85038af6576de0000007d01087000000330102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1010000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e99880000000000000000000000000000000000000000000000004563918244f40000")
+        // Expect user to have a balance of a new wrapped asset
+        let wa = new WrappedAsset("0x79183957Be84C0F4dA451E534d5bA5BA3FB9c696");
+        assert.equal(await wa.assetChain(), 1)
+        assert.equal(await wa.assetAddress(), "0x0000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e9988")
+        let balance = await wa.balanceOf("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1");
+        assert.equal(balance, "5000000000000000000");
+    });
+
+    it("should not accept the same VAA twice", async function () {
+        let bridge = await Wormhole.deployed();
+        try {
+            await bridge.submitVAA("0x0100000000008df1ef2b367213cf591e6f6a8de37dd5a4ca771590f6f964a2c4a63b44c1e8532c0e595f4e6e0e784314724c85038af6576de0000007d01087000000330102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1010000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e99880000000000000000000000000000000000000000000000004563918244f40000");
+        } catch (e) {
+            return
+        }
+        assert.fail("did not fail")
+    });
+
+    it("should burn tokens on lock", async function () {
+        let bridge = await Wormhole.deployed();
+        // Expect user to have a balance
+        let wa = new WrappedAsset("0x79183957Be84C0F4dA451E534d5bA5BA3FB9c696")
+
+        await bridge.lockAssets(wa.address, "4000000000000000000", "0x0", 2);
+        let balance = await wa.balanceOf("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1");
+
+        // Expect user balance to decrease
+        assert.equal(balance, "1000000000000000000");
+
+        // Expect contract balance to be 0 since tokens have been burned
+        balance = await wa.balanceOf(bridge.address);
+        assert.equal(balance, "0");
+    });
+
+    it("should transfer tokens in and out", async function () {
+        let bridge = await Wormhole.deployed();
+        let token = await ERC20.new("Test Token", "TKN");
+
+        await token.mint("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", "1000000000000000000");
+        // Expect user to have a balance
+        assert.equal(await token.balanceOf("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"), "1000000000000000000");
+
+        // Approve bridge
+        await token.approve(bridge.address, "1000000000000000000");
+
+        // Transfer of that token out of the contract should not work
+        let threw = false;
+        try {
+            await bridge.submitVAA("0x0100000000636e71c9cb08d64b6388a39d28779fab9dd42edad20331d022c9e90a43b78b1bfc737f2973136230a9e323fbd5d2f7d6cb599c2bfffff82f1087000000310102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1020000000000000000000000009561c133dd8580860b6b7e504bc5aa500f0f06a70000000000000000000000000000000000000000000000000de0b6b3a7640000");
+        } catch (e) {
+            threw = true;
+        }
+        assert.isTrue(threw);
+
+        // Lock assets
+        let ev = await bridge.lockAssets(token.address, "1000000000000000000", "0x1230000000000000000000000000000000000000000000000000000000000000", 3);
+
+        // Check that the lock event was emitted correctly
+        assert.lengthOf(ev.logs, 1)
+        assert.equal(ev.logs[0].event, "LogTokensLocked")
+        assert.equal(ev.logs[0].args.target_chain, "3")
+        assert.equal(ev.logs[0].args.token_chain, "2")
+        assert.equal(ev.logs[0].args.token, "0x0000000000000000000000009561c133dd8580860b6b7e504bc5aa500f0f06a7")
+        assert.equal(ev.logs[0].args.sender, "0x00000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1")
+        assert.equal(ev.logs[0].args.recipient, "0x1230000000000000000000000000000000000000000000000000000000000000")
+        assert.equal(ev.logs[0].args.amount, "1000000000000000000")
+
+        // Check that the tokens were transferred to the bridge
+        assert.equal(await token.balanceOf("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"), "0");
+        assert.equal(await token.balanceOf(bridge.address), "1000000000000000000");
+
+        // Transfer this token back
+        await bridge.submitVAA("0x0100000000636e71c9cb08d64b6388a39d28779fab9dd42edad20331d022c9e90a43b78b1bfc737f2973136230a9e323fbd5d2f7d6cb599c2bfffff82f1087000000310102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1020000000000000000000000009561c133dd8580860b6b7e504bc5aa500f0f06a70000000000000000000000000000000000000000000000000de0b6b3a7640000");
+        assert.equal(await token.balanceOf("0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"), "1000000000000000000");
+        assert.equal(await token.balanceOf(bridge.address), "0");
+    });
+
+    it("should accept validator set change", async function () {
+        let bridge = await Wormhole.deployed();
+
+        // Push time by 1000
+        await advanceTimeAndBlock(1000);
+        await bridge.submitVAA("0x0100000000fe60d5766a84300effedd5362dcf6ff8f4ed75ab3dbe4c1ae07151ab48bc8cbf767b4aa42cf768477dc5bb45367044bd2de6d6b3000003e801253e2f87d126ef42ac22d284de7619d2c87437198a32887efeddb4debfd016747f0000000001")
+        // Expect user to have a balance of a new wrapped asset
+        assert.equal(await bridge.guardian_set_index(), 1);
+        assert.equal((await bridge.guardian_sets(1)).x, "28127375798693063422362909717576839343810687066240716944661469189277081826431");
+
+        // Test VAA from guardian set 0; timestamp 1000
+        await bridge.submitVAA("0x01000000004f871da18c25af540bf7ea0ef28df13ff8945903fa1b82aa5d11ff749f33dba57b6064666dfe07b627e5e1da1f4bf620f92c15c2000003e81087000000340102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1010000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e99880000000000000000000000000000000000000000000000004563918244f40000")
+
+        await advanceTimeAndBlock(1000);
+
+        // Test VAA from guardian set 0; timestamp 2000 - should not work anymore
+        let threw = false;
+        try {
+            await bridge.submitVAA("0x01000000004629dc39ea4b284d31f9c7d5350013aeed4b1c38a80fc65fb21e6c7da5ebd0eb13b46039f40a0ddd7c94c3e974b51cacf9eaa1bb000007d01087000000340102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1010000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e99880000000000000000000000000000000000000000000000004563918244f40000")
+        } catch (e) {
+            threw = true;
+            assert.equal(e.reason, "guardian set has expired")
+        }
+        assert.isTrue(threw, "guardian set did not expire")
+
+        // Test same transaction with guardian set 1; timestamp 2000
+        await bridge.submitVAA("0x01000000011322402df3ec812a145aa2d9b0f627ff3654c9b3ca471622a1439e81da62ec384ad14db65ae4bee55a23b8082628590902e3d778000007d01087000000340102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1010000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e99880000000000000000000000000000000000000000000000004563918244f40000")
+    });
+
+    it("should expire VAA", async function () {
+        let bridge = await Wormhole.deployed();
+
+        // Push time by 1000
+        await advanceTimeAndBlock(1000);
+
+        // Test same transaction with guardian set 1; timestamp 2000
+        let threw = false;
+        try {
+            await bridge.submitVAA("0x01000000013faebdc02d6427d1e8d33919fbaa519ca402323723922c772e4e2da7fedc820c15b24aa5e4c99bec6a9f4c9b612970590ea3acd1000007d01087000000350102020104000000000000000000000000000000000000000000000000000000000000000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c1010000000000000000000000000347ef34687bdc9f189e87a9200658d9c40e99880000000000000000000000000000000000000000000000004563918244f40000")
+        } catch (e) {
+            threw = true;
+            assert.equal(e.reason, "VAA has expired")
+        }
+        assert.isTrue(threw, "VAA did not expire")
+    });
+});

+ 96 - 0
ethereum/truffle-config.js

@@ -0,0 +1,96 @@
+/**
+ * Use this file to configure your truffle project. It's seeded with some
+ * common settings for different networks and features like migrations,
+ * compilation and testing. Uncomment the ones you need or modify
+ * them to suit your project as necessary.
+ *
+ * More information about configuration can be found at:
+ *
+ * truffleframework.com/docs/advanced/configuration
+ *
+ * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
+ * to sign your transactions before they're sent to a remote public node. Infura accounts
+ * are available for free at: infura.io/register.
+ *
+ * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
+ * public/private key pairs. If you're publishing your code to GitHub make sure you load this
+ * phrase from a file you've .gitignored so it doesn't accidentally become public.
+ *
+ */
+
+// const HDWalletProvider = require('@truffle/hdwallet-provider');
+// const infuraKey = "fj4jll3k.....";
+//
+// const fs = require('fs');
+// const mnemonic = fs.readFileSync(".secret").toString().trim();
+
+module.exports = {
+  /**
+   * Networks define how you connect to your ethereum client and let you set the
+   * defaults web3 uses to send transactions. If you don't specify one truffle
+   * will spin up a development blockchain for you on port 9545 when you
+   * run `develop` or `test`. You can ask a truffle command to use a specific
+   * network from the command line, e.g
+   *
+   * $ truffle test --network <network-name>
+   */
+
+  networks: {
+    // Useful for testing. The `development` name is special - truffle uses it by default
+    // if it's defined here and no other network is specified at the command line.
+    // You should run a client (like ganache-cli, geth or parity) in a separate terminal
+    // tab if you use this network and you must also set the `host`, `port` and `network_id`
+    // options below to some value.
+    //
+    development: {
+     host: "127.0.0.1",     // Localhost (default: none)
+     port: 8545,            // Standard Ethereum port (default: none)
+     network_id: "*",       // Any network (default: none)
+    },
+    // Another network with more advanced options...
+    // advanced: {
+    // port: 8777,             // Custom port
+    // network_id: 1342,       // Custom network
+    // gas: 8500000,           // Gas sent with each transaction (default: ~6700000)
+    // gasPrice: 20000000000,  // 20 gwei (in wei) (default: 100 gwei)
+    // from: <address>,        // Account to send txs from (default: accounts[0])
+    // websockets: true        // Enable EventEmitter interface for web3 (default: false)
+    // },
+    // Useful for deploying to a public network.
+    // NB: It's important to wrap the provider as a function.
+    // ropsten: {
+    // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
+    // network_id: 3,       // Ropsten's id
+    // gas: 5500000,        // Ropsten has a lower block limit than mainnet
+    // confirmations: 2,    // # of confs to wait between deployments. (default: 0)
+    // timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
+    // skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
+    // },
+    // Useful for private networks
+    // private: {
+    // provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
+    // network_id: 2111,   // This network is yours, in the cloud.
+    // production: true    // Treats this network as if it was a public net. (default: false)
+    // }
+  },
+
+  // Set default mocha options here, use special reporters etc.
+  mocha: {
+    // timeout: 100000
+  },
+
+  // Configure your compilers
+  compilers: {
+    solc: {
+       version: "0.6.12",    // Fetch exact version from solc-bin (default: truffle's version)
+      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
+      // settings: {          // See the solidity docs for advice about optimization and evmVersion
+      //  optimizer: {
+      //    enabled: false,
+      //    runs: 200
+      //  },
+      //  evmVersion: "byzantium"
+      // }
+    },
+  },
+};

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません