From 66ec7cc143af58dda8fde0d6adc30a4758685d1e Mon Sep 17 00:00:00 2001 From: Roger <50648015+RogerLamTd@users.noreply.github.com> Date: Wed, 7 Dec 2022 10:13:42 -0500 Subject: [PATCH] fix(test): making tests type-safe (#318) --- .../bridge/libs/LibBridgeProcess.sol | 1 + packages/protocol/tasks/utils.ts | 65 ++ packages/protocol/test/bridge/Bridge.test.ts | 732 +++++++++++------- .../test/bridge/libs/LibBridgeData.test.ts | 16 +- .../test/bridge/libs/LibBridgeInvoke.test.ts | 11 +- .../test/bridge/libs/LibBridgeProcess.test.ts | 123 +-- .../test/bridge/libs/LibBridgeRetry.test.ts | 122 ++- .../test/bridge/libs/LibBridgeSend.test.ts | 24 +- .../test/bridge/libs/LibBridgeSignal.test.ts | 8 +- 9 files changed, 655 insertions(+), 447 deletions(-) diff --git a/packages/protocol/contracts/bridge/libs/LibBridgeProcess.sol b/packages/protocol/contracts/bridge/libs/LibBridgeProcess.sol index 9696e841cf1..9925ccef2bf 100644 --- a/packages/protocol/contracts/bridge/libs/LibBridgeProcess.sol +++ b/packages/protocol/contracts/bridge/libs/LibBridgeProcess.sol @@ -95,6 +95,7 @@ library LibBridgeProcess { uint256 gasLimit = msg.sender == message.owner ? gasleft() : message.gasLimit; + bool success = LibBridgeInvoke.invokeMessageCall({ state: state, message: message, diff --git a/packages/protocol/tasks/utils.ts b/packages/protocol/tasks/utils.ts index 8c2c948c00e..3820c49a297 100644 --- a/packages/protocol/tasks/utils.ts +++ b/packages/protocol/tasks/utils.ts @@ -1,5 +1,7 @@ import * as fs from "fs" import * as log from "./log" +import { Block, BlockHeader, EthGetProofResponse } from "../test/utils/rpc" +import RLP from "rlp" async function deployContract( hre: any, @@ -84,6 +86,67 @@ const MessageStatus = { FAILED: 3, } +async function getLatestBlockHeader(hre: any) { + const block: Block = await hre.ethers.provider.send( + "eth_getBlockByNumber", + ["latest", false] + ) + + const logsBloom = block.logsBloom.toString().substring(2) + + const blockHeader: BlockHeader = { + parentHash: block.parentHash, + ommersHash: block.sha3Uncles, + beneficiary: block.miner, + stateRoot: block.stateRoot, + transactionsRoot: block.transactionsRoot, + receiptsRoot: block.receiptsRoot, + logsBloom: logsBloom.match(/.{1,64}/g)!.map((s: string) => "0x" + s), + difficulty: block.difficulty, + height: block.number, + gasLimit: block.gasLimit, + gasUsed: block.gasUsed, + timestamp: block.timestamp, + extraData: block.extraData, + mixHash: block.mixHash, + nonce: block.nonce, + baseFeePerGas: block.baseFeePerGas ? parseInt(block.baseFeePerGas) : 0, + } + + return { block, blockHeader } +} + +async function getSignalProof( + hre: any, + contractAddress: string, + key: string, + blockNumber: number, + blockHeader: BlockHeader +) { + const proof: EthGetProofResponse = await hre.ethers.provider.send( + "eth_getProof", + [contractAddress, [key], blockNumber] + ) + + // RLP encode the proof together for LibTrieProof to decode + const encodedProof = hre.ethers.utils.defaultAbiCoder.encode( + ["bytes", "bytes"], + [ + RLP.encode(proof.accountProof), + RLP.encode(proof.storageProof[0].proof), + ] + ) + // encode the SignalProof struct from LibBridgeSignal + const signalProof = hre.ethers.utils.defaultAbiCoder.encode( + [ + "tuple(tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, bytes proof)", + ], + [{ header: blockHeader, proof: encodedProof }] + ) + + return signalProof +} + export { deployContract, getDeployer, @@ -94,4 +157,6 @@ export { getSlot, decode, MessageStatus, + getLatestBlockHeader, + getSignalProof, } diff --git a/packages/protocol/test/bridge/Bridge.test.ts b/packages/protocol/test/bridge/Bridge.test.ts index fe3ee1a84aa..77a4dc47697 100644 --- a/packages/protocol/test/bridge/Bridge.test.ts +++ b/packages/protocol/test/bridge/Bridge.test.ts @@ -1,10 +1,17 @@ import { expect } from "chai" +import hre, { ethers } from "hardhat" import { BigNumber, Signer } from "ethers" -import { ethers } from "hardhat" -import RLP from "rlp" -import { AddressManager, Bridge, EtherVault } from "../../typechain" import { Message } from "../utils/message" -import { Block, BlockHeader, EthGetProofResponse } from "../utils/rpc" +import { + AddressManager, + Bridge, + EtherVault, + LibTrieProof, + TestBadReceiver, + TestHeaderSync, + TestLibBridgeData, +} from "../../typechain" +import { getLatestBlockHeader, getSignalProof } from "../../tasks/utils" async function deployBridge( signer: Signer, @@ -12,7 +19,9 @@ async function deployBridge( destChain: number, srcChain: number ): Promise<{ bridge: Bridge; etherVault: EtherVault }> { - const libTrieProof = await (await ethers.getContractFactory("LibTrieProof")) + const libTrieProof: LibTrieProof = await ( + await ethers.getContractFactory("LibTrieProof") + ) .connect(signer) .deploy() @@ -474,7 +483,7 @@ describe("integration:Bridge", function () { .connect(l2Signer) .setAddress(`${srcChainId}.bridge`, l1Bridge.address) - const headerSync = await ( + const headerSync: TestHeaderSync = await ( await ethers.getContractFactory("TestHeaderSync") ) .connect(l2Signer) @@ -484,6 +493,22 @@ describe("integration:Bridge", function () { .connect(l2Signer) .setAddress(`${enabledDestChainId}.taiko`, headerSync.address) + const m: Message = { + id: 1, + sender: owner.address, + srcChainId: srcChainId, + destChainId: enabledDestChainId, + owner: owner.address, + to: owner.address, + refundAddress: owner.address, + depositValue: 1000, + callValue: 1000, + processingFee: 1000, + gasLimit: 10000, + data: ethers.constants.HashZero, + memo: "", + } + return { owner, l2Signer, @@ -497,6 +522,7 @@ describe("integration:Bridge", function () { l2EtherVault, srcChainId, headerSync, + m, } } @@ -557,30 +583,8 @@ describe("integration:Bridge", function () { }) it("should throw if messageStatus of message is != NEW", async function () { - const { - owner, - l1Bridge, - srcChainId, - enabledDestChainId, - l2Bridge, - headerSync, - } = await deployBridgeFixture() - - const m: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: enabledDestChainId, - owner: owner.address, - to: owner.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 10000, - data: ethers.constants.HashZero, - memo: "", - } + const { l1Bridge, l2Bridge, headerSync, m } = + await deployBridgeFixture() const expectedAmount = m.depositValue + m.callValue + m.processingFee @@ -603,60 +607,16 @@ describe("integration:Bridge", function () { ) ) - // use this instead of ethers.provider.getBlock() beccause it doesnt have stateRoot - // in the response - const block: Block = await ethers.provider.send( - "eth_getBlockByNumber", - ["latest", false] - ) + const { block, blockHeader } = await getLatestBlockHeader(hre) await headerSync.setSyncedHeader(block.hash) - const logsBloom = block.logsBloom.toString().substring(2) - - const blockHeader: BlockHeader = { - parentHash: block.parentHash, - ommersHash: block.sha3Uncles, - beneficiary: block.miner, - stateRoot: block.stateRoot, - transactionsRoot: block.transactionsRoot, - receiptsRoot: block.receiptsRoot, - logsBloom: logsBloom - .match(/.{1,64}/g)! - .map((s: string) => "0x" + s), - difficulty: block.difficulty, - height: block.number, - gasLimit: block.gasLimit, - gasUsed: block.gasUsed, - timestamp: block.timestamp, - extraData: block.extraData, - mixHash: block.mixHash, - nonce: block.nonce, - baseFeePerGas: block.baseFeePerGas - ? parseInt(block.baseFeePerGas) - : 0, - } - - // rpc call to get the merkle proof what value is at key on the bridge contract - const proof: EthGetProofResponse = await ethers.provider.send( - "eth_getProof", - [l1Bridge.address, [key], block.hash] - ) - - // RLP encode the proof together for LibTrieProof to decode - const encodedProof = ethers.utils.defaultAbiCoder.encode( - ["bytes", "bytes"], - [ - RLP.encode(proof.accountProof), - RLP.encode(proof.storageProof[0].proof), - ] - ) - // encode the SignalProof struct from LibBridgeSignal - const signalProof = ethers.utils.defaultAbiCoder.encode( - [ - "tuple(tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, bytes proof)", - ], - [{ header: blockHeader, proof: encodedProof }] + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader ) // upon successful processing, this immediately gets marked as DONE @@ -669,37 +629,10 @@ describe("integration:Bridge", function () { }) it("should throw if message signalproof is not valid", async function () { - const { - owner, - l1Bridge, - srcChainId, - enabledDestChainId, - l2Bridge, - headerSync, - } = await deployBridgeFixture() - - const m: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: enabledDestChainId, - owner: owner.address, - to: owner.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 10000, - data: ethers.constants.HashZero, - memo: "", - } - - const block: Block = await ethers.provider.send( - "eth_getBlockByNumber", - ["latest", false] - ) + const { l1Bridge, l2Bridge, headerSync, m } = + await deployBridgeFixture() - const libData = await ( + const libData: TestLibBridgeData = await ( await ethers.getContractFactory("TestLibBridgeData") ).deploy() @@ -713,53 +646,16 @@ describe("integration:Bridge", function () { [sender, signal] ) ) + const { block, blockHeader } = await getLatestBlockHeader(hre) await headerSync.setSyncedHeader(ethers.constants.HashZero) - const logsBloom = block.logsBloom.toString().substring(2) - - const blockHeader: BlockHeader = { - parentHash: block.parentHash, - ommersHash: block.sha3Uncles, - beneficiary: block.miner, - stateRoot: block.stateRoot, - transactionsRoot: block.transactionsRoot, - receiptsRoot: block.receiptsRoot, - logsBloom: logsBloom - .match(/.{1,64}/g)! - .map((s: string) => "0x" + s), - difficulty: block.difficulty, - height: block.number, - gasLimit: block.gasLimit, - gasUsed: block.gasUsed, - timestamp: block.timestamp, - extraData: block.extraData, - mixHash: block.mixHash, - nonce: block.nonce, - baseFeePerGas: block.baseFeePerGas - ? parseInt(block.baseFeePerGas) - : 0, - } - - const proof: EthGetProofResponse = await ethers.provider.send( - "eth_getProof", - [l1Bridge.address, [key], block.hash] - ) - - // RLP encode the proof together for LibTrieProof to decode - const encodedProof = ethers.utils.defaultAbiCoder.encode( - ["bytes", "bytes"], - [ - RLP.encode(proof.accountProof), - RLP.encode(proof.storageProof[0].proof), - ] - ) - // encode the SignalProof struct from LibBridgeSignal - const signalProof = ethers.utils.defaultAbiCoder.encode( - [ - "tuple(tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, bytes proof)", - ], - [{ header: blockHeader, proof: encodedProof }] + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader ) await expect( @@ -768,30 +664,8 @@ describe("integration:Bridge", function () { }) it("should throw if message has not been received", async function () { - const { - owner, - l1Bridge, - srcChainId, - enabledDestChainId, - l2Bridge, - headerSync, - } = await deployBridgeFixture() - - const m: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: enabledDestChainId, - owner: owner.address, - to: owner.address, - refundAddress: owner.address, - depositValue: 1000, - callValue: 1000, - processingFee: 1000, - gasLimit: 10000, - data: ethers.constants.HashZero, - memo: "", - } + const { l1Bridge, l2Bridge, headerSync, m } = + await deployBridgeFixture() const expectedAmount = m.depositValue + m.callValue + m.processingFee @@ -820,40 +694,10 @@ describe("integration:Bridge", function () { ) ) - // use this instead of ethers.provider.getBlock() beccause it doesnt have stateRoot - // in the response - const block: Block = await ethers.provider.send( - "eth_getBlockByNumber", - ["latest", false] - ) + const { block, blockHeader } = await getLatestBlockHeader(hre) await headerSync.setSyncedHeader(ethers.constants.HashZero) - const logsBloom = block.logsBloom.toString().substring(2) - - const blockHeader: BlockHeader = { - parentHash: block.parentHash, - ommersHash: block.sha3Uncles, - beneficiary: block.miner, - stateRoot: block.stateRoot, - transactionsRoot: block.transactionsRoot, - receiptsRoot: block.receiptsRoot, - logsBloom: logsBloom - .match(/.{1,64}/g)! - .map((s: string) => "0x" + s), - difficulty: block.difficulty, - height: block.number, - gasLimit: block.gasLimit, - gasUsed: block.gasUsed, - timestamp: block.timestamp, - extraData: block.extraData, - mixHash: block.mixHash, - nonce: block.nonce, - baseFeePerGas: block.baseFeePerGas - ? parseInt(block.baseFeePerGas) - : 0, - } - // get storageValue for the key const storageValue = await ethers.provider.getStorageAt( l1Bridge.address, @@ -864,26 +708,13 @@ describe("integration:Bridge", function () { expect(storageValue).to.be.eq( "0x0000000000000000000000000000000000000000000000000000000000000001" ) - // rpc call to get the merkle proof what value is at key on the bridge contract - const proof: EthGetProofResponse = await ethers.provider.send( - "eth_getProof", - [l1Bridge.address, [key], block.hash] - ) - // RLP encode the proof together for LibTrieProof to decode - const encodedProof = ethers.utils.defaultAbiCoder.encode( - ["bytes", "bytes"], - [ - RLP.encode(proof.accountProof), - RLP.encode(proof.storageProof[0].proof), - ] - ) - // encode the SignalProof struct from LibBridgeSignal - const signalProof = ethers.utils.defaultAbiCoder.encode( - [ - "tuple(tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, bytes proof)", - ], - [{ header: blockHeader, proof: encodedProof }] + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader ) await expect( @@ -892,34 +723,144 @@ describe("integration:Bridge", function () { }) it("processes a message when the signal has been verified from the sending chain", async () => { + const { l1Bridge, l2Bridge, headerSync, m } = + await deployBridgeFixture() + + const expectedAmount = + m.depositValue + m.callValue + m.processingFee + const tx = await l1Bridge.sendMessage(m, { + value: expectedAmount, + }) + + const receipt = await tx.wait() + + const [messageSentEvent] = receipt.events as any as Event[] + + const { signal, message } = (messageSentEvent as any).args + + expect(signal).not.to.be.eq(ethers.constants.HashZero) + + const messageStatus = await l1Bridge.getMessageStatus(signal) + + expect(messageStatus).to.be.eq(0) + + const sender = l1Bridge.address + + const key = ethers.utils.keccak256( + ethers.utils.solidityPack( + ["address", "bytes32"], + [sender, signal] + ) + ) + + const { block, blockHeader } = await getLatestBlockHeader(hre) + + await headerSync.setSyncedHeader(block.hash) + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ) + // make sure it equals 1 so our proof will pass + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ) + + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader + ) + + expect( + await l2Bridge.processMessage(message, signalProof, { + gasLimit: BigNumber.from(2000000), + }) + ).to.emit(l2Bridge, "MessageStatusChanged") + }) + }) + + describe("isMessageSent()", function () { + it("should return false, since no message was sent", async function () { + const { l1Bridge, m } = await deployBridgeFixture() + + const libData = await ( + await ethers.getContractFactory("TestLibBridgeData") + ).deploy() + const signal = await libData.hashMessage(m) + + expect(await l1Bridge.isMessageSent(signal)).to.be.eq(false) + }) + + it("should return true if message was sent properly", async function () { + const { l1Bridge, m } = await deployBridgeFixture() + + const expectedAmount = + m.depositValue + m.callValue + m.processingFee + const tx = await l1Bridge.sendMessage(m, { + value: expectedAmount, + }) + + const receipt = await tx.wait() + + const [messageSentEvent] = receipt.events as any as Event[] + + const { signal } = (messageSentEvent as any).args + + expect(signal).not.to.be.eq(ethers.constants.HashZero) + + expect(await l1Bridge.isMessageSent(signal)).to.be.eq(true) + }) + }) + + describe("retryMessage()", function () { + async function retriableMessageSetup() { const { owner, + l2Signer, + nonOwner, + l2NonOwner, l1Bridge, - srcChainId, - enabledDestChainId, l2Bridge, + addressManager, + enabledDestChainId, + l1EtherVault, + l2EtherVault, + srcChainId, headerSync, } = await deployBridgeFixture() + const testBadReceiver: TestBadReceiver = await ( + await ethers.getContractFactory("TestBadReceiver") + ) + .connect(l2Signer) + .deploy() + + await testBadReceiver.deployed() + const m: Message = { id: 1, sender: owner.address, srcChainId: srcChainId, destChainId: enabledDestChainId, owner: owner.address, - to: owner.address, + to: testBadReceiver.address, refundAddress: owner.address, depositValue: 1000, callValue: 1000, processingFee: 1000, - gasLimit: 10000, + gasLimit: 1, data: ethers.constants.HashZero, memo: "", } const expectedAmount = m.depositValue + m.callValue + m.processingFee - const tx = await l1Bridge.sendMessage(m, { + const tx = await l1Bridge.connect(owner).sendMessage(m, { value: expectedAmount, }) @@ -944,39 +885,76 @@ describe("integration:Bridge", function () { ) ) - // use this instead of ethers.provider.getBlock() beccause it doesnt have stateRoot - // in the response - const block: Block = await ethers.provider.send( - "eth_getBlockByNumber", - ["latest", false] - ) + const { block, blockHeader } = await getLatestBlockHeader(hre) await headerSync.setSyncedHeader(block.hash) - const logsBloom = block.logsBloom.toString().substring(2) - - const blockHeader: BlockHeader = { - parentHash: block.parentHash, - ommersHash: block.sha3Uncles, - beneficiary: block.miner, - stateRoot: block.stateRoot, - transactionsRoot: block.transactionsRoot, - receiptsRoot: block.receiptsRoot, - logsBloom: logsBloom - .match(/.{1,64}/g)! - .map((s: string) => "0x" + s), - difficulty: block.difficulty, - height: block.number, - gasLimit: block.gasLimit, - gasUsed: block.gasUsed, - timestamp: block.timestamp, - extraData: block.extraData, - mixHash: block.mixHash, - nonce: block.nonce, - baseFeePerGas: block.baseFeePerGas - ? parseInt(block.baseFeePerGas) - : 0, + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader + ) + + await l2Bridge + .connect(l2NonOwner) + .processMessage(message, signalProof, { + gasLimit: BigNumber.from(2000000), + }) + + const status = await l2Bridge.getMessageStatus(signal) + expect(status).to.be.eq(1) // message is retriable now + // because the LibBridgeInvoke call failed, because + // message.to is a bad receiver and throws upon receipt + + return { + message, + l2Signer, + l2NonOwner, + l1Bridge, + l2Bridge, + addressManager, + headerSync, + owner, + nonOwner, + srcChainId, + enabledDestChainId, + l1EtherVault, + l2EtherVault, + signal, } + } + it("setup message to fail first processMessage", async function () { + const { l2Bridge, signal } = await retriableMessageSetup() + l2Bridge + signal + }) + }) + + describe("isMessageReceived()", function () { + it("should throw if signal is not a bridge message; proof is invalid since sender != bridge.", async function () { + const { owner, l1Bridge, l2Bridge, headerSync, srcChainId } = + await deployBridgeFixture() + + const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)) + + const tx = await l1Bridge.connect(owner).sendSignal(signal) + + await tx.wait() + + const sender = owner.address + + const key = ethers.utils.keccak256( + ethers.utils.solidityPack( + ["address", "bytes32"], + [sender, signal] + ) + ) + + const { block, blockHeader } = await getLatestBlockHeader(hre) + + await headerSync.setSyncedHeader(block.hash) // get storageValue for the key const storageValue = await ethers.provider.getStorageAt( @@ -984,37 +962,219 @@ describe("integration:Bridge", function () { key, block.number ) - // make sure it equals 1 so our proof will pass + // // make sure it equals 1 so we know sendSignal worked expect(storageValue).to.be.eq( "0x0000000000000000000000000000000000000000000000000000000000000001" ) - // rpc call to get the merkle proof what value is at key on the bridge contract - const proof: EthGetProofResponse = await ethers.provider.send( - "eth_getProof", - [l1Bridge.address, [key], block.hash] + + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader ) - // RLP encode the proof together for LibTrieProof to decode - const encodedProof = ethers.utils.defaultAbiCoder.encode( - ["bytes", "bytes"], - [ - RLP.encode(proof.accountProof), - RLP.encode(proof.storageProof[0].proof), - ] + await expect( + l2Bridge.isMessageReceived(signal, srcChainId, signalProof) + ).to.be.revertedWith("Invalid large internal hash") + }) + + it("should return true", async function () { + const { l1Bridge, srcChainId, l2Bridge, headerSync, m } = + await deployBridgeFixture() + + const expectedAmount = + m.depositValue + m.callValue + m.processingFee + const tx = await l1Bridge.sendMessage(m, { + value: expectedAmount, + }) + + const receipt = await tx.wait() + + const [messageSentEvent] = receipt.events as any as Event[] + + const { signal } = (messageSentEvent as any).args + + const sender = l1Bridge.address + + const key = ethers.utils.keccak256( + ethers.utils.solidityPack( + ["address", "bytes32"], + [sender, signal] + ) ) - // encode the SignalProof struct from LibBridgeSignal - const signalProof = ethers.utils.defaultAbiCoder.encode( - [ - "tuple(tuple(bytes32 parentHash, bytes32 ommersHash, address beneficiary, bytes32 stateRoot, bytes32 transactionsRoot, bytes32 receiptsRoot, bytes32[8] logsBloom, uint256 difficulty, uint128 height, uint64 gasLimit, uint64 gasUsed, uint64 timestamp, bytes extraData, bytes32 mixHash, uint64 nonce, uint256 baseFeePerGas) header, bytes proof)", - ], - [{ header: blockHeader, proof: encodedProof }] + + const { block, blockHeader } = await getLatestBlockHeader(hre) + + await headerSync.setSyncedHeader(block.hash) + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ) + // // make sure it equals 1 so we know sendMessage worked + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" ) - // ROGER: this is where we at, we need now to deploy a custom TestHeaderSync that implements - // IHeaderSync where we can manually save synced headers. - await l2Bridge.processMessage(message, signalProof, { - gasLimit: BigNumber.from(2000000), - }) + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader + ) + + expect( + await l2Bridge.isMessageReceived( + signal, + srcChainId, + signalProof + ) + ).to.be.eq(true) + }) + }) + + describe("isSignalReceived()", function () { + it("should throw if sender == address(0)", async function () { + const { l2Bridge, srcChainId } = await deployBridgeFixture() + + const signal = ethers.utils.randomBytes(32) + const sender = ethers.constants.AddressZero + const signalProof = ethers.constants.HashZero + + await expect( + l2Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.revertedWith("B:sender") + }) + + it("should throw if signal == HashZero", async function () { + const { owner, l2Bridge, srcChainId } = await deployBridgeFixture() + + const signal = ethers.constants.HashZero + const sender = owner.address + const signalProof = ethers.constants.HashZero + + await expect( + l2Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.revertedWith("B:signal") + }) + + it("should throw if calling from same layer", async function () { + const { owner, l1Bridge, headerSync, srcChainId } = + await deployBridgeFixture() + const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)) + + const tx = await l1Bridge.connect(owner).sendSignal(signal) + + await tx.wait() + + const sender = owner.address + + const key = ethers.utils.keccak256( + ethers.utils.solidityPack( + ["address", "bytes32"], + [sender, signal] + ) + ) + + const { block, blockHeader } = await getLatestBlockHeader(hre) + + await headerSync.setSyncedHeader(block.hash) + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ) + // make sure it equals 1 so our proof is valid + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ) + + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader + ) + + await expect( + l1Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.revertedWith("B:srcBridge") + }) + + it("should return true and pass", async function () { + const { owner, l1Bridge, l2Bridge, headerSync, srcChainId } = + await deployBridgeFixture() + + const signal = ethers.utils.hexlify(ethers.utils.randomBytes(32)) + + const tx = await l1Bridge.connect(owner).sendSignal(signal) + + await tx.wait() + + const sender = owner.address + + const key = ethers.utils.keccak256( + ethers.utils.solidityPack( + ["address", "bytes32"], + [sender, signal] + ) + ) + + const { block, blockHeader } = await getLatestBlockHeader(hre) + + await headerSync.setSyncedHeader(block.hash) + + // get storageValue for the key + const storageValue = await ethers.provider.getStorageAt( + l1Bridge.address, + key, + block.number + ) + // make sure it equals 1 so our proof will pass + expect(storageValue).to.be.eq( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ) + + const signalProof = await getSignalProof( + hre, + l1Bridge.address, + key, + block.number, + blockHeader + ) + // proving functionality; l2Bridge can check if l1Bridge receives a signal + // allowing for dapp cross layer communication + expect( + await l2Bridge.isSignalReceived( + signal, + srcChainId, + sender, + signalProof + ) + ).to.be.eq(true) }) }) }) diff --git a/packages/protocol/test/bridge/libs/LibBridgeData.test.ts b/packages/protocol/test/bridge/libs/LibBridgeData.test.ts index 9729cfc3c67..7da0b4791eb 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeData.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeData.test.ts @@ -1,17 +1,19 @@ import { expect } from "chai" import { ethers } from "hardhat" +import { TestLibBridgeData } from "../../../typechain" import { K_BRIDGE_MESSAGE } from "../../constants/messages" import { MessageStatus } from "../../../tasks/utils" +import { Message } from "../../utils/message" describe("LibBridgeData", function () { async function deployLibBridgeDataFixture() { const [owner, nonOwner] = await ethers.getSigners() - const libData = await ( + const libData: TestLibBridgeData = await ( await ethers.getContractFactory("TestLibBridgeData") ).deploy() - const testMessage = { + const testMessage: Message = { id: 1, sender: owner.address, srcChainId: 1, @@ -41,7 +43,6 @@ describe("LibBridgeData", function () { testMessage, testTypes, testVar, - MessageStatus, } } @@ -49,7 +50,6 @@ describe("LibBridgeData", function () { it("should return properly hashed message", async function () { const { libData, testMessage, testTypes } = await deployLibBridgeDataFixture() - // dummy struct to test with const testVar = [K_BRIDGE_MESSAGE, testMessage] const hashed = await libData.hashMessage(testMessage) @@ -67,7 +67,7 @@ describe("LibBridgeData", function () { const { libData } = await deployLibBridgeDataFixture() // dummy struct to test with - const testMessage = { + const testMessage: Message = { id: 0, sender: "0xDA1Ea1362475997419D2055dD43390AEE34c6c37", srcChainId: 31336, @@ -93,8 +93,7 @@ describe("LibBridgeData", function () { describe("updateMessageStatus()", async function () { it("should emit upon successful change, and value should be changed correctly", async function () { - const { libData, testMessage, MessageStatus } = - await deployLibBridgeDataFixture() + const { libData, testMessage } = await deployLibBridgeDataFixture() const signal = await libData.hashMessage(testMessage) @@ -108,8 +107,7 @@ describe("LibBridgeData", function () { }) it("unchanged MessageStatus should not emit event", async function () { - const { libData, testMessage, MessageStatus } = - await deployLibBridgeDataFixture() + const { libData, testMessage } = await deployLibBridgeDataFixture() const signal = await libData.hashMessage(testMessage) diff --git a/packages/protocol/test/bridge/libs/LibBridgeInvoke.test.ts b/packages/protocol/test/bridge/libs/LibBridgeInvoke.test.ts index 8bd38e5ba8d..eba7340ca9e 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeInvoke.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeInvoke.test.ts @@ -1,10 +1,15 @@ import { expect } from "chai" import { ethers } from "hardhat" import { Message } from "../../utils/message" +import { + TestLibBridgeData, + TestLibBridgeInvoke, + TestReceiver, +} from "../../../typechain" describe("LibBridgeInvoke", function () { async function deployLibBridgeDataFixture() { - const libData = await ( + const libData: TestLibBridgeData = await ( await ethers.getContractFactory("TestLibBridgeData") ).deploy() return { libData } @@ -13,7 +18,7 @@ describe("LibBridgeInvoke", function () { async function deployLibBridgeInvokeFixture() { const [owner, nonOwner] = await ethers.getSigners() - const libInvoke = await ( + const libInvoke: TestLibBridgeInvoke = await ( await ethers.getContractFactory("TestLibBridgeInvoke") ) .connect(owner) @@ -88,7 +93,7 @@ describe("LibBridgeInvoke", function () { const { libData } = await deployLibBridgeDataFixture() - const testReceiver = await ( + const testReceiver: TestReceiver = await ( await ethers.getContractFactory("TestReceiver") ).deploy() diff --git a/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts b/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts index a05b9c458e8..431028b9ede 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeProcess.test.ts @@ -1,11 +1,17 @@ import * as helpers from "@nomicfoundation/hardhat-network-helpers" import { expect } from "chai" -import * as fs from "fs" import hre, { ethers } from "hardhat" +import * as fs from "fs" import * as path from "path" import { getSlot, MessageStatus } from "../../../tasks/utils" -import { AddressManager, Bridge } from "../../../typechain" import { Message } from "../../utils/message" +import { + AddressManager, + EtherVault, + LibTrieProof, + TestLibBridgeData, + TestLibBridgeProcess, +} from "../../../typechain" describe("LibBridgeProcess", function () { function getStateSlot() { @@ -51,7 +57,9 @@ describe("LibBridgeProcess", function () { ).deploy() await addressManager.init() - const etherVault = await (await ethers.getContractFactory("EtherVault")) + const etherVault: EtherVault = await ( + await ethers.getContractFactory("EtherVault") + ) .connect(etherVaultOwner) .deploy() @@ -71,7 +79,7 @@ describe("LibBridgeProcess", function () { value: ethers.utils.parseEther("10.0"), }) - const libTrieLink = await ( + const libTrieLink: LibTrieProof = await ( await ethers.getContractFactory("LibTrieProof") ) .connect(owner) @@ -89,7 +97,7 @@ describe("LibBridgeProcess", function () { .deploy() await libProcessLink.deployed() - const libProcess = await ( + const libProcess: TestLibBridgeProcess = await ( await ethers.getContractFactory("TestLibBridgeProcess", { libraries: { LibBridgeProcess: libProcessLink.address, @@ -101,7 +109,7 @@ describe("LibBridgeProcess", function () { await libProcess.init(addressManager.address) - const testLibData = await ( + const testLibData: TestLibBridgeData = await ( await ethers.getContractFactory("TestLibBridgeData") ).deploy() @@ -114,7 +122,6 @@ describe("LibBridgeProcess", function () { srcChainId, messageOwner, libProcess, - MessageStatus, stateSlot, blockChainId, nonOwner, @@ -122,60 +129,6 @@ describe("LibBridgeProcess", function () { addressManager, } } - async function deployBridgeFixture() { - const [owner, etherVault] = await ethers.getSigners() - - const addressManager: AddressManager = await ( - await ethers.getContractFactory("AddressManager") - ).deploy() - await addressManager.init() - - const chainId = hre.network.config.chainId ?? 0 - - await addressManager.setAddress( - `${chainId}.ether_vault`, - etherVault.address - ) - - const libTrieProof = await ( - await ethers.getContractFactory("LibTrieProof") - ).deploy() - - const libBridgeProcess = await ( - await ethers.getContractFactory("LibBridgeProcess", { - libraries: { - LibTrieProof: libTrieProof.address, - }, - }) - ).deploy() - - const libBridgeRetry = await ( - await ethers.getContractFactory("LibBridgeRetry") - ).deploy() - - const BridgeFactory = await ethers.getContractFactory("Bridge", { - libraries: { - LibBridgeProcess: libBridgeProcess.address, - LibBridgeRetry: libBridgeRetry.address, - LibTrieProof: libTrieProof.address, - }, - }) - - const bridge: Bridge = await BridgeFactory.connect(owner).deploy() - - await bridge.init(addressManager.address) - - const headerSync = await ( - await ethers.getContractFactory("TestHeaderSync") - ).deploy() - - await addressManager.setAddress(`${chainId}.taiko`, headerSync.address) - - return { - bridge, - headerSync, - } - } describe("processMessage()", async function () { it("should throw if gaslimit == 0 & msg.sender != message.owner", async function () { @@ -233,7 +186,6 @@ describe("LibBridgeProcess", function () { libProcess, testLibData, blockChainId, - MessageStatus, stateSlot, } = await deployLibBridgeProcessFixture() @@ -265,51 +217,6 @@ describe("LibBridgeProcess", function () { libProcess.processMessage(message, ethers.constants.HashZero) ).to.be.revertedWith("B:status") }) - - it("should throw if signal has not been received", async function () { - const { - owner, - srcChainId, - nonOwner, - libProcess, - blockChainId, - addressManager, - } = await deployLibBridgeProcessFixture() - const { bridge } = await deployBridgeFixture() - - await addressManager.setAddress( - `${srcChainId}.bridge`, - bridge.address - ) - - const message: Message = { - id: 1, - sender: owner.address, - srcChainId: srcChainId, - destChainId: blockChainId, - owner: owner.address, - to: nonOwner.address, - refundAddress: owner.address, - depositValue: 1, - callValue: 1, - processingFee: 1, - gasLimit: 100000000, - data: ethers.constants.HashZero, - memo: "", - } - await expect( - libProcess.processMessage(message, ethers.constants.HashZero) - ).to.be.reverted - - // Reverts because there is no HeaderSync, can't test without - }) - - it("if message fails, refund should go to the intended account and amount", async function () { - await deployLibBridgeProcessFixture() - }) - - it("should pass properly, message should be processed and marked DONE", async function () { - await deployLibBridgeProcessFixture() - }) + // Remaining test cases require integration, will be covered in Bridge.test.ts }) }) diff --git a/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts b/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts index b67b2cc22d7..a655b4e7c01 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeRetry.test.ts @@ -1,25 +1,35 @@ import * as helpers from "@nomicfoundation/hardhat-network-helpers" import { expect } from "chai" import hre, { ethers } from "hardhat" -import { decode, getSlot, MessageStatus } from "../../../tasks/utils" import { Message } from "../../utils/message" +import { getSlot, decode, MessageStatus } from "../../../tasks/utils" +import { + AddressManager, + EtherVault, + TestBadReceiver, + TestLibBridgeData, + TestLibBridgeRetry, + TestReceiver, +} from "../../../typechain" describe("LibBridgeRetry", function () { async function deployLibBridgeRetryFixture() { const [owner, refundAddress, nonOwner, etherVaultOwner] = await ethers.getSigners() - const addressManager = await ( + const addressManager: AddressManager = await ( await ethers.getContractFactory("AddressManager") ).deploy() await addressManager.init() - const badAddressManager = await ( + const badAddressManager: AddressManager = await ( await ethers.getContractFactory("AddressManager") ).deploy() await badAddressManager.init() - const etherVault = await (await ethers.getContractFactory("EtherVault")) + const etherVault: EtherVault = await ( + await ethers.getContractFactory("EtherVault") + ) .connect(etherVaultOwner) .deploy() @@ -57,11 +67,11 @@ describe("LibBridgeRetry", function () { }) ).connect(owner) - const libRetry = await libRetryFactory.deploy() + const libRetry: TestLibBridgeRetry = await libRetryFactory.deploy() await libRetry.init(addressManager.address) await libRetry.deployed() - const badLibRetry = await libRetryFactory.deploy() + const badLibRetry: TestLibBridgeRetry = await libRetryFactory.deploy() await badLibRetry.init(badAddressManager.address) await badLibRetry.deployed() @@ -69,7 +79,7 @@ describe("LibBridgeRetry", function () { .connect(etherVaultOwner) .authorize(libRetry.address, true) - const testLibData = await ( + const testLibData: TestLibBridgeData = await ( await ethers.getContractFactory("TestLibBridgeData") ).deploy() @@ -79,8 +89,8 @@ describe("LibBridgeRetry", function () { nonOwner, libRetry, testLibData, - MessageStatus, badLibRetry, + etherVault, } } @@ -161,15 +171,10 @@ describe("LibBridgeRetry", function () { }) it("if etherVault resolves to address(0), retry should fail and messageStatus should not change if not lastAttempt since no ether received", async function () { - const { - owner, - refundAddress, - testLibData, - MessageStatus, - badLibRetry, - } = await deployLibBridgeRetryFixture() - - const testReceiver = await ( + const { owner, refundAddress, testLibData, badLibRetry } = + await deployLibBridgeRetryFixture() + + const testReceiver: TestReceiver = await ( await ethers.getContractFactory("TestReceiver") ).deploy() @@ -218,15 +223,10 @@ describe("LibBridgeRetry", function () { }) it("should fail, but since lastAttempt == true messageStatus should be set to DONE", async function () { - const { - owner, - libRetry, - refundAddress, - testLibData, - MessageStatus, - } = await deployLibBridgeRetryFixture() - - const testBadReceiver = await ( + const { owner, libRetry, refundAddress, testLibData } = + await deployLibBridgeRetryFixture() + + const testBadReceiver: TestBadReceiver = await ( await ethers.getContractFactory("TestBadReceiver") ).deploy() @@ -277,16 +277,68 @@ describe("LibBridgeRetry", function () { ) }) + it("should fail, messageStatus is still RETRIABLE and balance is returned to etherVault", async function () { + const { owner, libRetry, testLibData, etherVault } = + await deployLibBridgeRetryFixture() + + const testBadReceiver: TestBadReceiver = await ( + await ethers.getContractFactory("TestBadReceiver") + ).deploy() + + await testBadReceiver.deployed() + + const destChainId = 5 + const message: Message = { + id: 1, + sender: owner.address, + srcChainId: 1, + destChainId: destChainId, + owner: owner.address, + to: testBadReceiver.address, + refundAddress: ethers.constants.AddressZero, + depositValue: 0, + callValue: 1, + processingFee: 1, + gasLimit: 300000, + data: ethers.constants.HashZero, + memo: "", + } + + const signal = await testLibData.hashMessage(message) + + await helpers.setStorageAt( + libRetry.address, + await getSlot(hre, signal, 202), + MessageStatus.RETRIABLE + ) + + const originalBalance = await ethers.provider.getBalance( + etherVault.address + ) + await libRetry.retryMessage(message, false) + const balancePlusRefund = await ethers.provider.getBalance( + etherVault.address + ) + + expect( + await decode( + hre, + "uint256", + await ethers.provider.getStorageAt( + libRetry.address, + getSlot(hre, signal, 202) + ) + ) + ).to.equal(MessageStatus.RETRIABLE.toString()) + + expect(balancePlusRefund).to.be.equal(originalBalance) + }) + it("should succeed, set message status to done, invoke message succesfsully", async function () { - const { - owner, - libRetry, - refundAddress, - testLibData, - MessageStatus, - } = await deployLibBridgeRetryFixture() - - const testReceiver = await ( + const { owner, libRetry, refundAddress, testLibData } = + await deployLibBridgeRetryFixture() + + const testReceiver: TestReceiver = await ( await ethers.getContractFactory("TestReceiver") ).deploy() diff --git a/packages/protocol/test/bridge/libs/LibBridgeSend.test.ts b/packages/protocol/test/bridge/libs/LibBridgeSend.test.ts index 12b26ad3e86..23416ecd0a3 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeSend.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeSend.test.ts @@ -1,28 +1,46 @@ import { expect } from "chai" import hre, { ethers } from "hardhat" +import { + AddressManager, + TestLibBridgeSend, + EtherVault, +} from "../../../typechain" import { Message } from "../../utils/message" describe("LibBridgeSend", function () { async function deployLibBridgeSendFixture() { - const [owner, nonOwner, etherVault] = await ethers.getSigners() + const [owner, nonOwner, etherVaultOwner] = await ethers.getSigners() - const addressManager = await ( + const addressManager: AddressManager = await ( await ethers.getContractFactory("AddressManager") ).deploy() await addressManager.init() + + const etherVault: EtherVault = await ( + await ethers.getContractFactory("EtherVault") + ) + .connect(etherVaultOwner) + .deploy() + + await etherVault.deployed() + await etherVault.init(addressManager.address) + const blockChainId = hre.network.config.chainId ?? 0 await addressManager.setAddress( `${blockChainId}.ether_vault`, etherVault.address ) - const libSend = await ( + const libSend: TestLibBridgeSend = await ( await ethers.getContractFactory("TestLibBridgeSend") ) .connect(owner) .deploy() await libSend.init(addressManager.address) + await etherVault + .connect(etherVaultOwner) + .authorize(libSend.address, true) const srcChainId = 1 diff --git a/packages/protocol/test/bridge/libs/LibBridgeSignal.test.ts b/packages/protocol/test/bridge/libs/LibBridgeSignal.test.ts index 12e2aef883d..7ea337d1f1b 100644 --- a/packages/protocol/test/bridge/libs/LibBridgeSignal.test.ts +++ b/packages/protocol/test/bridge/libs/LibBridgeSignal.test.ts @@ -1,6 +1,7 @@ import { expect } from "chai" import { ethers } from "hardhat" -import { TestLibBridgeSignal } from "../../../typechain" +import { TestLibBridgeData, TestLibBridgeSignal } from "../../../typechain" +import { Message } from "../../utils/message" describe("integration:LibBridgeSignal", function () { async function deployLibBridgeSignalFixture() { @@ -10,7 +11,7 @@ describe("integration:LibBridgeSignal", function () { await ethers.getContractFactory("TestLibBridgeSignal") ).deploy() - const testMessage = { + const testMessage: Message = { id: 1, sender: owner.address, srcChainId: 1, @@ -29,7 +30,7 @@ describe("integration:LibBridgeSignal", function () { return { owner, nonOwner, libSignal, testMessage } } async function deployLibBridgeDataFixture() { - const libData = await ( + const libData: TestLibBridgeData = await ( await ethers.getContractFactory("TestLibBridgeData") ).deploy() return { libData } @@ -57,6 +58,7 @@ describe("integration:LibBridgeSignal", function () { ).to.be.revertedWith("B:signal") }) }) + describe("isSignalSent()", async function () { it("properly sent message should be received", async function () { const { owner, libSignal, testMessage } =