Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,15 @@ The macro injects a `pub host: Host` field on the storage struct and a `fn host(

**DSL API** (explicit, manual dispatch):
```rust
let host = Host::new();
ContractBuilder::new()
.method(BALANCE_OF_SELECTOR, balance_of_handler)
.method(TRANSFER_SELECTOR, transfer_handler)
.dispatch::<HostFnImpl, 256>()
.dispatch_impl::<256>(&host)
```

DSL handlers take a concrete `&Host` (same type the macro path injects on the storage struct). For typed cross-contract calls, handlers wrap a cloned host in `Context::new(host.clone())` — `Context` impls `ContractContext` so it can be passed to `.call(&mut cx)` / `.delegate_call(&mut cx)`. `Host::clone()` is `Copy` on riscv64 (ZST) and a single `Rc::clone` on host targets. Because the wrapper carries only the host handle (no storage state), the borrow checker cannot enforce view-vs-mutating in DSL; use the `#[contract]` macro path if you need that static guarantee. The same `Context` type is used in unit tests, where it owns a `Host` backed by a `MockHost`.

### Macro-Generated Code

The `#[contract]` macro generates two PolkaVM entry points:
Expand All @@ -104,6 +107,48 @@ Selectors are Keccak-256 of the canonical Solidity signature (first 4 bytes), co

- `#[method]` — marks a public function as a contract method
- `#[method(rename = "name")]` — overrides the Solidity function name (default: snake_case to camelCase)
- `#[payable]` — marks the method as `payable` (must be combined with `&mut self`)

### Mutability Inference

Solidity `stateMutability` is inferred from the Rust receiver. No explicit `#[view]` or `#[pure]` attribute — receiver shape is the source of truth.

| Receiver | `#[payable]` | ABI emits |
|---|---|---|
| none (`fn foo(args)`) | — | `pure` |
| `&self` | — | `view` |
| `&mut self` | — | `nonpayable` |
| `&mut self` | yes | `payable` |
| `&self` | yes | **compile error** |
| no receiver | yes | **compile error** |

**Constructor:** must take `&mut self`; `pure`/`view` constructors are rejected (they cannot initialize storage). `#[payable]` is allowed.

**Fallback:** follows the same inference table as regular methods.

**`.sol` consistency check:** when a `.sol` interface is provided, the macro errors if the Rust-inferred mutability disagrees with the `.sol` declaration (e.g., `.sol` says `view` but Rust uses `&mut self`).

### Mutability Enforcement

Three layers, in increasing strength:

1. **Compile-time (typed-API)** — `#[contract]` auto-implements `ContractContext` on the storage struct (and forbids `#[derive(Clone)]` on it). Cross-contract call builders take `&impl ContractContext` for `view`/`pure` callees and `&mut impl ContractContext` for `nonpayable`/`payable` callees, so a `&self` (view) method *cannot* initiate a state-mutating call through the typed `abi_import!`-generated SDK. `delegate_call` and `instantiate` always require `&mut`. Storage helpers (`Lazy`, `Mapping`) similarly gate `set`/`insert` on `&mut self`.

2. **Runtime (contract-side)** — non-payable methods (`pure`/`view`/`nonpayable`) get an injected `__pvm_assert_non_payable` / `__pvm_assert_value_zero` guard at the dispatch entry; the contract reverts if `msg.value > 0`.

3. **Runtime (host-side)** — `pallet-revive` enforces the STATICCALL boundary: state-mutating host calls revert when invoked inside a static frame. This is what backstops `view`/`pure` for cross-contract callers.

**Honest caveat:** the typed-API gate covers cross-contract calls made through `abi_import!`-generated wrappers and storage operations through `pvm-storage`. Raw `pallet_revive_uapi` calls (e.g., `api::set_storage`) bypass the type-level check — only the host's STATICCALL enforcement and the runtime payable guard apply there. Use the typed APIs as the primary surface; reach for raw uAPI only when the typed surface lacks coverage.

**Pure semantics (matches Solidity, by design):** a pure method has no receiver and therefore no `host` accessor. By construction it cannot:
- make cross-contract calls (no `&impl ContractContext` to pass to `CallBuilder::call`),
- read block/chain/tx context (`block.number`, `chain.id`, etc.),
- call host-routed helpers (`keccak256`, event emission, storage),
- emit events.

