diff --git a/pages/interop/tutorials/unit-tests.mdx b/pages/interop/tutorials/unit-tests.mdx new file mode 100644 index 000000000..e5467827f --- /dev/null +++ b/pages/interop/tutorials/unit-tests.mdx @@ -0,0 +1,29 @@ +--- +title: Interop and unit tests +description: Learn how to write unit tests for interop +lang: en-US +content_type: landing-page +topic: interop-tutorial-unit-tests +personas: + - app-developer +categories: + - protocol + - interoperability + - cross-chain-messaging + - tutorial + - testing +is_imported_content: 'false' +--- + +import { Card, Cards } from 'nextra/components' + +# Interop and unit tests + +Documentation covering Interop related tutorials. + + + } /> + + } /> + + diff --git a/pages/interop/tutorials/unit-tests/_meta.json b/pages/interop/tutorials/unit-tests/_meta.json new file mode 100644 index 000000000..e9cbbaa02 --- /dev/null +++ b/pages/interop/tutorials/unit-tests/_meta.json @@ -0,0 +1,4 @@ +{ + "foundry": "Foundry", + "hardhat": "Hardhat" +} diff --git a/pages/interop/tutorials/unit-tests/foundry.mdx b/pages/interop/tutorials/unit-tests/foundry.mdx new file mode 100644 index 000000000..bf5f7156f --- /dev/null +++ b/pages/interop/tutorials/unit-tests/foundry.mdx @@ -0,0 +1,147 @@ +--- +title: Interop and unit tests using Foundry +description: Learn how to write unit tests for interop when using Foundry +lang: en-US +content_type: tutorial +topic: interop-tutorial-unit-tests-foundry +personas: + - app-developer +categories: + - protocol + - interoperability + - cross-chain-messaging + - tutorial + - testing + - foundry +is_imported_content: 'false' +--- + +import { Callout, Steps } from 'nextra/components' +import { InteropCallout } from '@/components/WipCallout' + + + +# Interop and unit tests using Foundry + +Most parts of the contracts can be [tested normally](/app-developers/testing-apps). +This tutorial teaches you how to verify that a message has been sent successfully and to simulate receiving a message, the two functions that tie directly into interop. +To show how this works, we test [the `Greeter` and `GreetingSender` contracts](/interop/tutorials/message-passing). + +## Setup + +This script creates a Foundry project (in `testing/forge`) and a Hardhat project (in `testing/hardhat`) with the necessary files. +Execute it in a UNIX or Linux environment. + +The results of this step are similar to what the [message passing tutorial](/interop/tutorials/message-passing) produces. + +```sh file=/public/tutorials/setup-for-testing.sh hash=4d0fa175564131911ab6a12fb110294c +``` + +## Test sending a message + +The easiest way to test sending a message is to see [the event emitted by `L2ToL2CrossDomainMessenger.sendMessage`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L160). + +To see this in action, run these commands: + +```sh +cd testing/forge +forge test GreetingSender --fork-url https://interop-alpha-0.optimism.io +``` + +The default [`anvil`](https://book.getfoundry.sh/anvil/overview) instance created by `forge test` does not contain the interop contracts, so we need to fork [a blockchain that does](/interop/tools/devnet). + +The test is implemented by `tests/GreetingSender.t.sol`. + +
+ Explanation + + ```solidity file=/public/tutorials/setup-for-testing.sh#L109-L117 hash=e8c77299b01dd6f03d3e36d2e9d82608 + ``` + + The definition for the event we expect to see, [from the `L2ToL2CrossDomainMessenger` source code](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L160). + + ```solidity file=/public/tutorials/setup-for-testing.sh#L119-L121 hash=645c522fafccb282df37b7d3df27bd3f + ``` + + Create a new `GreetingSender` instance for each test. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L124-L129 hash=5e79ce67c4e5f54eee5b8d09ace39860 + ``` + + Calculate the message to be sent, the same way that `GreetingSender` does. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L132-L132 hash=028411291cccc262557abde834549fc2 + ``` + + Out of the indexed topics, verify the first (destination chain) and second (address on the destination chain). + Ignore the third topic, the nonce, because it can vary. + Finally, verify that the unindexed data of the log entry (the sender address and the message) is correct. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L133-L133 hash=37203df9264e95e763befaf2e5dbbfab + ``` + + Emit the message we expect to see. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L135-L135 hash=9322933b2f965ddc75bb8d3b2ab4c95d + ``` + + Call the code being tested, which should emit a similar log entry to the one we just emitted. +
+ +## Test receiving a message + +To simulate receiving a message, we need to ensure two conditions are fulfilled: + +* The tested code is called by [`L2ToL2CrossDomainMessenger`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol). +* The tested code can receive additional information through simulations of: + * [`L2ToL2CrossDomainMessenger.crossDomainMessageSender`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L102-L108) + * [`L2ToL2CrossDomainMessenger.crossDomainMessageSource`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L110-L116) + * [`L2ToL2CrossDomainMessenger.crossDomainMessageContext`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L118-L126) + +To see this in action, run these commands: + +``` +cd testing/forge +forge test Greeter +``` + +The test is implemented by `tests/Greeter.t.sol`. + +
+ Explanation + + ```solidity file=/public/tutorials/setup-for-testing.sh#L171 hash=d9bb7b8ca88e8856bd86c256934a0b0b + ``` + + This function sets up the environment for a pretend interop call. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L172-L182 hash=65aae51c8573ecb296b942c7073a3379 + ``` + + Use [`vm.mockCall`](https://book.getfoundry.sh/reference/cheatcodes/mock-call) to specify the responses to calls to `L2ToL2CrossDomainMessenger`. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L183-L187 hash=478229c219493adbbfea088cf266dec5 + ``` + + At writing `crossDomainMessageContext` is not available in the [contracts npm package](https://www.npmjs.com/package/@eth-optimism/contracts-bedrock), so we calculate the selector directly using `bytes4(keccak256("crossDomainMessageContext()"))`. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L188-L189 hash=18498ce1031cf749a13f6207d0828ca7 + ``` + + Use [`vm.prank`](https://book.getfoundry.sh/reference/cheatcodes/prank) to make it appear the tested code is called by `L2ToL2CrossDomainMessenger`. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L191-L201 hash=035afa3348edfa18a0be06b9f9991c38 + ``` + + Test how `Greeter` acts when the greeting is set from another chain. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L203-L210 hash=ea83b0fcf27566b5ae9221d1091bfc6b + ``` + + Test how `Greeter` acts when the greeting is set from this chain. +
+ +## Next steps + +* Write a revolutionary app that uses multiple blockchains within the Superchain. +* Write tests to make sure it works correctly. diff --git a/pages/interop/tutorials/unit-tests/hardhat.mdx b/pages/interop/tutorials/unit-tests/hardhat.mdx new file mode 100644 index 000000000..1a58bc7c8 --- /dev/null +++ b/pages/interop/tutorials/unit-tests/hardhat.mdx @@ -0,0 +1,185 @@ +--- +title: Interop and unit tests using Hardhat +description: Learn how to write unit tests for interop when using Hardhat +lang: en-US +content_type: tutorial +topic: interop-tutorial-unit-tests-hardhat +personas: + - app-developer +categories: + - protocol + - interoperability + - cross-chain-messaging + - tutorial + - testing + - hardhat +is_imported_content: 'false' +--- + +import { Callout, Steps } from 'nextra/components' +import { InteropCallout } from '@/components/WipCallout' + + + +# Interop and unit tests using Hardhat + +Most parts of the contracts can be [tested normally](/app-developers/testing-apps). +This tutorial teaches you how to verify that a message has been sent successfully and to simulate receiving a message, the two functions that tie directly into interop. +To show how this works, we test [the `Greeter` and `GreetingSender` contracts](/interop/tutorials/message-passing). + +## Setup + +This script creates a Foundry project (in `testing/forge`) and a Hardhat project (in `testing/hardhat`) with the necessary files. +Execute it in a UNIX or Linux environment. + +The results of this step are similar to what the [message passing tutorial](/interop/tutorials/message-passing) produces. + +```sh file=/public/tutorials/setup-for-testing.sh hash=4d0fa175564131911ab6a12fb110294c +``` + +## Test sending a message + +The easiest way to test sending a message is to see [the event emitted by `L2ToL2CrossDomainMessenger.sendMessage`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L160). + +To see this in action, run these commands: + +```sh +cd testing/hardhat +npx hardhat test test/GreetingSender.js +``` + +Of course, Hardhat does not include the interop contracts by default. +To have `L2ToL2CrossDomainMessenger.sendMessage` we fork [a blockchain that does have it](/interop/tools/devnet). +This is specified in `hardhat.config.js`. + +The test is implemented by `test/GreetingSender.js`. + +
+ Explanation + + ```js file=/public/tutorials/setup-for-testing.sh#L258-L264 hash=3f669191f745ca77f1186d5d6b8dd062 + ``` + + Deploy a `GreetingSender`. + + ```js file=/public/tutorials/setup-for-testing.sh#L266-L270 hash=40785947670a88582c43f31e37d7cb28 + ``` + + Create a contract object for `L2ToL2CrossDomainMessenger`. + + ```js file=/public/tutorials/setup-for-testing.sh#L280-L286 hash=80f81c9dafbea8a958f0bfacb12e9389 + ``` + + Calculate the calldata that will be sent by `L2ToL2CrossDomainMessenger`. + + ```js file=/public/tutorials/setup-for-testing.sh#L288 hash=ea3c2728d499ef1536178d53fdb5497a + ``` + + The operation we're testing, `greetingSender.setGreeting(greeting)`. + + ```js file=/public/tutorials/setup-for-testing.sh#L289 hash=76eeeefe7b7605474ab8e43105e7dc0d + ``` + + The expected result, to have the `messenger` contract emit a `SentMessage` log entry. + + ```js file=/public/tutorials/setup-for-testing.sh#L290-L296 hash=14bf71dc909944de148fda7bcc4d0f2d + ``` + + The parameters in the log entry. + We use `anyValue` for the nonce because we do not know in advance what value it would be. +
+ +## Test receiving a message + +To simulate receiving a message, we need to ensure two conditions are fulfilled: + +* The tested code is called by [`L2ToL2CrossDomainMessenger`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol). +* The tested code can receive additional information through simulations of: + * [`L2ToL2CrossDomainMessenger.crossDomainMessageSender`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L102-L108) + * [`L2ToL2CrossDomainMessenger.crossDomainMessageSource`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L110-L116) + * [`L2ToL2CrossDomainMessenger.crossDomainMessageContext`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L118-L126) + +To see this in action, run these commands: + +``` +cd testing/hardhat +npx hardhat test test/Greeter.js +``` + +This test is implemented by two files, `contracts/MockL2ToL2Messenger.sol` which replaces `L2ToL2CrossDomainMessenger` with a contract we control, and `test/Greeter.js` which actually has the tests. + +
+ Explanation of `contracts/MockL2ToL2Messenger.sol` + + ```solidity file=/public/tutorials/setup-for-testing.sh#L374-L380 hash=870b143463550d8f0915a4db9d584cea + ``` + + When the tested code receive a message, these are the source chain and address where it originates. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L382-L392 hash=7f9cf709fa17ce391ec40728267b82a0 + ``` + + These are the three functions we need to implement for tested code, such as `Greeter`, to call. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L394-L408 hash=13b29fd8bbcccfc6c7a5201f5d316d0a + ``` + + Because we are replacing the `L2ToL2CrossDomainMessenger` with this contract, and the order in which tests are executed is not deterministic, we need this contract to work for the `GreetingSender` test. + The code here is copied from [`L2ToL2CrossDomainMessenger.sendMessage`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L128-L161), with the irrelevant parts removed. + + ```solidity file=/public/tutorials/setup-for-testing.sh#L410-L413 hash=0797417fc01fcc3a27511f55729e1cbe + ``` + + The interop message `Greeter` gets has to come from the `L2ToL2CrossDomainMessenger` address. + We can either send a transaction into `MockL2ToL2Messenger` that then calls `Greeter`, or give the `L2ToL2CrossDomainMessenger` address some ETH and send the transaction directly from it. + This function enables us to give the contract ETH. +
+ +
+ Explanation of `test/Greeter.js` + + ```js file=/public/tutorials/setup-for-testing.sh#L310 hash=6fd176fc44208d2f92f0210015c8e806 + ``` + + This function deploys the fixtures we need to run the tests. + + ```js file=/public/tutorials/setup-for-testing.sh#L312-L319 hash=7d218ce8fead9bc8412e79303d026c16 + ``` + + This is the code that first deploys `MockL2ToL2Messenger` and then tells Hardhat to direct calls to the `L2ToL2CrossDomainMessenger` contract to it instead. + + ```js file=/public/tutorials/setup-for-testing.sh#L321-L322 hash=b6df4166a44f7b5f006d269f97a4e34a + ``` + + Deploy a `Greeter` to test. + + ```js file=/public/tutorials/setup-for-testing.sh#L324-L328 hash=ee7023cfaf1b9672914aa77b75e77285 + ``` + + Create a contract object for `L2ToL2CrossDomainMessenger`. + + ```js file=/public/tutorials/setup-for-testing.sh#L334-L340 hash=d44100c903a4fd42aedadce81455226a + ``` + + Run the test for local messages. + + ```js file=/public/tutorials/setup-for-testing.sh#L347-L352 hash=b37199ac43cee36514ab508d763049f8 + ``` + + Create an object for `MockL2ToL2Messenger` and give it some ETH to be able to send transactions. + + ```js file=/public/tutorials/setup-for-testing.sh#L354-L358 hash=7ae6ae561d95426670ea1d0311bc9711 + ``` + + Test that when receiving an interop message, `Greeter` still emits `SetGreeting` correctly. + + ```js file=/public/tutorials/setup-for-testing.sh#L360-L364 hash=e0b49ce1ca70cc58378abcfba8d1d2ca + ``` + + Test that when receiving an interop message, `Greeter` also emits `CrossDomainSetGreeting`. +
+ +## Next steps + +* Write a revolutionary app that uses multiple blockchains within the Superchain. +* Write tests to make sure it works correctly. diff --git a/public/tutorials/setup-for-testing.sh b/public/tutorials/setup-for-testing.sh new file mode 100644 index 000000000..9580c138c --- /dev/null +++ b/public/tutorials/setup-for-testing.sh @@ -0,0 +1,415 @@ +#! /bin/sh + +rm -rf testing +mkdir -p testing/forge +cd testing/forge + +forge init +find . -name 'Counter*' -exec rm {} \; +cd lib +npm install @eth-optimism/contracts-bedrock +cd .. +echo @eth-optimism/=lib/node_modules/@eth-optimism/ >> remappings.txt + +cat > src/Greeter.sol < src/GreetingSender.sol < test/GreetingSender.t.sol < test/Greeter.t.sol < contracts/GreetingSender.sol +find . -name 'Lock*' -exec rm {} \; +npm install @eth-optimism/contracts-bedrock dotenv @eth-optimism/viem + +cat > hardhat.config.js < test/GreetingSender.js < { + + const GreetingSender = await ethers.getContractFactory( + "GreetingSender" + ) + const greetingSender = await GreetingSender.deploy( + targetGreeter, + targetChain + ) + + const messenger = new ethers.Contract( + contracts.l2ToL2CrossDomainMessenger.address, + l2ToL2CrossDomainMessengerAbi, + ethers.provider + ); + + return { greetingSender, messenger }; + } + + it("emits SentMessage with the right arguments", async () => { + const { greetingSender, messenger } = await deployFixture() + + const greeting = "Hello" + + // build the exact calldata the test expects + const iface = new ethers.Interface([ + "function setGreeting(string)", + ]) + const calldata = iface.encodeFunctionData("setGreeting", [ + greeting, + ]) + + await expect(greetingSender.setGreeting(greeting)) + .to.emit(messenger, "SentMessage") + .withArgs( + targetChain, + targetGreeter, + anyValue, + greetingSender.target, + calldata + ) + }) +}) +EOF + +cat > test/Greeter.js < { + const fakeSender = "0x0123456789012345678901234567890123456789"; + const fakeSourceChain = 901; + + async function deployFixture() { + + const MockMessenger = await ethers.getContractFactory("MockL2ToL2Messenger"); + const mock = await MockMessenger.deploy(fakeSender, fakeSourceChain); + + // overwrite predeploy with mock code + await network.provider.send("hardhat_setCode", [ + contracts.l2ToL2CrossDomainMessenger.address, + await ethers.provider.getCode(mock.target), + ]); + + const Greeter = await ethers.getContractFactory("Greeter"); + const greeter = await Greeter.deploy(); + + const messenger = new ethers.Contract( + contracts.l2ToL2CrossDomainMessenger.address, + MockMessenger.interface, + ethers.provider + ); + + return { greeter, messenger, mockMessenger: mock }; + } + + it("emits SetGreeting with the right arguments when called locally", async () => { + const { greeter } = await deployFixture(); + const greeting = "Hello"; + + await expect(greeter.setGreeting(greeting)) + .to.emit(greeter, "SetGreeting") + .withArgs((await ethers.getSigners())[0].address, greeting); + }); + + it("emits SetGreeting and CrossDomainSetGreeting with the right arguments when called remotely", + async () => { + const { greeter } = await deployFixture(); + const greeting = "Hello"; + + const impersonatedMessenger = + await ethers.getImpersonatedSigner(contracts.l2ToL2CrossDomainMessenger.address); + const tx = await (await ethers.getSigners())[0].sendTransaction({ + to: contracts.l2ToL2CrossDomainMessenger.address, + value: ethers.parseEther("1.0") + }); + + await expect( + greeter.connect(impersonatedMessenger).setGreeting(greeting) + ) + .to.emit(greeter, "SetGreeting") + .withArgs(contracts.l2ToL2CrossDomainMessenger.address, greeting); + + await expect( + greeter.connect(impersonatedMessenger).setGreeting(greeting) + ) + .to.emit(greeter, "CrossDomainSetGreeting") + .withArgs(fakeSender, fakeSourceChain, greeting); + }); +}); +EOF + +cat > contracts/MockL2ToL2Messenger.sol <