├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── anvil.messaging.json ├── cairo ├── .gitignore ├── .tool-versions ├── Makefile ├── Scarb.lock ├── Scarb.toml ├── katana-0.json ├── katana-0.key.json ├── katana.env └── src │ ├── contract_msg.cairo │ └── lib.cairo └── solidity ├── .github └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── anvil.env ├── foundry.toml ├── lib └── starknet │ ├── IStarknetMessaging.sol │ ├── IStarknetMessagingEvents.sol │ ├── NamedStorage.sol │ └── StarknetMessaging.sol ├── script ├── ConsumeMessage.s.sol ├── ContractMsg.s.sol ├── LocalTesting.s.sol └── SendMessage.s.sol └── src ├── ContractMsg.sol └── StarknetMessagingLocal.sol /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "solidity/lib/forge-std"] 2 | path = solidity/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 glihm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starknet messaging local development 2 | 3 | This repository aims at giving the detailed steps to locally work 4 | on Starknet messaging with `Anvil` and `Katana`. 5 | 6 | ## Requirements 7 | 8 | Please before start, install: 9 | 10 | - [scarb](https://docs.swmansion.com/scarb/) to build cairo contracts. 11 | - [starkli](https://github.com/xJonathanLEI/starkli) to interact with Katana. 12 | - [katana](https://www.dojoengine.org/en/) to install Katana, that belongs to dojo. 13 | - [foundry](https://book.getfoundry.sh/getting-started/installation) to interact with Anvil. 14 | 15 | If it's your first time cloning the repository, please install forge dependencies as follow: 16 | 17 | ```bash 18 | cd solidity 19 | forge install 20 | ``` 21 | 22 | ## Setup Ethereum contracts 23 | 24 | To setup Ethereum part for local testing, please follow those steps: 25 | 26 | 1. Start Anvil in a new terminal with the command `anvil`. 27 | 28 | 2. In an other terminal, change directory into the solidity folder: 29 | 30 | ```bash 31 | cd solidity 32 | 33 | # Copies the example of anvil configuration file into .env that is loaded by 34 | # foundry automatically. 35 | cp anvil.env .env 36 | 37 | # Ensure all variables are exported for the use of forge commands. 38 | source .env 39 | ``` 40 | 41 | 3. Then, we will deploy the `StarknetMessagingLocal` contract that simulates the work 42 | done by the `StarknetMessaging` core contract on Ethereum. Then we will deploy the `ContractMsg.sol` 43 | to send/receive message. To do so, run the following: 44 | 45 | ```bash 46 | forge script script/LocalTesting.s.sol:LocalSetup --broadcast --rpc-url ${ETH_RPC_URL} 47 | ``` 48 | 49 | 4. Keep this terminal open for later use to send transactions on Anvil. 50 | 51 | ## Setup Starknet contracts 52 | 53 | To setup Starknet contract, please follow those steps: 54 | 55 | 1. Update katana on the 1.0.9 version to use the latest RPC version: 56 | 57 | ```bash 58 | starkliup 59 | dojoup -v 1.0.9 60 | ``` 61 | 62 | 2. Then open a terminal and starts katana by passing the messaging configuration where Anvil contract address and account keys are setup: 63 | 64 | ```bash 65 | katana --messaging anvil.messaging.json 66 | ``` 67 | 68 | Katana will now poll anvil logs exactly as the Starknet sequencer does on the `StarknetMessaging` contract on ethereum. 69 | 70 | 3. In a new terminal, go into cairo folder and use starkli to declare and deploy the contracts: 71 | 72 | ```bash 73 | cd cairo 74 | 75 | # To ensure starkli env variables are setup correctly. 76 | source katana.env 77 | 78 | # Scarb version is defined by `.tool-versions` 79 | # Or use `asdf install scarb 2.6.3` then `asdf local scarb 2.6.3`. 80 | 81 | scarb build 82 | 83 | starkli declare ./target/dev/messaging_tuto_contract_msg.contract_class.json --compiler-version 2.8.5 84 | 85 | starkli deploy 0x0727468c660613faf8ebfbf149f05a9c3016702c362fccb69e9addb6ed1b934c \ 86 | --salt 0x1234 87 | ``` 88 | 89 | 4. Keep this terminal open to later send transactions on Katana. 90 | 91 | ## Interaction between the two chains 92 | 93 | Once you have both dev nodes setup with contracts deployed, we can start interacting with them. 94 | You can use `starkli` and `cast` to send transactions. But for the sake of simplicity, some scripts 95 | are already written to replace `cast` usage. 96 | 97 | ### To send messages L1 -> L2: 98 | 99 | ```bash 100 | # In the terminal that is inside solidity folder you've used to run forge script previously (ensure you've sourced the .env file). 101 | forge script script/SendMessage.s.sol:Value --broadcast --rpc-url ${ETH_RPC_URL} 102 | forge script script/SendMessage.s.sol:Struct --broadcast --rpc-url ${ETH_RPC_URL} 103 | ``` 104 | 105 | You will then see Katana picking up the messages, and executing exactly as Starknet would 106 | do with Ethereum on testnet or mainnet. 107 | 108 | Example here where you can see the details of the message and the event being emitted `ValueReceivedFromL1`. 109 | 110 | ```bash 111 | 2025-01-08T21:47:12.431364Z INFO messaging: L1Handler transaction added to the pool. tx_hash=0x51ab77a5b4fb2188fd270c59f56916bfff4636ca4da8a0a95438e4c2287437c contract_address=0x26558b1ab48a5411f589d8ec66fdef5e6dd9c2f88f7f9274b88997444248aec selector=0x5421de947699472df434466845d68528f221a52fce7ad2934c5dae2e1f1cdc calldata=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512, 0x7b 112 | 2025-01-08T21:47:12.431377Z INFO pool: Transaction received. hash="0x51ab77a5b4fb2188fd270c59f56916bfff4636ca4da8a0a95438e4c2287437c" 113 | 2025-01-08T21:47:12.431398Z INFO messaging: Collected messages from settlement chain. msg_count=1 114 | 2025-01-08T21:47:12.432088Z TRACE executor: Transaction resource usage. usage="steps: 1385 | memory holes: 0 | pedersen_builtin: 12 | range_check_builtin: 19" 115 | ``` 116 | 117 | You can try to change the payload into the scripts to see how the contract on starknet behaves receiveing the message. Try to set both values to 0 for the struct. In the case of the value, you'll see a warning in Katana saying `Invalid value` because the contract is expected `123`. 118 | 119 | ### To send messages L2 -> L1: 120 | 121 | ```bash 122 | # In the terminal that is inside the cairo folder you've used to run starkli commands to declare (ensure you've sourced the katana.env file). 123 | 124 | starkli invoke 0x26558b1ab48a5411f589d8ec66fdef5e6dd9c2f88f7f9274b88997444248aec \ 125 | send_message_value 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 1 126 | 127 | starkli invoke 0x26558b1ab48a5411f589d8ec66fdef5e6dd9c2f88f7f9274b88997444248aec \ 128 | send_message_struct 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 1 2 129 | ``` 130 | 131 | You will then see Katana sending transactions to L1 to register the hashes of the messages, 132 | simulating the work done by the `StarknetMessaging` contract on L1 on testnet or mainnet. 133 | 134 | You've to wait few seconds to see the confirmation of Katana that the messages has been sent to Anvil: 135 | 136 | ```bash 137 | 2025-01-08T21:48:17.978664Z INFO pool: Transaction received. hash="0x6aba5937d0ed0ce06486cc554898306cc7670cc85e82d6f39ee1c67bd0ab885" 138 | 2025-01-08T21:48:17.990643Z TRACE executor: Transaction resource usage. usage="steps: 5913 | memory holes: 53 | ec_op_builtin: 3 | pedersen_builtin: 20 | range_check_builtin: 136" 139 | 2025-01-08T21:48:17.991560Z INFO katana::core::backend: Block mined. block_number=5 tx_count=1 140 | 2025-01-08T21:48:18.449588Z INFO messaging: Collected messages from settlement chain. msg_count=0 141 | 2025-01-08T21:48:18.450702Z INFO messaging: Message sent to settlement layer. hash=0x37857b83ff01d1f42340b94d28c148939c6a050e6c2f25bfc425cf2d760b6553 from_address=0x26558b1ab48a5411f589d8ec66fdef5e6dd9c2f88f7f9274b88997444248aec to_address=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 payload=0x1 142 | 2025-01-08T21:48:18.450738Z INFO messaging: Sent messages to the settlement chain. msg_count=1 143 | 144 | 145 | 2025-01-08T21:48:43.952621Z INFO pool: Transaction received. hash="0x6753e9c03461a3adeb944294bfb76d256934aeb9fc693082aab8c584445dc9a" 146 | 2025-01-08T21:48:43.964609Z TRACE executor: Transaction resource usage. usage="steps: 5937 | memory holes: 53 | ec_op_builtin: 3 | pedersen_builtin: 21 | range_check_builtin: 136" 147 | 2025-01-08T21:48:43.965437Z INFO katana::core::backend: Block mined. block_number=6 tx_count=1 148 | 2025-01-08T21:48:44.467839Z INFO messaging: Collected messages from settlement chain. msg_count=0 149 | 2025-01-08T21:48:44.471201Z INFO messaging: Message sent to settlement layer. hash=0x9a853bfe92bb85e2d377cba8df36ef8af1a1da1c8f8a9c2c966ad737e8f79e8b from_address=0x26558b1ab48a5411f589d8ec66fdef5e6dd9c2f88f7f9274b88997444248aec to_address=0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 payload=0x1, 0x2 150 | 2025-01-08T21:48:44.471214Z INFO messaging: Sent messages to the settlement chain. msg_count=1 151 | ``` 152 | 153 | To then consume the messages, you must send a transaction on Anvil, exactly as you would do 154 | on L1 for testnet or mainnet. 155 | 156 | ```bash 157 | # In the terminal used for solidity / forge stuff. 158 | 159 | # Try to run a first time, it should pass. Try to run a second time, you should have the 160 | # error INVALID_MESSAGE_TO_CONSUME because the message is already consumed. 161 | # Or try to change the payload value or address in the script, to see how the consumption 162 | # of the message is denied. 163 | forge script script/ConsumeMessage.s.sol:Value --broadcast -vvvv --rpc-url ${ETH_RPC_URL} 164 | 165 | # Same here, try to consume a message sent with a struct inside. 166 | forge script script/ConsumeMessage.s.sol:Struct --broadcast -vvvv --rpc-url ${ETH_RPC_URL} 167 | ``` 168 | 169 | And that's it! 170 | 171 | With those examples, you can now try your own messaging contracts, mostly to ensure that your serialization/deserialization 172 | of arguments between solidity and cairo is done correctly. 173 | -------------------------------------------------------------------------------- /anvil.messaging.json: -------------------------------------------------------------------------------- 1 | { 2 | "chain": "ethereum", 3 | "rpc_url": "http://127.0.0.1:8545", 4 | "contract_address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", 5 | "sender_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 6 | "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", 7 | "interval": 2, 8 | "from_block": 0 9 | } 10 | -------------------------------------------------------------------------------- /cairo/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /cairo/.tool-versions: -------------------------------------------------------------------------------- 1 | scarb 2.8.4 2 | -------------------------------------------------------------------------------- /cairo/Makefile: -------------------------------------------------------------------------------- 1 | ## 2 | # Messaging Makefile. 3 | # 4 | # Only for local testing on Katana and Anvil as addresses are pre-computed. 5 | ACCOUNT_FILE=./katana-0.json 6 | ACCOUNT_ADDR=0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca 7 | ACCOUNT_PRIVATE_KEY=0x2bbf4f9fd0bbb2e60b0316c1fe0b76cf7a4d0198bd493ced9b8df2a3a24d68a 8 | 9 | # The address of testing contract on Anvil, it's fixed as Anvil seed is not modified 10 | # from default. 11 | L1_CONTRACT_ADDR=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 12 | 13 | # The deployed address on Katana is also pre-computed. So if you change the contract, 14 | # please consider changing this value too as the contract class will change. 15 | CONTRACT_MSG_ADDR=0x02defe8eeb8e8c1cf59de8ba1a6844e8781d27e2ee20439204757b338f8ae74c 16 | CONTRACT_MSG_CLASS_HASH=0x051a7b3565ab605475512178fc157d743c9a394242ab60207a6932a25e087456 17 | 18 | value = 1 19 | 20 | data = 1 2 21 | 22 | OPTS := --account ${ACCOUNT_FILE} \ 23 | --rpc http://0.0.0.0:5050 \ 24 | --private-key ${ACCOUNT_PRIVATE_KEY} 25 | 26 | # Important to use a salt for deploy to ensure constant address between runs. 27 | setup_for_messaging: 28 | scarb build 29 | starkli declare target/dev/messaging_tuto_contract_msg.contract_class.json ${OPTS} 30 | starkli deploy --salt 0x1234 ${CONTRACT_MSG_CLASS_HASH} ${OPTS} 31 | 32 | send_msg_value_l1: 33 | starkli invoke ${CONTRACT_MSG_ADDR} send_message_value ${L1_CONTRACT_ADDR} $(value) ${OPTS} 34 | 35 | send_msg_struct_l1: 36 | starkli invoke ${CONTRACT_MSG_ADDR} send_message_struct ${L1_CONTRACT_ADDR} $(data) ${OPTS} 37 | -------------------------------------------------------------------------------- /cairo/Scarb.lock: -------------------------------------------------------------------------------- 1 | # Code generated by scarb DO NOT EDIT. 2 | version = 1 3 | 4 | [[package]] 5 | name = "messaging_tuto" 6 | version = "0.1.0" 7 | -------------------------------------------------------------------------------- /cairo/Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "messaging_tuto" 3 | version = "0.1.0" 4 | edition = "2023_11" 5 | 6 | [dependencies] 7 | starknet = "=2.8.4" 8 | 9 | [[target.starknet-contract]] 10 | sierra = true 11 | 12 | [lib] 13 | -------------------------------------------------------------------------------- /cairo/katana-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "variant": { 4 | "type": "open_zeppelin", 5 | "version": 1, 6 | "public_key": "0x640466ebd2ce505209d3e5c4494b4276ed8f1cde764d757eb48831961f7cdea", 7 | "legacy": false 8 | }, 9 | "deployment": { 10 | "status": "deployed", 11 | "class_hash": "0x5400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c", 12 | "address": "0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cairo/katana-0.key.json: -------------------------------------------------------------------------------- 1 | {"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"c83aab1aa2b20efba1bf0d32ae17edbe"},"ciphertext":"67be589b6760b427294e90127836b470717bdcda9666326c3f7bfc5142a72462","kdf":"scrypt","kdfparams":{"dklen":32,"n":8192,"p":1,"r":8,"salt":"f164f9a2af41b736e998171fc934585684c61ce6ed5164e623ac5656eb541314"},"mac":"eadf0479542375cea93d640167f24fecb05a51f1985992c5e70c0fbf5e1baf2a"},"id":"3719e55d-f8ec-42e0-981b-1fe6905599c6","version":3} -------------------------------------------------------------------------------- /cairo/katana.env: -------------------------------------------------------------------------------- 1 | export STARKNET_ACCOUNT=katana-0 2 | export STARKNET_RPC=http://0.0.0.0:5050 3 | -------------------------------------------------------------------------------- /cairo/src/contract_msg.cairo: -------------------------------------------------------------------------------- 1 | //! A simple contract that sends and receives messages from/to 2 | //! the L1 (Ethereum). 3 | //! 4 | //! The reception of the messages is done using the `l1_handler` functions. 5 | //! The messages are sent by using the `send_message_to_l1_syscall` syscall. 6 | 7 | /// A custom struct, which is already 8 | /// serializable as `felt252` is serializable. 9 | #[derive(Drop, Serde)] 10 | struct MyData { 11 | a: felt252, 12 | b: felt252, 13 | } 14 | 15 | #[starknet::interface] 16 | trait IContractL1 { 17 | /// Sends a message to L1 contract with a single felt252 value. 18 | /// 19 | /// # Arguments 20 | /// 21 | /// * `to_address` - Contract address on L1. 22 | /// * `value` - Value to be sent in the payload. 23 | fn send_message_value(ref self: T, to_address: starknet::EthAddress, value: felt252); 24 | 25 | /// Sends a message to L1 contract with a serialized struct. 26 | /// To send a struct in a payload of a message, you only have to ensure that 27 | /// your structure is serializable implementing the `Serde` traits. Which 28 | /// is automatically done if your structure only contains already serializable members. 29 | /// 30 | /// # Arguments 31 | /// 32 | /// * `to_address` - Contract address on L1. 33 | /// * `data` - Data to be sent in the payload. 34 | fn send_message_struct(ref self: T, to_address: starknet::EthAddress, data: MyData); 35 | } 36 | 37 | #[starknet::contract] 38 | mod contract_msg { 39 | use super::{IContractL1, MyData}; 40 | use starknet::{EthAddress, SyscallResultTrait}; 41 | use core::num::traits::Zero; 42 | use starknet::syscalls::send_message_to_l1_syscall; 43 | 44 | #[storage] 45 | struct Storage {} 46 | 47 | #[event] 48 | #[derive(Drop, starknet::Event)] 49 | enum Event { 50 | ValueReceivedFromL1: ValueReceived, 51 | StructReceivedFromL1: StructReceived, 52 | } 53 | 54 | #[derive(Drop, starknet::Event)] 55 | struct ValueReceived { 56 | #[key] 57 | l1_address: felt252, 58 | value: felt252, 59 | } 60 | 61 | #[derive(Drop, starknet::Event)] 62 | struct StructReceived { 63 | #[key] 64 | l1_address: felt252, 65 | data_a: felt252, 66 | data_b: felt252, 67 | } 68 | 69 | /// Handles a message received from L1. 70 | /// 71 | /// Only functions that are annotated with `#[l1_handler]` can 72 | /// receive message from L1, as the sequencer will execute 73 | /// the contract code using a specific transaction type (`L1HandlerTransaction`) 74 | /// that can only see endpoints annotated as such. 75 | /// 76 | /// # Arguments 77 | /// 78 | /// * `from_address` - The L1 contract sending the message. 79 | /// * `value` - Expected value in the payload. 80 | /// 81 | /// In production, you must always check if the `from_address` is 82 | /// a contract you allowed to send messages, as any contract from L1 83 | /// can send message to any contract on L2 and vice-versa. 84 | /// 85 | /// In this example, the payload is expected to be a single felt value. But it can be any 86 | /// deserializable struct written in cairo. 87 | #[l1_handler] 88 | fn msg_handler_value(ref self: ContractState, from_address: felt252, value: felt252) { 89 | // assert(from_address == ...); 90 | 91 | assert(value == 123, 'Invalid value'); 92 | 93 | self.emit(ValueReceived { l1_address: from_address, value, }); 94 | } 95 | 96 | /// Handles a message received from L1. 97 | /// In this example, the handler is expecting the data members to both be greater than 0. 98 | /// 99 | /// # Arguments 100 | /// 101 | /// * `from_address` - The L1 contract sending the message. 102 | /// * `data` - Expected data in the payload (automatically deserialized by cairo). 103 | #[l1_handler] 104 | fn msg_handler_struct(ref self: ContractState, from_address: felt252, data: MyData) { 105 | // assert(from_address == ...); 106 | 107 | assert(!data.a.is_zero(), 'data.a is invalid'); 108 | assert(!data.b.is_zero(), 'data.b is invalid'); 109 | 110 | self.emit(StructReceived { l1_address: from_address, data_a: data.a, data_b: data.b, }); 111 | } 112 | 113 | #[abi(embed_v0)] 114 | impl ContractL1Impl of IContractL1 { 115 | fn send_message_value(ref self: ContractState, to_address: EthAddress, value: felt252) { 116 | // Note here, we "serialize" the felt252 value, as the payload must be 117 | // a `Span`. 118 | send_message_to_l1_syscall(to_address.into(), array![value].span()).unwrap_syscall(); 119 | } 120 | 121 | fn send_message_struct(ref self: ContractState, to_address: EthAddress, data: MyData) { 122 | // Explicit serialization of our structure `MyData`. 123 | let mut buf: Array = array![]; 124 | data.serialize(ref buf); 125 | send_message_to_l1_syscall(to_address.into(), buf.span()).unwrap_syscall(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /cairo/src/lib.cairo: -------------------------------------------------------------------------------- 1 | mod contract_msg; 2 | -------------------------------------------------------------------------------- /solidity/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /solidity/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | /broadcast 7 | 8 | # Docs 9 | docs/ 10 | 11 | # Dotenv file 12 | .env 13 | 14 | # logs 15 | logs 16 | -------------------------------------------------------------------------------- /solidity/README.md: -------------------------------------------------------------------------------- 1 | ## Foundry 2 | 3 | **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** 4 | 5 | Foundry consists of: 6 | 7 | - **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). 8 | - **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. 9 | - **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. 10 | - **Chisel**: Fast, utilitarian, and verbose solidity REPL. 11 | 12 | ## Documentation 13 | 14 | https://book.getfoundry.sh/ 15 | 16 | ## Usage 17 | 18 | ### Build 19 | 20 | ```shell 21 | $ forge build 22 | ``` 23 | 24 | ### Test 25 | 26 | ```shell 27 | $ forge test 28 | ``` 29 | 30 | ### Format 31 | 32 | ```shell 33 | $ forge fmt 34 | ``` 35 | 36 | ### Gas Snapshots 37 | 38 | ```shell 39 | $ forge snapshot 40 | ``` 41 | 42 | ### Anvil 43 | 44 | ```shell 45 | $ anvil 46 | ``` 47 | 48 | ### Deploy 49 | 50 | ```shell 51 | $ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key 52 | ``` 53 | 54 | ### Cast 55 | 56 | ```shell 57 | $ cast 58 | ``` 59 | 60 | ### Help 61 | 62 | ```shell 63 | $ forge --help 64 | $ anvil --help 65 | $ cast --help 66 | ``` 67 | -------------------------------------------------------------------------------- /solidity/anvil.env: -------------------------------------------------------------------------------- 1 | # General config. 2 | ETH_RPC_URL=http://127.0.0.1:8545 3 | ETHERSCAN_API_KEY=0x1 4 | 5 | # Account related variables (EOA account). 6 | ACCOUNT_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 7 | ACCOUNT_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 8 | 9 | # Contracts on L1. 10 | CONTRACT_MSG_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 11 | SN_MESSAGING_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 12 | 13 | # Contracts on L2. 14 | L2_CONTRACT_ADDRESS=0x026558b1ab48a5411f589d8ec66fdef5e6dd9c2f88f7f9274b88997444248aec 15 | # The "msg_handler_value" selector. 16 | L2_SELECTOR_VALUE=0x005421de947699472df434466845d68528f221a52fce7ad2934c5dae2e1f1cdc 17 | # The "msg_handler_struct" selector. 18 | L2_SELECTOR_STRUCT=0x00f1149cade9d692862ad41df96b108aa2c20af34f640457e781d166c98dc6b0 19 | # Account contract address that send messages on L2. 20 | L2_ACCOUNT=0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca 21 | -------------------------------------------------------------------------------- /solidity/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | fs_permissions = [{ access = "read-write", path = "./logs"}] 6 | 7 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 8 | -------------------------------------------------------------------------------- /solidity/lib/starknet/IStarknetMessaging.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | import "./IStarknetMessagingEvents.sol"; 20 | 21 | interface IStarknetMessaging is IStarknetMessagingEvents { 22 | /** 23 | Returns the max fee (in Wei) that StarkNet will accept per single message. 24 | */ 25 | function getMaxL1MsgFee() external pure returns (uint256); 26 | 27 | /** 28 | Sends a message to an L2 contract. 29 | This function is payable, the payed amount is the message fee. 30 | 31 | Returns the hash of the message and the nonce of the message. 32 | */ 33 | function sendMessageToL2( 34 | uint256 toAddress, 35 | uint256 selector, 36 | uint256[] calldata payload 37 | ) external payable returns (bytes32, uint256); 38 | 39 | /** 40 | Consumes a message that was sent from an L2 contract. 41 | 42 | Returns the hash of the message. 43 | */ 44 | function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) 45 | external 46 | returns (bytes32); 47 | 48 | /** 49 | Starts the cancellation of an L1 to L2 message. 50 | A message can be canceled messageCancellationDelay() seconds after this function is called. 51 | 52 | Note: This function may only be called for a message that is currently pending and the caller 53 | must be the sender of the that message. 54 | */ 55 | function startL1ToL2MessageCancellation( 56 | uint256 toAddress, 57 | uint256 selector, 58 | uint256[] calldata payload, 59 | uint256 nonce 60 | ) external returns (bytes32); 61 | 62 | /** 63 | Cancels an L1 to L2 message, this function should be called at least 64 | messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation(). 65 | A message may only be cancelled by its sender. 66 | If the message is missing, the call will revert. 67 | 68 | Note that the message fee is not refunded. 69 | */ 70 | function cancelL1ToL2Message( 71 | uint256 toAddress, 72 | uint256 selector, 73 | uint256[] calldata payload, 74 | uint256 nonce 75 | ) external returns (bytes32); 76 | } 77 | -------------------------------------------------------------------------------- /solidity/lib/starknet/IStarknetMessagingEvents.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | interface IStarknetMessagingEvents { 20 | // This event needs to be compatible with the one defined in Output.sol. 21 | event LogMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload); 22 | 23 | // An event that is raised when a message is sent from L1 to L2. 24 | event LogMessageToL2( 25 | address indexed fromAddress, 26 | uint256 indexed toAddress, 27 | uint256 indexed selector, 28 | uint256[] payload, 29 | uint256 nonce, 30 | uint256 fee 31 | ); 32 | 33 | // An event that is raised when a message from L2 to L1 is consumed. 34 | event ConsumedMessageToL1( 35 | uint256 indexed fromAddress, 36 | address indexed toAddress, 37 | uint256[] payload 38 | ); 39 | 40 | // An event that is raised when a message from L1 to L2 is consumed. 41 | event ConsumedMessageToL2( 42 | address indexed fromAddress, 43 | uint256 indexed toAddress, 44 | uint256 indexed selector, 45 | uint256[] payload, 46 | uint256 nonce 47 | ); 48 | 49 | // An event that is raised when a message from L1 to L2 Cancellation is started. 50 | event MessageToL2CancellationStarted( 51 | address indexed fromAddress, 52 | uint256 indexed toAddress, 53 | uint256 indexed selector, 54 | uint256[] payload, 55 | uint256 nonce 56 | ); 57 | 58 | // An event that is raised when a message from L1 to L2 is canceled. 59 | event MessageToL2Canceled( 60 | address indexed fromAddress, 61 | uint256 indexed toAddress, 62 | uint256 indexed selector, 63 | uint256[] payload, 64 | uint256 nonce 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /solidity/lib/starknet/NamedStorage.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | /* 20 | Library to provide basic storage, in storage location out of the low linear address space. 21 | 22 | New types of storage variables should be added here upon need. 23 | */ 24 | library NamedStorage { 25 | function bytes32ToUint256Mapping(string memory tag_) 26 | internal 27 | pure 28 | returns (mapping(bytes32 => uint256) storage randomVariable) 29 | { 30 | bytes32 location = keccak256(abi.encodePacked(tag_)); 31 | assembly { 32 | randomVariable.slot := location 33 | } 34 | } 35 | 36 | function bytes32ToAddressMapping(string memory tag_) 37 | internal 38 | pure 39 | returns (mapping(bytes32 => address) storage randomVariable) 40 | { 41 | bytes32 location = keccak256(abi.encodePacked(tag_)); 42 | assembly { 43 | randomVariable.slot := location 44 | } 45 | } 46 | 47 | function uintToAddressMapping(string memory tag_) 48 | internal 49 | pure 50 | returns (mapping(uint256 => address) storage randomVariable) 51 | { 52 | bytes32 location = keccak256(abi.encodePacked(tag_)); 53 | assembly { 54 | randomVariable.slot := location 55 | } 56 | } 57 | 58 | function addressToBoolMapping(string memory tag_) 59 | internal 60 | pure 61 | returns (mapping(address => bool) storage randomVariable) 62 | { 63 | bytes32 location = keccak256(abi.encodePacked(tag_)); 64 | assembly { 65 | randomVariable.slot := location 66 | } 67 | } 68 | 69 | function getUintValue(string memory tag_) internal view returns (uint256 retVal) { 70 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 71 | assembly { 72 | retVal := sload(slot) 73 | } 74 | } 75 | 76 | function setUintValue(string memory tag_, uint256 value) internal { 77 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 78 | assembly { 79 | sstore(slot, value) 80 | } 81 | } 82 | 83 | function setUintValueOnce(string memory tag_, uint256 value) internal { 84 | require(getUintValue(tag_) == 0, "ALREADY_SET"); 85 | setUintValue(tag_, value); 86 | } 87 | 88 | function getAddressValue(string memory tag_) internal view returns (address retVal) { 89 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 90 | assembly { 91 | retVal := sload(slot) 92 | } 93 | } 94 | 95 | function setAddressValue(string memory tag_, address value) internal { 96 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 97 | assembly { 98 | sstore(slot, value) 99 | } 100 | } 101 | 102 | function setAddressValueOnce(string memory tag_, address value) internal { 103 | require(getAddressValue(tag_) == address(0x0), "ALREADY_SET"); 104 | setAddressValue(tag_, value); 105 | } 106 | 107 | function getBoolValue(string memory tag_) internal view returns (bool retVal) { 108 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 109 | assembly { 110 | retVal := sload(slot) 111 | } 112 | } 113 | 114 | function setBoolValue(string memory tag_, bool value) internal { 115 | bytes32 slot = keccak256(abi.encodePacked(tag_)); 116 | assembly { 117 | sstore(slot, value) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /solidity/lib/starknet/StarknetMessaging.sol: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2022 StarkWare Industries Ltd. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.starkware.co/open-source-license/ 9 | 10 | Unless required by applicable law or agreed to in writing, 11 | software distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions 14 | and limitations under the License. 15 | */ 16 | // SPDX-License-Identifier: Apache-2.0. 17 | pragma solidity ^0.8.0; 18 | 19 | import "./IStarknetMessaging.sol"; 20 | import "./NamedStorage.sol"; 21 | 22 | /** 23 | Implements sending messages to L2 by adding them to a pipe and consuming messages from L2 by 24 | removing them from a different pipe. A deriving contract can handle the former pipe and add items 25 | to the latter pipe while interacting with L2. 26 | */ 27 | contract StarknetMessaging is IStarknetMessaging { 28 | /* 29 | Random slot storage elements and accessors. 30 | */ 31 | string constant L1L2_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L1TOL2_MAPPPING_V2"; 32 | string constant L2L1_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L2TOL1_MAPPPING"; 33 | 34 | string constant L1L2_MESSAGE_NONCE_TAG = "STARKNET_1.0_MSGING_L1TOL2_NONCE"; 35 | 36 | string constant L1L2_MESSAGE_CANCELLATION_MAP_TAG = ( 37 | "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_MAPPPING" 38 | ); 39 | 40 | string constant L1L2_MESSAGE_CANCELLATION_DELAY_TAG = ( 41 | "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_DELAY" 42 | ); 43 | 44 | uint256 constant MAX_L1_MSG_FEE = 1 ether; 45 | 46 | function getMaxL1MsgFee() public pure override returns (uint256) { 47 | return MAX_L1_MSG_FEE; 48 | } 49 | 50 | /** 51 | Returns the msg_fee + 1 for the message with the given 'msgHash', 52 | or 0 if no message with such a hash is pending. 53 | */ 54 | function l1ToL2Messages(bytes32 msgHash) external view returns (uint256) { 55 | return l1ToL2Messages()[msgHash]; 56 | } 57 | 58 | function l2ToL1Messages(bytes32 msgHash) external view returns (uint256) { 59 | return l2ToL1Messages()[msgHash]; 60 | } 61 | 62 | function l1ToL2Messages() internal pure returns (mapping(bytes32 => uint256) storage) { 63 | return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_MAP_TAG); 64 | } 65 | 66 | function l2ToL1Messages() internal pure returns (mapping(bytes32 => uint256) storage) { 67 | return NamedStorage.bytes32ToUint256Mapping(L2L1_MESSAGE_MAP_TAG); 68 | } 69 | 70 | function l1ToL2MessageNonce() public view returns (uint256) { 71 | return NamedStorage.getUintValue(L1L2_MESSAGE_NONCE_TAG); 72 | } 73 | 74 | function messageCancellationDelay() public view returns (uint256) { 75 | return NamedStorage.getUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG); 76 | } 77 | 78 | function messageCancellationDelay(uint256 delayInSeconds) internal { 79 | NamedStorage.setUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG, delayInSeconds); 80 | } 81 | 82 | /** 83 | Returns the timestamp at the time cancelL1ToL2Message was called with a message 84 | matching 'msgHash'. 85 | 86 | The function returns 0 if cancelL1ToL2Message was never called. 87 | */ 88 | function l1ToL2MessageCancellations(bytes32 msgHash) external view returns (uint256) { 89 | return l1ToL2MessageCancellations()[msgHash]; 90 | } 91 | 92 | function l1ToL2MessageCancellations() 93 | internal 94 | pure 95 | returns (mapping(bytes32 => uint256) storage) 96 | { 97 | return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_CANCELLATION_MAP_TAG); 98 | } 99 | 100 | /** 101 | Returns the hash of an L1 -> L2 message from msg.sender. 102 | */ 103 | function getL1ToL2MsgHash( 104 | uint256 toAddress, 105 | uint256 selector, 106 | uint256[] calldata payload, 107 | uint256 nonce 108 | ) internal view returns (bytes32) { 109 | return 110 | keccak256( 111 | abi.encodePacked( 112 | // MODIFIED HERE: adding uint160 for casting. 113 | uint256(uint160(msg.sender)), 114 | toAddress, 115 | nonce, 116 | selector, 117 | payload.length, 118 | payload 119 | ) 120 | ); 121 | } 122 | 123 | /** 124 | Sends a message to an L2 contract. 125 | */ 126 | function sendMessageToL2( 127 | uint256 toAddress, 128 | uint256 selector, 129 | uint256[] calldata payload 130 | ) external payable override returns (bytes32, uint256) { 131 | require(msg.value > 0, "L1_MSG_FEE_MUST_BE_GREATER_THAN_0"); 132 | require(msg.value <= getMaxL1MsgFee(), "MAX_L1_MSG_FEE_EXCEEDED"); 133 | uint256 nonce = l1ToL2MessageNonce(); 134 | NamedStorage.setUintValue(L1L2_MESSAGE_NONCE_TAG, nonce + 1); 135 | emit LogMessageToL2(msg.sender, toAddress, selector, payload, nonce, msg.value); 136 | bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); 137 | // Note that the inclusion of the unique nonce in the message hash implies that 138 | // l1ToL2Messages()[msgHash] was not accessed before. 139 | l1ToL2Messages()[msgHash] = msg.value + 1; 140 | return (msgHash, nonce); 141 | } 142 | 143 | /** 144 | Consumes a message that was sent from an L2 contract. 145 | 146 | Returns the hash of the message. 147 | */ 148 | function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) 149 | external 150 | override 151 | returns (bytes32) 152 | { 153 | bytes32 msgHash = keccak256( 154 | // MODIFIED HERE: adding uint160 for casting. 155 | abi.encodePacked(fromAddress, uint256(uint160(msg.sender)), payload.length, payload) 156 | ); 157 | 158 | require(l2ToL1Messages()[msgHash] > 0, "INVALID_MESSAGE_TO_CONSUME"); 159 | emit ConsumedMessageToL1(fromAddress, msg.sender, payload); 160 | l2ToL1Messages()[msgHash] -= 1; 161 | return msgHash; 162 | } 163 | 164 | function startL1ToL2MessageCancellation( 165 | uint256 toAddress, 166 | uint256 selector, 167 | uint256[] calldata payload, 168 | uint256 nonce 169 | ) external override returns (bytes32) { 170 | emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce); 171 | bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); 172 | uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; 173 | require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL"); 174 | l1ToL2MessageCancellations()[msgHash] = block.timestamp; 175 | return msgHash; 176 | } 177 | 178 | function cancelL1ToL2Message( 179 | uint256 toAddress, 180 | uint256 selector, 181 | uint256[] calldata payload, 182 | uint256 nonce 183 | ) external override returns (bytes32) { 184 | emit MessageToL2Canceled(msg.sender, toAddress, selector, payload, nonce); 185 | // Note that the message hash depends on msg.sender, which prevents one contract from 186 | // cancelling another contract's message. 187 | // Trying to do so will result in NO_MESSAGE_TO_CANCEL. 188 | bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); 189 | uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; 190 | require(msgFeePlusOne != 0, "NO_MESSAGE_TO_CANCEL"); 191 | 192 | uint256 requestTime = l1ToL2MessageCancellations()[msgHash]; 193 | require(requestTime != 0, "MESSAGE_CANCELLATION_NOT_REQUESTED"); 194 | 195 | uint256 cancelAllowedTime = requestTime + messageCancellationDelay(); 196 | require(cancelAllowedTime >= requestTime, "CANCEL_ALLOWED_TIME_OVERFLOW"); 197 | require(block.timestamp >= cancelAllowedTime, "MESSAGE_CANCELLATION_NOT_ALLOWED_YET"); 198 | 199 | l1ToL2Messages()[msgHash] = 0; 200 | return (msgHash); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /solidity/script/ConsumeMessage.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import "src/ContractMsg.sol"; 7 | 8 | /** 9 | * @notice A simple script to consume a message from Starknet. 10 | */ 11 | contract Value is Script { 12 | uint256 _privateKey; 13 | address _contractMsgAddress; 14 | uint256 _l2Contract; 15 | 16 | function setUp() public { 17 | _privateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); 18 | _contractMsgAddress = vm.envAddress("CONTRACT_MSG_ADDRESS"); 19 | _l2Contract = vm.envUint("L2_CONTRACT_ADDRESS"); 20 | } 21 | 22 | function run() public{ 23 | vm.startBroadcast(_privateKey); 24 | 25 | // This value must match what was sent from starknet. 26 | // In our example, we have sent the value 1 with starkli. 27 | uint256[] memory payload = new uint256[](1); 28 | payload[0] = 1; 29 | 30 | // The address must be the contract's address that has sent the message. 31 | ContractMsg(_contractMsgAddress).consumeMessageValue( 32 | _l2Contract, 33 | payload); 34 | 35 | vm.stopBroadcast(); 36 | } 37 | } 38 | 39 | /** 40 | * @notice A simple script to consume a message from Starknet. 41 | */ 42 | contract Struct is Script { 43 | uint256 _privateKey; 44 | address _contractMsgAddress; 45 | uint256 _l2Contract; 46 | 47 | function setUp() public { 48 | _privateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); 49 | _contractMsgAddress = vm.envAddress("CONTRACT_MSG_ADDRESS"); 50 | _l2Contract = vm.envUint("L2_CONTRACT_ADDRESS"); 51 | } 52 | 53 | function run() public{ 54 | vm.startBroadcast(_privateKey); 55 | 56 | // In the example, we've sent a message with serialize MyData. 57 | uint256[] memory payload = new uint256[](2); 58 | payload[0] = 1; 59 | payload[1] = 2; 60 | 61 | ContractMsg(_contractMsgAddress).consumeMessageStruct( 62 | _l2Contract, 63 | payload); 64 | 65 | vm.stopBroadcast(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /solidity/script/ContractMsg.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import "src/ContractMsg.sol"; 7 | 8 | /** 9 | Deploys only the ContractMsg. 10 | */ 11 | contract Deploy is Script { 12 | function setUp() public {} 13 | 14 | function run() public{ 15 | uint256 deployerPrivateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); 16 | address snMessagingAddress = vm.envAddress("SN_MESSAGING_ADDRESS"); 17 | 18 | string memory json = "contract_msg_deploy"; 19 | 20 | vm.startBroadcast(deployerPrivateKey); 21 | 22 | address contractMsg = address(new ContractMsg(snMessagingAddress)); 23 | vm.serializeString(json, "contractMsg_address", vm.toString(contractMsg)); 24 | 25 | vm.stopBroadcast(); 26 | 27 | string memory data = vm.serializeBool(json, "success", true); 28 | 29 | string memory localLogs = "./logs/"; 30 | vm.createDir(localLogs, true); 31 | vm.writeJson(data, string.concat(localLogs, "contract_msg_deploy.json")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /solidity/script/LocalTesting.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import "src/ContractMsg.sol"; 7 | import "src/StarknetMessagingLocal.sol"; 8 | 9 | /** 10 | Deploys the ContractMsg and StarknetMessagingLocal contracts. 11 | Very handy to quickly setup Anvil to debug. 12 | */ 13 | contract LocalSetup is Script { 14 | function setUp() public {} 15 | 16 | function run() public{ 17 | uint256 deployerPrivateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); 18 | 19 | string memory json = "local_testing"; 20 | 21 | vm.startBroadcast(deployerPrivateKey); 22 | 23 | address snLocalAddress = address(new StarknetMessagingLocal()); 24 | vm.serializeString(json, "snMessaging_address", vm.toString(snLocalAddress)); 25 | 26 | address contractMsg = address(new ContractMsg(snLocalAddress)); 27 | vm.serializeString(json, "contractMsg_address", vm.toString(contractMsg)); 28 | 29 | vm.stopBroadcast(); 30 | 31 | string memory data = vm.serializeBool(json, "success", true); 32 | 33 | string memory localLogs = "./logs/"; 34 | vm.createDir(localLogs, true); 35 | vm.writeJson(data, string.concat(localLogs, "local_setup.json")); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /solidity/script/SendMessage.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | import "src/ContractMsg.sol"; 7 | 8 | /** 9 | * @notice A simple script to send a message to Starknet. 10 | */ 11 | contract Value is Script { 12 | uint256 _privateKey; 13 | address _contractMsgAddress; 14 | address _starknetMessagingAddress; 15 | uint256 _l2ContractAddress; 16 | uint256 _l2Selector; 17 | 18 | function setUp() public { 19 | _privateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); 20 | _contractMsgAddress = vm.envAddress("CONTRACT_MSG_ADDRESS"); 21 | _starknetMessagingAddress = vm.envAddress("SN_MESSAGING_ADDRESS"); 22 | _l2ContractAddress = vm.envUint("L2_CONTRACT_ADDRESS"); 23 | _l2Selector = vm.envUint("L2_SELECTOR_VALUE"); 24 | } 25 | 26 | function run() public{ 27 | vm.startBroadcast(_privateKey); 28 | 29 | uint256[] memory payload = new uint256[](1); 30 | payload[0] = 123; 31 | 32 | // Remember that there is a cost of at least 20k wei to send a message. 33 | // Let's send 30k here to ensure that we pay enough for our payload serialization. 34 | ContractMsg(_contractMsgAddress).sendMessage{value: 30000}( 35 | _l2ContractAddress, 36 | _l2Selector, 37 | payload); 38 | 39 | vm.stopBroadcast(); 40 | } 41 | } 42 | 43 | /** 44 | * @notice A simple script to send a message to Starknet. 45 | */ 46 | contract Struct is Script { 47 | uint256 _privateKey; 48 | address _contractMsgAddress; 49 | address _starknetMessagingAddress; 50 | uint256 _l2ContractAddress; 51 | uint256 _l2Selector; 52 | 53 | function setUp() public { 54 | _privateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); 55 | _contractMsgAddress = vm.envAddress("CONTRACT_MSG_ADDRESS"); 56 | _starknetMessagingAddress = vm.envAddress("SN_MESSAGING_ADDRESS"); 57 | _l2ContractAddress = vm.envUint("L2_CONTRACT_ADDRESS"); 58 | _l2Selector = vm.envUint("L2_SELECTOR_STRUCT"); 59 | } 60 | 61 | function run() public{ 62 | vm.startBroadcast(_privateKey); 63 | 64 | uint256[] memory payload = new uint256[](2); 65 | payload[0] = 1; 66 | payload[1] = 2; 67 | 68 | // Remember that there is a cost of at least 20k wei to send a message. 69 | // Let's send 30k here to ensure that we pay enough for our payload serialization. 70 | ContractMsg(_contractMsgAddress).sendMessage{value: 30000}( 71 | _l2ContractAddress, 72 | _l2Selector, 73 | payload); 74 | 75 | vm.stopBroadcast(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /solidity/src/ContractMsg.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "starknet/IStarknetMessaging.sol"; 6 | 7 | // Define some custom error as an example. 8 | // It saves a lot's of space to use those custom error instead of strings. 9 | error InvalidPayload(); 10 | 11 | /** 12 | @title Test contract to receive / send messages to starknet. 13 | */ 14 | contract ContractMsg { 15 | 16 | // 17 | IStarknetMessaging private _snMessaging; 18 | 19 | /** 20 | @notice Constructor. 21 | 22 | @param snMessaging The address of Starknet Core contract, responsible 23 | or messaging. 24 | */ 25 | constructor(address snMessaging) { 26 | _snMessaging = IStarknetMessaging(snMessaging); 27 | } 28 | 29 | /** 30 | @notice Sends a message to Starknet contract. 31 | 32 | @param contractAddress The contract's address on starknet. 33 | @param selector The l1_handler function of the contract to call. 34 | @param payload The serialized data to be sent. 35 | 36 | @dev Consider that Cairo only understands felts252. 37 | So the serialization on solidity must be adjusted. For instance, a uint256 38 | must be split in two uint256 with low and high part to be understood by Cairo. 39 | */ 40 | function sendMessage( 41 | uint256 contractAddress, 42 | uint256 selector, 43 | uint256[] memory payload 44 | ) 45 | external 46 | payable 47 | { 48 | _snMessaging.sendMessageToL2{value: msg.value}( 49 | contractAddress, 50 | selector, 51 | payload 52 | ); 53 | } 54 | 55 | 56 | /** 57 | @notice A simple function that sends a message with a pre-determined payload. 58 | */ 59 | function sendMessageValue( 60 | uint256 contractAddress, 61 | uint256 selector, 62 | uint256 value 63 | ) 64 | external 65 | payable 66 | { 67 | uint256[] memory payload = new uint256[](1); 68 | payload[0] = value; 69 | 70 | _snMessaging.sendMessageToL2{value: msg.value}( 71 | contractAddress, 72 | selector, 73 | payload 74 | ); 75 | } 76 | 77 | /** 78 | @notice Manually consumes a message that was received from L2. 79 | 80 | @param fromAddress L2 contract (account) that has sent the message. 81 | @param payload Payload of the message used to verify the hash. 82 | 83 | @dev A message "receive" means that the message hash is registered as consumable. 84 | One must provide the message content, to let Starknet Core contract verify the hash 85 | and validate the message content before being consumed. 86 | */ 87 | function consumeMessage( 88 | uint256 fromAddress, 89 | uint256[] calldata payload 90 | ) 91 | external 92 | { 93 | // Will revert if the message is not consumable. 94 | _snMessaging.consumeMessageFromL2(fromAddress, payload); 95 | 96 | // The previous call returns the message hash (bytes32) 97 | // that can be used if necessary. 98 | 99 | // You can use the payload to do stuff here as you now know that the message is 100 | // valid and safe to process. 101 | // Remember that the payload contains cairo serialized data. So you must 102 | // deserialize the payload depending on the data it contains. 103 | } 104 | 105 | /** 106 | @notice Example of consuming a value received from L2. 107 | */ 108 | function consumeMessageValue( 109 | uint256 fromAddress, 110 | uint256[] calldata payload 111 | ) 112 | external 113 | { 114 | _snMessaging.consumeMessageFromL2(fromAddress, payload); 115 | 116 | // We expect the payload to contain only a felt252 value (which is a uint256 in solidity). 117 | if (payload.length != 1) { 118 | revert InvalidPayload(); 119 | } 120 | 121 | uint256 value = payload[0]; 122 | require(value > 0, "Invalid value"); 123 | } 124 | 125 | /** 126 | @notice Example of consuming a serialized struct from L2. 127 | */ 128 | function consumeMessageStruct( 129 | uint256 fromAddress, 130 | uint256[] calldata payload 131 | ) 132 | external 133 | { 134 | _snMessaging.consumeMessageFromL2(fromAddress, payload); 135 | 136 | // We expect the payload to contain field `a` and `b` from `MyData`. 137 | if (payload.length != 2) { 138 | revert InvalidPayload(); 139 | } 140 | 141 | uint256 a = payload[0]; 142 | uint256 b = payload[1]; 143 | require(a > 0 && b > 0, "Invalid value"); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /solidity/src/StarknetMessagingLocal.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0. 2 | pragma solidity ^0.8.0; 3 | 4 | import "starknet/StarknetMessaging.sol"; 5 | 6 | /** 7 | @notice Interface related to local messaging for Starknet. 8 | */ 9 | interface IStarknetMessagingLocal { 10 | function addMessageHashesFromL2( 11 | uint256[] calldata msgHashes 12 | ) 13 | external 14 | payable; 15 | } 16 | 17 | /** 18 | @title A superset of StarknetMessaging to support 19 | local development by adding a way to directly register 20 | a message hash ready to be consumed, without waiting the block 21 | to be verified. 22 | 23 | @dev The idea is that, to not wait on the block to be proven, 24 | this messaging contract can receive directly a hash of a message 25 | to be considered as `received`. This message can then be consumed normally. 26 | 27 | DISCLAIMER: 28 | The purpose of this contract is for local development only. 29 | */ 30 | contract StarknetMessagingLocal is StarknetMessaging, IStarknetMessagingLocal { 31 | 32 | /** 33 | @notice Hashes were added. 34 | */ 35 | event MessageHashesAddedFromL2( 36 | uint256[] hashes 37 | ); 38 | 39 | /** 40 | @notice Adds the hashes of messages from L2. 41 | 42 | @param msgHashes Hashes to register as consumable. 43 | */ 44 | function addMessageHashesFromL2( 45 | uint256[] calldata msgHashes 46 | ) 47 | external 48 | payable 49 | { 50 | for (uint256 i = 0; i < msgHashes.length; i++) { 51 | bytes32 hash = bytes32(msgHashes[i]); 52 | l2ToL1Messages()[hash] += 1; 53 | } 54 | 55 | emit MessageHashesAddedFromL2(msgHashes); 56 | } 57 | 58 | } 59 | --------------------------------------------------------------------------------