This matches Solidity's `pure` rules — solc rejects the same operations in a `pure` function. If a method needs `keccak256`, block context, or any host call, mark it `view` (`&self`) rather than pure. The restriction isn't a SDK limitation; it's the same semantic boundary Solidity callers expect when they see `pure` in the ABI.

**Reentrancy non-protection:** `&mut self` enforces single-threaded mutation within a frame, but persistent storage is shared across reentrant frames (each callee gets a fresh contract struct, so the borrow checker offers no cross-frame guarantee). A reentrancy-sensitive method needs an explicit guard (not provided by the SDK yet).

## Type System

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#![no_main]
#![no_std]

use pvm_contract_builder_dsl::{ContractBuilder, HandlerResult, solidity_selector};
use pvm_contract_builder_dsl::pvm_contract_types::{HostApi, PolkaVmHost, SolDecode, SolEncode, StaticEncodedLen};
use pvm_contract_builder_dsl::{
ContractBuilder, HandlerResult, assert_non_payable_deploy, solidity_selector,
};
use pvm_contract_builder_dsl::pvm_contract_types::{Host, SolDecode, SolEncode, StaticEncodedLen};

const FIBONACCI_SELECTOR: [u8; 4] = solidity_selector("fibonacci(uint32)");

Expand All @@ -16,18 +18,20 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {

#[unsafe(no_mangle)]
#[polkavm_derive::polkavm_export]
pub extern "C" fn deploy() {}
pub extern "C" fn deploy() {
assert_non_payable_deploy(&Host::new());
}

#[unsafe(no_mangle)]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
let host = PolkaVmHost;
ContractBuilder::<PolkaVmHost>::new()
.method(FIBONACCI_SELECTOR, fibonacci_handler::<PolkaVmHost>)
let host = Host::new();
ContractBuilder::new()
.method(FIBONACCI_SELECTOR, fibonacci_handler)
.dispatch_impl::<256>(&host);
}

fn fibonacci_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn fibonacci_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let n = u32::decode_at(input, 0);
let result = fibonacci(n);
let len = <u32 as StaticEncodedLen>::ENCODED_SIZE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod fibonacci {
}

