Defining Messages
Messages define the external interface of a contract: the operations that can be called from outside. They’re similar to external functions in Solidity but with a cleaner, more explicit design.
The msg Declaration
Section titled “The msg Declaration”Define a message group using the msg keyword:
use std::abi::sol
msg TokenMsg { #[selector = sol("transfer(address,uint256)")] Transfer { to: Address, amount: u256 } -> bool,
#[selector = sol("balanceOf(address)")] BalanceOf { account: Address } -> u256,
#[selector = sol("totalSupply()")] TotalSupply -> u256,}Each message group contains one or more variants: individual operations that can be called.
Message Variants
Section titled “Message Variants”A variant defines a single callable operation:
#[selector = sol("transfer(address,uint256)")] Transfer { to: Address, amount: u256 } -> bool,Components:
- Selector attribute: The 4-byte identifier (
#[selector = sol(...)]) - Name: The variant name (
Transfer) - Fields: Parameters in curly braces (
{ to: Address, amount: u256 }) - Return type: What the handler returns (
-> bool)
Variants Without Parameters
Section titled “Variants Without Parameters”Some operations don’t need parameters:
#[selector = sol("totalSupply()")] TotalSupply -> u256,Variants Without Return Values
Section titled “Variants Without Return Values”Operations that don’t return a value omit the return type:
#[selector = sol("safeTransferFrom(address,address,uint256)")] SafeTransfer { from: Address, to: Address, token_id: u256 },This implicitly returns () (unit).
Complete Example
Section titled “Complete Example”A simple token message interface:
use std::abi::sol
msg Erc20 { #[selector = sol("transfer(address,uint256)")] Transfer { to: Address, amount: u256 } -> bool,
#[selector = sol("approve(address,uint256)")] Approve { spender: Address, amount: u256 } -> bool,
#[selector = sol("transferFrom(address,address,uint256)")] TransferFrom { from: Address, to: Address, amount: u256 } -> bool,
#[selector = sol("balanceOf(address)")] BalanceOf { account: Address } -> u256,
#[selector = sol("allowance(address,address)")] Allowance { owner: Address, spender: Address } -> u256,
#[selector = sol("totalSupply()")] TotalSupply -> u256,}Why Messages?
Section titled “Why Messages?”Messages provide:
- Clear interface definition: All callable operations in one place
- ABI compatibility: Selectors match Ethereum’s function selector mechanism
- Type safety: Parameters and return types are checked at compile time
- Separation of concerns: Interface definition separate from implementation
Using Messages
Section titled “Using Messages”Messages are handled in recv blocks within contracts:
contract Token { // storage fields...
recv Erc20 { Transfer { to, amount } -> bool { // handle transfer true } // ... other handlers }}See Receive Blocks for details on implementing handlers.
Multiple Return Values
Section titled “Multiple Return Values”A handler returning a tuple uses Solidity’s multiple-return-value encoding. For example, -> (DynString, u256) corresponds to returns (string, uint256). The ABI lists two outputs, and the dynamic string’s offset is measured from that output parameter list.
Fe 26.4 corrects the encoding of tuples containing dynamic elements: earlier compilers could insert an extra outer offset. Static tuple returns are unchanged. A single DynString return still follows the usual single-dynamic-output encoding.
use std::abi::{sol, DynString}
msg InfoMsg { #[selector = sol("info()")] Info -> (DynString, u256),}
pub contract Info { recv InfoMsg { Info -> (DynString, u256) { ("Fe", 264) } }}
#[test]fn multiple_return_values() uses (evm: mut Evm) { let target = evm.create2<Info>(value: 0, args: (), salt: 0) let outcome = evm.try_call(addr: target, gas: 1000000, value: 0, message: InfoMsg::Info {}) assert!(outcome.success()) let data = outcome.returndata() assert!(data.word_at(0) == 64) // String tail follows the two-word head. assert!(data.word_at(32) == 264) let (name, version): (DynString, u256) = evm.call( addr: target, gas: 1000000, value: 0, message: InfoMsg::Info {}, ) assert!(name == "Fe" && version == 264)}