From 040eace4f7c3617b7d53a004788ccb871e29cdc8 Mon Sep 17 00:00:00 2001 From: Jonathan Claudius Date: Thu, 14 Apr 2022 10:11:04 -0400 Subject: [PATCH] Add to ethereum contract integration test coverage (#1053) * Fix assert typos * Restructure data field and add comments * Move magic hex into named variables * Add README items for running tests * Add make test to README * Show work on core string generation * Add action transfer fee * Add more VM integration test cases --- ethereum/README.md | 6 +- ethereum/test/wormhole.js | 266 ++++++++++++++++++++++++++++++++++---- 2 files changed, 249 insertions(+), 23 deletions(-) diff --git a/ethereum/README.md b/ethereum/README.md index 49294bd13..e97fb87fa 100644 --- a/ethereum/README.md +++ b/ethereum/README.md @@ -28,7 +28,11 @@ 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 run test` +Run the all ethereum tests using `make test` + +Run a specific test file using `npx truffle test test/wormhole.js` + +Run a specific test file while skipping compile `npx truffle test test/wormhole.js --compile-none` ### User methods diff --git a/ethereum/test/wormhole.js b/ethereum/test/wormhole.js index a2a45b79a..c984c891e 100644 --- a/ethereum/test/wormhole.js +++ b/ethereum/test/wormhole.js @@ -9,6 +9,15 @@ const Implementation = artifacts.require("Implementation"); const testSigner1PK = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; const testSigner2PK = "892330666a850761e7370376430bb8c2aa1494072d3bfeaed0c4fa3d5a9135fe"; const testSigner3PK = "87b45997ea577b93073568f06fc4838cffc1d01f90fc4d57f936957f3c4d99fb"; +const testBadSigner1PK = "87b45997ea577b93073568f06fc4838cffc1d01f90fc4d57f936957f3c4d99fc"; + + +const core = '0x' + Buffer.from("Core").toString("hex").padStart(64,0) +const actionContractUpgrade = "01" +const actionGuardianSetUpgrade = "02" +const actionMessageFee = "03" +const actionTransferFee = "04" + const ImplementationFullABI = jsonfile.readFileSync("build/contracts/Implementation.json").abi @@ -167,6 +176,124 @@ contract("Wormhole", function () { assert.equal(result.reason, ""); }) + + it("should fail quorum on VMs with no signers", async function () { + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = 11; + const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee" + const data = "0xaaaaaa"; + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 1337, + data, + [], // no valid signers present + 0, + 2 + ); + + let result = await initialized.methods.parseAndVerifyVM("0x" + vm).call(); + assert.equal(result[1], false) + assert.equal(result[2], "no quorum") + }) + + + it("should fail to verify on VMs with bad signer", async function () { + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = 11; + const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee" + const data = "0xaaaaaa"; + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 1337, + data, + [ + testBadSigner1PK, // not a valid signer + ], + 0, + 2 + ); + + let result = await initialized.methods.parseAndVerifyVM("0x" + vm).call(); + assert.equal(result[1], false) + assert.equal(result[2], "VM signature invalid") + }) + + it("should error on VMs with invalid guardian set index", async function () { + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = 11; + const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee" + const data = "0xaaaaaa"; + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 1337, + data, + [ + testSigner1PK, + ], + 200, + 2 + ); + + let result = await initialized.methods.parseAndVerifyVM("0x" + vm).call(); + assert.equal(result[1], false) + assert.equal(result[2], "invalid guardian set") + }) + + it("should revert on VMs with duplicate non-monotonic signature indexes", async function () { + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = 11; + const emitterAddress = "0x0000000000000000000000000000000000000000000000000000000000000eee" + const data = "0xaaaaaa"; + + const vm = await signAndEncodeVMFixedIndex( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 1337, + data, + [ + testSigner1PK, + testSigner1PK, + testSigner1PK, + ], + 0, + 2 + ); + + try { + await initialized.methods.parseAndVerifyVM("0x" + vm).call(); + assert.fail("accepted signature indexes being the same in a VM"); + } catch (e) { + assert.equal(e.data[Object.keys(e.data)[0]].reason, 'signature indices must be ascending') + } + }) + + it("should set and enforce fees", async function () { const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); const accounts = await web3.eth.getAccounts(); @@ -175,10 +302,15 @@ contract("Wormhole", function () { const nonce = 1001; const emitterChainId = testGovernanceChainId; const emitterAddress = testGovernanceContract - let data = "0x00000000000000000000000000000000000000000000000000000000436f726503"; - data += [ + data = [ + //Core + core, + // Action 3 (Set Message Fee) + actionMessageFee, + // ChainID web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + // Message Fee web3.eth.abi.encodeParameter("uint256", 1111).substring(2), ].join('') @@ -247,11 +379,17 @@ contract("Wormhole", function () { const nonce = 1001; const emitterChainId = testGovernanceChainId; const emitterAddress = testGovernanceContract - let data = "0x00000000000000000000000000000000000000000000000000000000436f726504"; - data += [ + data = [ + // Core + core, + // Action 4 (Transfer Fees) + actionTransferFee, + // ChainID web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + // Amount web3.eth.abi.encodeParameter("uint256", 11).substring(2), + // Recipient web3.eth.abi.encodeParameter("address", receiver).substring(2), ].join('') @@ -293,11 +431,14 @@ contract("Wormhole", function () { const nonce = 1001; const emitterChainId = testGovernanceChainId; const emitterAddress = testGovernanceContract - let data = "0x00000000000000000000000000000000000000000000000000000000436f726502"; let oldIndex = Number(await initialized.methods.getCurrentGuardianSetIndex().call()); - data += [ + data = [ + // Core + core, + // Action 2 (Guardian Set Upgrade) + actionGuardianSetUpgrade, web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), web3.eth.abi.encodeParameter("uint32", oldIndex + 1).substring(2 + (64 - 8)), web3.eth.abi.encodeParameter("uint8", 3).substring(2 + (64 - 2)), @@ -362,10 +503,15 @@ contract("Wormhole", function () { const nonce = 1001; const emitterChainId = testGovernanceChainId; const emitterAddress = testGovernanceContract - let data = "0x00000000000000000000000000000000000000000000000000000000436f726501"; - data += [ + data = [ + // Core + core, + // Action 1 (Contract Upgrade) + actionContractUpgrade, + // ChainID web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + // New Contract Address web3.eth.abi.encodeParameter("address", mock.address).substring(2), ].join('') @@ -410,10 +556,16 @@ contract("Wormhole", function () { const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); const accounts = await web3.eth.getAccounts(); - let data = "0x00000000000000000000000000000000000000000000000000000000436f726504"; - data += [ + data = [ + // Core + core, + // Action 4 (Transfer Fee) + actionTransferFee, + // ChainID web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + // Amount web3.eth.abi.encodeParameter("uint256", 1).substring(2), + // Recipient web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2), ].join('') @@ -438,7 +590,7 @@ contract("Wormhole", function () { from: accounts[0], gasLimit: 1000000 }); - asset.fail("governance packet of old guardian set accepted") + assert.fail("governance packet of old guardian set accepted") } catch (e) { assert.equal(e.data[Object.keys(e.data)[0]].reason, "not signed by current guardian set") } @@ -483,10 +635,16 @@ contract("Wormhole", function () { const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); const accounts = await web3.eth.getAccounts(); - let data = "0x00000000000000000000000000000000000000000000000000000000436f726504"; - data += [ + data = [ + // Core + core, + // Action 4 (set fees) + actionTransferFee, + // ChainID web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + // Amount web3.eth.abi.encodeParameter("uint256", 1).substring(2), + // Recipient web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2), ].join('') @@ -512,7 +670,7 @@ contract("Wormhole", function () { from: accounts[0], gasLimit: 1000000 }); - asset.fail("governance packet from wrong governance chain accepted") + assert.fail("governance packet from wrong governance chain accepted") } catch (e) { assert.equal(e.data[Object.keys(e.data)[0]].reason, "wrong governance chain") } @@ -522,8 +680,11 @@ contract("Wormhole", function () { const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); const accounts = await web3.eth.getAccounts(); - let data = "0x00000000000000000000000000000000000000000000000000000000436f726504"; - data += [ + data = [ + // Core + core, + // Action 4 (Transfer Fee) + actionTransferFee, web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), web3.eth.abi.encodeParameter("uint256", 1).substring(2), web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2), @@ -533,7 +694,7 @@ contract("Wormhole", function () { 0, 0, testGovernanceChainId, - "0x00000000000000000000000000000000000000000000000000000000436f7265", + core, 0, data, [ @@ -551,7 +712,7 @@ contract("Wormhole", function () { from: accounts[0], gasLimit: 1000000 }); - asset.fail("governance packet from wrong governance contract accepted") + assert.fail("governance packet from wrong governance contract accepted") } catch (e) { assert.equal(e.data[Object.keys(e.data)[0]].reason, "wrong governance contract") } @@ -561,10 +722,16 @@ contract("Wormhole", function () { const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); const accounts = await web3.eth.getAccounts(); - let data = "0x00000000000000000000000000000000000000000000000000000000436f726504"; - data += [ + data = [ + // Core + core, + // Action 4 (Transfer Fee) + actionTransferFee, + // ChainID web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + // Amount web3.eth.abi.encodeParameter("uint256", 1).substring(2), + // Recipient web3.eth.abi.encodeParameter("address", "0x0000000000000000000000000000000000000000").substring(2), ].join('') @@ -597,11 +764,12 @@ contract("Wormhole", function () { gasLimit: 1000000 }); - asset.fail("governance packet accepted twice") + assert.fail("governance packet accepted twice") } catch (e) { assert.equal(e.data[Object.keys(e.data)[0]].reason, "governance action already consumed") } }) + }); const signAndEncodeVM = async function ( @@ -656,9 +824,63 @@ const signAndEncodeVM = async function ( return vm } +const signAndEncodeVMFixedIndex = async function ( + timestamp, + nonce, + emitterChainId, + emitterAddress, + sequence, + data, + signers, + guardianSetIndex, + consistencyLevel +) { + const body = [ + web3.eth.abi.encodeParameter("uint32", timestamp).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint32", nonce).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint16", emitterChainId).substring(2 + (64 - 4)), + web3.eth.abi.encodeParameter("bytes32", emitterAddress).substring(2), + web3.eth.abi.encodeParameter("uint64", sequence).substring(2 + (64 - 16)), + web3.eth.abi.encodeParameter("uint8", consistencyLevel).substring(2 + (64 - 2)), + data.substr(2) + ] + + const hash = web3.utils.soliditySha3(web3.utils.soliditySha3("0x" + body.join(""))) + + let signatures = ""; + + for (let i in signers) { + const ec = new elliptic.ec("secp256k1"); + const key = ec.keyFromPrivate(signers[i]); + const signature = key.sign(hash.substr(2), {canonical: true}); + + const packSig = [ + // Fixing the index to be zero to product a non-monotonic VM + web3.eth.abi.encodeParameter("uint8", 0).substring(2 + (64 - 2)), + zeroPadBytes(signature.r.toString(16), 32), + zeroPadBytes(signature.s.toString(16), 32), + web3.eth.abi.encodeParameter("uint8", signature.recoveryParam).substr(2 + (64 - 2)), + ] + + signatures += packSig.join("") + } + + const vm = [ + web3.eth.abi.encodeParameter("uint8", 1).substring(2 + (64 - 2)), + web3.eth.abi.encodeParameter("uint32", guardianSetIndex).substring(2 + (64 - 8)), + web3.eth.abi.encodeParameter("uint8", signers.length).substring(2 + (64 - 2)), + + signatures, + body.join("") + ].join(""); + + return vm +} + + function zeroPadBytes(value, length) { while (value.length < 2 * length) { value = "0" + value; } return value; -} \ No newline at end of file +}