#[pvm_contract_sdk::method]
pub fn fibonacci(&self, n: u32) -> u32 {
pub fn fibonacci(n: u32) -> u32 {
if n <= 1 {
n
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mod fibonacci {
}

#[pvm_contract_sdk::method]
pub fn fibonacci(&self, n: u32) -> u32 {
pub fn fibonacci(n: u32) -> u32 {
if n <= 1 {
n
} else {
Expand Down
54 changes: 29 additions & 25 deletions crates/cargo-pvm-contract/templates/examples/multi/multi_dsl.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#![no_main]
#![no_std]

use pvm_contract_builder_dsl::{ContractBuilder, HandlerResult, solidity_selector};
use pvm_contract_builder_dsl::pvm_contract_types::{HostApi, PolkaVmHost, SolDecode, SolEncode, StaticEncodedLen};
use pvm_contract_builder_dsl::{
ContractBuilder, HandlerResult, assert_non_payable_deploy, solidity_selector,
};
use pvm_contract_builder_dsl::pvm_contract_types::{Host, SolDecode, SolEncode, StaticEncodedLen};
use pvm_contract_builder_dsl::ruint::aliases::U256;

const ADD_SELECTOR: [u8; 4] = solidity_selector("add(uint32,uint32)");
Expand All @@ -26,27 +28,29 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {

#[unsafe(no_mangle)]
#[polkavm_derive::polkavm_export]
pub extern "C" fn deploy() {}
pub extern "C" fn deploy() {
assert_non_payable_deploy(&Host::new());
}

#[unsafe(no_mangle)]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
let host = PolkaVmHost;
ContractBuilder::<PolkaVmHost>::new()
.method(ADD_SELECTOR, add_handler::<PolkaVmHost>)
.method(MULTIPLY_SELECTOR, multiply_handler::<PolkaVmHost>)
.method(IS_EVEN_SELECTOR, is_even_handler::<PolkaVmHost>)
.method(NEGATE_SELECTOR, negate_handler::<PolkaVmHost>)
.method(MAX_SELECTOR, max_handler::<PolkaVmHost>)
.method(HASH_SELECTOR, hash_handler::<PolkaVmHost>)
.method(SUM3_SELECTOR, sum3_handler::<PolkaVmHost>)
.method(BIT_AND_SELECTOR, bit_and_handler::<PolkaVmHost>)
.method(IS_ZERO_SELECTOR, is_zero_handler::<PolkaVmHost>)
.method(INCREMENT_SELECTOR, increment_handler::<PolkaVmHost>)
let host = Host::new();
ContractBuilder::new()
.method(ADD_SELECTOR, add_handler)
.method(MULTIPLY_SELECTOR, multiply_handler)
.method(IS_EVEN_SELECTOR, is_even_handler)
.method(NEGATE_SELECTOR, negate_handler)
.method(MAX_SELECTOR, max_handler)
.method(HASH_SELECTOR, hash_handler)
.method(SUM3_SELECTOR, sum3_handler)
.method(BIT_AND_SELECTOR, bit_and_handler)
.method(IS_ZERO_SELECTOR, is_zero_handler)
.method(INCREMENT_SELECTOR, increment_handler)
.dispatch_impl::<256>(&host);
}

fn add_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn add_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let a = u32::decode_at(input, 0);
let b = u32::decode_at(input, <u32 as StaticEncodedLen>::ENCODED_SIZE);
let result = a.wrapping_add(b);
Expand All @@ -55,7 +59,7 @@ fn add_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> Handle
HandlerResult::Ok(len)
}

fn multiply_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn multiply_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let a = u64::decode_at(input, 0);
let b = u64::decode_at(input, <u64 as StaticEncodedLen>::ENCODED_SIZE);
let result = a.wrapping_mul(b);
Expand All @@ -64,23 +68,23 @@ fn multiply_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> H
HandlerResult::Ok(len)
}

fn is_even_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn is_even_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let n = u32::decode_at(input, 0);
let result = (n & 1) == 0;
let len = <bool as StaticEncodedLen>::ENCODED_SIZE;
result.encode_to(&mut output[..len]);
HandlerResult::Ok(len)
}

fn negate_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn negate_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let value = U256::decode_at(input, 0);
let result = !value + U256::from(1u8);
let len = <U256 as StaticEncodedLen>::ENCODED_SIZE;
result.encode_to(&mut output[..len]);
HandlerResult::Ok(len)
}

fn max_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn max_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let a = U256::decode_at(input, 0);
let b = U256::decode_at(input, <U256 as StaticEncodedLen>::ENCODED_SIZE);
let result = if a > b { a } else { b };
Expand All @@ -89,7 +93,7 @@ fn max_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> Handle
HandlerResult::Ok(len)
}

fn hash_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn hash_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let account = <[u8; 20]>::decode_at(input, 0);
let mut bytes = [0u8; 32];
bytes[12..].copy_from_slice(&account);
Expand All @@ -99,7 +103,7 @@ fn hash_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> Handl
HandlerResult::Ok(len)
}

fn sum3_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn sum3_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let a = u32::decode_at(input, 0);
let b = u32::decode_at(input, <u32 as StaticEncodedLen>::ENCODED_SIZE);
let c = u32::decode_at(input, <u32 as StaticEncodedLen>::ENCODED_SIZE * 2);
Expand All @@ -109,7 +113,7 @@ fn sum3_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> Handl
HandlerResult::Ok(len)
}

fn bit_and_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn bit_and_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let a = U256::decode_at(input, 0);
let b = U256::decode_at(input, <U256 as StaticEncodedLen>::ENCODED_SIZE);
let result = a & b;
Expand All @@ -118,15 +122,15 @@ fn bit_and_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> Ha
HandlerResult::Ok(len)
}

fn is_zero_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn is_zero_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let value = U256::decode_at(input, 0);
let result = value == U256::ZERO;
let len = <bool as StaticEncodedLen>::ENCODED_SIZE;
result.encode_to(&mut output[..len]);
HandlerResult::Ok(len)
}

