From cec5bc7528e64d78aadd83e943ad278f23998674 Mon Sep 17 00:00:00 2001 From: Kevin Peters Date: Fri, 9 Sep 2022 17:26:41 +0000 Subject: [PATCH] ethereum: Added fork protection tests --- .../bridge/mock/MockBridgeImplementation.sol | 5 + .../contracts/mock/MockImplementation.sol | 5 + .../nft/mock/MockNFTBridgeImplementation.sol | 5 + ethereum/test/bridge.js | 182 ++++++++++++++++++ ethereum/test/nft.js | 182 ++++++++++++++++++ ethereum/test/wormhole.js | 180 ++++++++++++++++- 6 files changed, 558 insertions(+), 1 deletion(-) diff --git a/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol b/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol index 27f9be1a1..f9db2e568 100644 --- a/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol +++ b/ethereum/contracts/bridge/mock/MockBridgeImplementation.sol @@ -17,4 +17,9 @@ contract MockBridgeImplementation is BridgeImplementation { function testUpdateWETHAddress(address WETH) external { setWETH(WETH); } + + function testOverwriteEVMChainId(uint16 fakeChainId, uint256 fakeEvmChainId) external { + _state.provider.chainId = fakeChainId; + _state.evmChainId = fakeEvmChainId; + } } diff --git a/ethereum/contracts/mock/MockImplementation.sol b/ethereum/contracts/mock/MockImplementation.sol index 45e81a2c4..dfc93ecc2 100644 --- a/ethereum/contracts/mock/MockImplementation.sol +++ b/ethereum/contracts/mock/MockImplementation.sol @@ -9,4 +9,9 @@ contract MockImplementation is Implementation { function testNewImplementationActive() external pure returns (bool) { return true; } + + function testOverwriteEVMChainId(uint16 fakeChainId, uint256 fakeEvmChainId) external { + _state.provider.chainId = fakeChainId; + _state.evmChainId = fakeEvmChainId; + } } diff --git a/ethereum/contracts/nft/mock/MockNFTBridgeImplementation.sol b/ethereum/contracts/nft/mock/MockNFTBridgeImplementation.sol index 2787ef274..9debb31b2 100644 --- a/ethereum/contracts/nft/mock/MockNFTBridgeImplementation.sol +++ b/ethereum/contracts/nft/mock/MockNFTBridgeImplementation.sol @@ -13,4 +13,9 @@ contract MockNFTBridgeImplementation is NFTBridgeImplementation { function testNewImplementationActive() external pure returns (bool) { return true; } + + function testOverwriteEVMChainId(uint16 fakeChainId, uint256 fakeEvmChainId) external { + _state.provider.chainId = fakeChainId; + _state.evmChainId = fakeEvmChainId; + } } diff --git a/ethereum/test/bridge.js b/ethereum/test/bridge.js index 565c507d1..69baee5c2 100644 --- a/ethereum/test/bridge.js +++ b/ethereum/test/bridge.js @@ -18,6 +18,14 @@ const WormholeImplementationFullABI = jsonfile.readFileSync("build/contracts/Imp const BridgeImplementationFullABI = jsonfile.readFileSync("build/contracts/BridgeImplementation.json").abi const TokenImplementationFullABI = jsonfile.readFileSync("build/contracts/TokenImplementation.json").abi +const actionContractUpgrade = "02" +const actionRecoverChainId = "03" + +const fakeChainId = 1337; +const fakeEvmChainId = 10001; + +let lastDeployed; + contract("Bridge", function () { const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK); const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK); @@ -155,6 +163,7 @@ contract("Bridge", function () { let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); assert.ok(isUpgraded); + lastDeployed = mock; }) it("bridged tokens should only be mint- and burn-able by owner", async function () { @@ -1318,6 +1327,179 @@ contract("Bridge", function () { assert.ok(failed) }) + + it("should reject smart contract upgrades on forks", async function () { + const mockInitialized = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address); + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockBridgeImplementation.new(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + // simulate a fork + await mockInitialized.methods.testOverwriteEVMChainId(fakeChainId, fakeEvmChainId).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, fakeChainId); + + const evmChainId = await initialized.methods.evmChainId().call(); + assert.equal(evmChainId, fakeEvmChainId); + + data = [ + "0x", + "000000000000000000000000000000000000000000546f6b656e427269646765", + // 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('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + ], + 0, + 0 + ); + + try { + await initialized.methods.upgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + assert.fail("governance packet accepted") + } catch (e) { + assert.equal(e.data[Object.keys(e.data)[0]].reason, "invalid fork") + } + }) + + it("should allow recover chain ID governance packets forks", async function () { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const accounts = await web3.eth.getAccounts(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, fakeChainId); + + const evmChainId = await initialized.methods.evmChainId().call(); + assert.equal(evmChainId, fakeEvmChainId); + + data = [ + "0x", + "000000000000000000000000000000000000000000546f6b656e427269646765", + // Action 3 (Recover Chain ID) + actionRecoverChainId, + // EvmChainID + web3.eth.abi.encodeParameter("uint256", testEvmChainId).substring(2), + // NewChainID + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + ].join('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + ], + 0, + 0 + ); + + await initialized.methods.submitRecoverChainId("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + const newChainId = await initialized.methods.chainId().call(); + assert.equal(newChainId, testChainId); + + const newEvmChainId = await initialized.methods.evmChainId().call(); + assert.equal(newEvmChainId, testEvmChainId); + }) + + it("should accept smart contract upgrades after chain ID has been recovered", async function () { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockBridgeImplementation.new(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + data = [ + "0x", + "000000000000000000000000000000000000000000546f6b656e427269646765", + // Action 2 (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('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + ], + 0, + 0 + ); + + let before = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), lastDeployed.address.toLowerCase()); + + let set = await initialized.methods.upgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + let after = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + + const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address); + + let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); + + assert.ok(isUpgraded); + }) }); const signAndEncodeVM = async function ( diff --git a/ethereum/test/nft.js b/ethereum/test/nft.js index a22199d49..ade76fe2b 100644 --- a/ethereum/test/nft.js +++ b/ethereum/test/nft.js @@ -15,6 +15,14 @@ const WormholeImplementationFullABI = jsonfile.readFileSync("build/contracts/Imp const BridgeImplementationFullABI = jsonfile.readFileSync("build/contracts/NFTBridgeImplementation.json").abi const NFTImplementationFullABI = jsonfile.readFileSync("build/contracts/NFTImplementation.json").abi +const actionContractUpgrade = "02" +const actionRecoverChainId = "03" + +const fakeChainId = 1337; +const fakeEvmChainId = 10001; + +let lastDeployed; + contract("NFT", function () { const testSigner1 = web3.eth.accounts.privateKeyToAccount(testSigner1PK); const testSigner2 = web3.eth.accounts.privateKeyToAccount(testSigner2PK); @@ -149,6 +157,7 @@ contract("NFT", function () { let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); assert.ok(isUpgraded); + lastDeployed = mock; }) it("bridged tokens should only be mint- and burn-able by owner", async function () { @@ -673,6 +682,179 @@ contract("NFT", function () { assert.equal(e.data[Object.keys(e.data)[0]].reason, "ERC721: owner query for nonexistent token") } }) + + it("should reject smart contract upgrades on forks", async function () { + const mockInitialized = new web3.eth.Contract(MockBridgeImplementation.abi, NFTBridge.address); + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, NFTBridge.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockBridgeImplementation.new(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + // simulate a fork + await mockInitialized.methods.testOverwriteEVMChainId(fakeChainId, fakeEvmChainId).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, fakeChainId); + + const evmChainId = await initialized.methods.evmChainId().call(); + assert.equal(evmChainId, fakeEvmChainId); + + data = [ + "0x", + "00000000000000000000000000000000000000000000004e4654427269646765", + // 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('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + ], + 0, + 0 + ); + + try { + await initialized.methods.upgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + assert.fail("governance packet accepted") + } catch (e) { + assert.equal(e.data[Object.keys(e.data)[0]].reason, "invalid fork") + } + }) + + it("should allow recover chain ID governance packets forks", async function () { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, NFTBridge.address); + const accounts = await web3.eth.getAccounts(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, fakeChainId); + + const evmChainId = await initialized.methods.evmChainId().call(); + assert.equal(evmChainId, fakeEvmChainId); + + data = [ + "0x", + "00000000000000000000000000000000000000000000004e4654427269646765", + // Action 3 (Recover Chain ID) + actionRecoverChainId, + // EvmChainID + web3.eth.abi.encodeParameter("uint256", testEvmChainId).substring(2), + // NewChainID + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + ].join('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + ], + 0, + 0 + ); + + await initialized.methods.submitRecoverChainId("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + const newChainId = await initialized.methods.chainId().call(); + assert.equal(newChainId, testChainId); + + const newEvmChainId = await initialized.methods.evmChainId().call(); + assert.equal(newEvmChainId, testEvmChainId); + }) + + it("should accept smart contract upgrades after chain ID has been recovered", async function () { + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, NFTBridge.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockBridgeImplementation.new(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + data = [ + "0x", + "00000000000000000000000000000000000000000000004e4654427269646765", + // Action 2 (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('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + ], + 0, + 0 + ); + + let before = await web3.eth.getStorageAt(NFTBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), lastDeployed.address.toLowerCase()); + + let set = await initialized.methods.upgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + let after = await web3.eth.getStorageAt(NFTBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + + const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, NFTBridge.address); + + let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); + + assert.ok(isUpgraded); + }) }); const signAndEncodeVM = async function ( diff --git a/ethereum/test/wormhole.js b/ethereum/test/wormhole.js index 3a4ad81f9..59f772bb1 100644 --- a/ethereum/test/wormhole.js +++ b/ethereum/test/wormhole.js @@ -23,6 +23,11 @@ const actionRecoverChainId = "05" const ImplementationFullABI = jsonfile.readFileSync("build/contracts/Implementation.json").abi +const fakeChainId = 1337; +const fakeEvmChainId = 10001; + +let lastDeployed; + // Taken from https://medium.com/fluidity/standing-the-time-of-test-b906fcc374a9 advanceTimeAndBlock = async (time) => { await advanceTime(time); @@ -612,9 +617,10 @@ contract("Wormhole", function () { let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); assert.ok(isUpgraded); + lastDeployed = mock; }) - it("should revert recover chain ID governance packets on canonical chains (non-forked)", async function () { + it("should revert recover chain ID governance packets on canonical chains (non-fork)", async function () { const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); const accounts = await web3.eth.getAccounts(); @@ -880,6 +886,178 @@ contract("Wormhole", function () { } }) + it("should reject smart contract upgrades on forks", async function () { + const mockInitialized = new web3.eth.Contract(MockImplementation.abi, Wormhole.address); + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockImplementation.new(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + // simulate a fork + await mockInitialized.methods.testOverwriteEVMChainId(fakeChainId, fakeEvmChainId).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + const chainId = await initialized.methods.chainId().call(); + assert.equal(chainId, fakeChainId); + + const evmChainId = await initialized.methods.evmChainId().call(); + assert.equal(evmChainId, fakeEvmChainId); + + 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('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + testSigner2PK, + testSigner3PK + ], + 1, + 2 + ); + + try { + await initialized.methods.submitContractUpgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + assert.fail("governance packet accepted") + } catch (e) { + assert.equal(e.data[Object.keys(e.data)[0]].reason, "invalid fork") + } + }) + + it("should allow recover chain ID governance packets forks", async function () { + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + const accounts = await web3.eth.getAccounts(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract; + + data = [ + // Core + core, + // Action 5 (Recover Chain ID) + actionRecoverChainId, + // EvmChainID + web3.eth.abi.encodeParameter("uint256", testEvmChainId).substring(2), + // NewChainID + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)), + ].join('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + testSigner2PK, + testSigner3PK + ], + 1, + 2 + ); + + await initialized.methods.submitRecoverChainId("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + const newChainId = await initialized.methods.chainId().call(); + assert.equal(newChainId, testChainId); + + const newEvmChainId = await initialized.methods.evmChainId().call(); + assert.equal(newEvmChainId, testEvmChainId); + }) + + it("should accept smart contract upgrades after chain ID has been recovered", async function () { + const initialized = new web3.eth.Contract(ImplementationFullABI, Wormhole.address); + const accounts = await web3.eth.getAccounts(); + + const mock = await MockImplementation.new(); + + const timestamp = 1000; + const nonce = 1001; + const emitterChainId = testGovernanceChainId; + const emitterAddress = testGovernanceContract + + 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('') + + const vm = await signAndEncodeVM( + timestamp, + nonce, + emitterChainId, + emitterAddress, + 0, + data, + [ + testSigner1PK, + testSigner2PK, + testSigner3PK + ], + 1, + 2 + ); + + let before = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(before.toLowerCase(), lastDeployed.address.toLowerCase()); + + let set = await initialized.methods.submitContractUpgrade("0x" + vm).send({ + value: 0, + from: accounts[0], + gasLimit: 1000000 + }); + + let after = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"); + + assert.equal(after.toLowerCase(), mock.address.toLowerCase()); + + const mockImpl = new web3.eth.Contract(MockImplementation.abi, Wormhole.address); + + let isUpgraded = await mockImpl.methods.testNewImplementationActive().call(); + + assert.ok(isUpgraded); + }) }); const signAndEncodeVM = async function (