fn increment_handler<H: HostApi>(_host: &H, input: &[u8], output: &mut [u8]) -> HandlerResult {
fn increment_handler(_host: &Host, input: &[u8], output: &mut [u8]) -> HandlerResult {
let n = u32::decode_at(input, 0);
let result = n.wrapping_add(1);
let len = <u32 as StaticEncodedLen>::ENCODED_SIZE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ mod multi {
}

#[pvm_contract_sdk::method]
pub fn add(&self, a: u32, b: u32) -> u32 {
pub fn add(a: u32, b: u32) -> u32 {
a.wrapping_add(b)
}

#[pvm_contract_sdk::method]
pub fn multiply(&self, a: u64, b: u64) -> u64 {
pub fn multiply(a: u64, b: u64) -> u64 {
a.wrapping_mul(b)
}

#[pvm_contract_sdk::method]
pub fn is_even(&self, n: u32) -> bool {
pub fn is_even(n: u32) -> bool {
(n & 1) == 0
}

#[pvm_contract_sdk::method]
pub fn negate(&self, value: U256) -> U256 {
pub fn negate(value: U256) -> U256 {
!value + U256::from(1u8)
}

#[pvm_contract_sdk::method]
pub fn max(&self, a: U256, b: U256) -> U256 {
pub fn max(a: U256, b: U256) -> U256 {
if a > b { a } else { b }
}

Expand All @@ -55,22 +55,22 @@ mod multi {
}

#[pvm_contract_sdk::method]
pub fn sum3(&self, a: u32, b: u32, c: u32) -> u32 {
pub fn sum3(a: u32, b: u32, c: u32) -> u32 {
a.wrapping_add(b).wrapping_add(c)
}

#[pvm_contract_sdk::method]
pub fn bit_and(&self, a: U256, b: U256) -> U256 {
pub fn bit_and(a: U256, b: U256) -> U256 {
a & b
}

#[pvm_contract_sdk::method]
pub fn is_zero(&self, value: U256) -> bool {
pub fn is_zero(value: U256) -> bool {
value == U256::ZERO
}

#[pvm_contract_sdk::method]
pub fn increment(&self, n: u32) -> u32 {
pub fn increment(n: u32) -> u32 {
n.wrapping_add(1)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ mod multi {
}

#[pvm_contract_sdk::method]
pub fn add(&self, a: u32, b: u32) -> u32 {
pub fn add(a: u32, b: u32) -> u32 {
a.wrapping_add(b)
}

#[pvm_contract_sdk::method]
pub fn multiply(&self, a: u64, b: u64) -> u64 {
pub fn multiply(a: u64, b: u64) -> u64 {
a.wrapping_mul(b)
}

#[pvm_contract_sdk::method]
pub fn is_even(&self, n: u32) -> bool {
pub fn is_even(n: u32) -> bool {
(n & 1) == 0
}

#[pvm_contract_sdk::method]
pub fn negate(&self, value: U256) -> U256 {
pub fn negate(value: U256) -> U256 {
!value + U256::from(1u8)
}

#[pvm_contract_sdk::method]
pub fn max(&self, a: U256, b: U256) -> U256 {
pub fn max(a: U256, b: U256) -> U256 {
if a > b { a } else { b }
}

Expand All @@ -55,22 +55,22 @@ mod multi {
}

#[pvm_contract_sdk::method]
pub fn sum3(&self, a: u32, b: u32, c: u32) -> u32 {
pub fn sum3(a: u32, b: u32, c: u32) -> u32 {
a.wrapping_add(b).wrapping_add(c)
}

#[pvm_contract_sdk::method]
pub fn bit_and(&self, a: U256, b: U256) -> U256 {
pub fn bit_and(a: U256, b: U256) -> U256 {
a & b
}

#[pvm_contract_sdk::method]
pub fn is_zero(&self, value: U256) -> bool {
pub fn is_zero(value: U256) -> bool {
value == U256::ZERO
}

#[pvm_contract_sdk::method]
pub fn increment(&self, n: u32) -> u32 {
pub fn increment(n: u32) -> u32 {
n.wrapping_add(1)
}
}
Expand Down
Loading
Loading