Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ The `pvm-storage` crate provides typed storage helpers with Solidity-compatible
- `set(&mut self)` / `insert(&mut self)` / `entry(&mut self)` take `&mut self` for future view enforcement
- `Mapping::entry()` returns a `Lazy<V>` handle for the derived slot, allowing read-then-write on the same key with a single keccak derivation instead of two
- Nested mappings via chaining: `self.allowances.get(&owner).get(&spender)`
- **Composability:** `#[storage]` structs auto-implement `StorageComponent` (slot reservation) and, under `--features abi-gen`, `StorageLayoutEmit` (so the outer contract flattens their leaves into the `storageLayout` JSON with dotted labels like `erc20.total_supply`). Hand-rolled storage components (e.g. a future `StorageVec<T>`) must implement both traits to participate in auto-numbered layouts and abi-gen output. `StorageComponent::PACKED_BYTES = 32` declares "always start a fresh slot" — mappings, multi-slot composites, and embedded `#[storage]` sub-structs all set this; sub-32-byte primitives propagate `T::PACKED_BYTES`
- **Field-level packing:** adjacent sub-32-byte contract fields share a slot byte-for-byte with solc's layout — `Lazy<u128> a; Lazy<u128> b;` lands at `(slot=0, offset=16)` and `(slot=0, offset=0)`. The macro walker (`pvm_storage::layout_step`) is the const-fn computing each placement; the `storageLayout` JSON's `offset` field matches solc. Packed writes are read-modify-write (one SLOAD + one SSTORE), matching solc/Stylus gas profile
- **Composability:** `#[storage]` structs auto-implement `StorageComponent` (slot reservation) and, under `--features abi-gen`, `StorageLayoutEmit` (so the outer contract flattens their leaves into the `storageLayout` JSON with dotted labels like `erc20.total_supply`). Hand-rolled storage components (e.g. `StorageVec<T>`) must implement both traits to participate in auto-numbered layouts and abi-gen output. `StorageComponent::PACKED_BYTES = 32` declares "always start a fresh slot" — mappings, multi-slot composites, and embedded `#[storage]` sub-structs all set this; sub-32-byte primitives propagate `T::PACKED_BYTES`
- **Field-level packing:** adjacent sub-32-byte contract fields share a slot byte-for-byte with solc's layout — `Lazy<u128> a; Lazy<u128> b;` lands at `(slot=0, offset=16)` and `(slot=0, offset=0)`. The macro walker (`pvm_storage::layout_step`) is the const-fn computing each placement; the `storageLayout` JSON's `offset` field matches solc. Packed writes are read-modify-write (one SLOAD + one SSTORE), matching solc's gas profile
- **`try_get` is full-slot only:** `Lazy::<T>::try_get` is rejected at compile time for sub-32-byte `T` (e.g. `Lazy<u128>`) with a const-assert message — a neighbour's write to the same slot would make `try_get` indistinguishable from `get`. For packed fields, use `.get()` and compare to the zero value of `T`
- **Test-harness contract modules:** `#[contract(no_main)]` suppresses the abi-gen `fn main()` emission so a `#[contract]` can sit inside a `tests/` integration test or library crate without colliding with the test harness's own `main`. `__abi_json()` / `__storage_layout_json()` accessors are still emitted on the module

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

interface Allowlist {
function add(address a) external;
function remove(uint64 index) external;
function contains(address a) external view returns (bool);
function count() external view returns (uint64);
function at(uint64 index) external view returns (address);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#![no_main]
#![no_std]

#[pvm_contract_sdk::contract("Allowlist.sol", buffer = 256)]
mod allowlist {
use pvm_contract_sdk::{Address, EmptyError, StorageVec};

pub struct Allowlist {
addresses: StorageVec<Address>,
}

impl Allowlist {
#[pvm_contract_sdk::constructor]
pub fn new(&mut self) -> Result<(), EmptyError> {
Ok(())
}

/// Append `a` to the end of the list. Duplicates allowed —
/// `contains` returns `true` if any entry matches.
#[pvm_contract_sdk::method]
pub fn add(&mut self, a: Address) -> Result<(), EmptyError> {
self.addresses.push(&a);
Ok(())
}

/// Solc-style swap-and-pop removal: O(1) writes, preserves no order.
/// Out-of-bounds `index` is a no-op.
#[pvm_contract_sdk::method]
pub fn remove(&mut self, index: u64) -> Result<(), EmptyError> {
let len = self.addresses.len();
if index >= len {
return Ok(());
}
let last_idx = len - 1;
if index != last_idx {
let last = self.addresses.get(last_idx);
self.addresses.set(index, &last);
}
self.addresses.pop();
Ok(())
}

/// Linear scan — O(n). Realistic for small allowlists (governance,
/// multisig owners). For large sets use `Mapping<Address, bool>`.
#[pvm_contract_sdk::method]
pub fn contains(&self, a: Address) -> bool {
let len = self.addresses.len();
let mut i = 0u64;
while i < len {
if self.addresses.get(i) == a {
return true;
}
i += 1;
}
false
}

#[pvm_contract_sdk::method]
pub fn count(&self) -> u64 {
self.addresses.len()
}

#[pvm_contract_sdk::method]
pub fn at(&self, index: u64) -> Address {
self.addresses.get(index)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#![no_main]
#![no_std]

#[pvm_contract_sdk::contract("Allowlist.sol", allocator = "bump")]
mod allowlist {
use pvm_contract_sdk::{Address, EmptyError, StorageVec};

pub struct Allowlist {
addresses: StorageVec<Address>,
}

impl Allowlist {
#[pvm_contract_sdk::constructor]
pub fn new(&mut self) -> Result<(), EmptyError> {
Ok(())
}

/// Append `a` to the end of the list. Duplicates allowed —
/// `contains` returns `true` if any entry matches.
#[pvm_contract_sdk::method]
pub fn add(&mut self, a: Address) -> Result<(), EmptyError> {
self.addresses.push(&a);
Ok(())
}

/// Solc-style swap-and-pop removal: O(1) writes, preserves no order.
/// Out-of-bounds `index` is a no-op.
#[pvm_contract_sdk::method]
pub fn remove(&mut self, index: u64) -> Result<(), EmptyError> {
let len = self.addresses.len();
if index >= len {
return Ok(());
}
let last_idx = len - 1;
if index != last_idx {
let last = self.addresses.get(last_idx);
self.addresses.set(index, &last);
}
self.addresses.pop();
Ok(())
}

/// Linear scan — O(n). Realistic for small allowlists (governance,
/// multisig owners). For large sets use `Mapping<Address, bool>`.
#[pvm_contract_sdk::method]
pub fn contains(&self, a: Address) -> bool {
let len = self.addresses.len();
let mut i = 0u64;
while i < len {
if self.addresses.get(i) == a {
return true;
}
i += 1;
}
false
}

#[pvm_contract_sdk::method]
pub fn count(&self) -> u64 {
self.addresses.len()
}

#[pvm_contract_sdk::method]
pub fn at(&self, index: u64) -> Address {
self.addresses.get(index)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.0;

// Simplified Uniswap V2-style packed pool reserves. `reserve0` and
// `reserve1` are stored together so a single SLOAD reads both — the
// canonical Solidity pattern for sub-256-bit values that change atomically.
interface AmmReserves {
function getReserves() external view returns (uint128, uint128);
function sync(uint128 reserve0, uint128 reserve1) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#![no_main]
#![no_std]

// Simplified Uniswap V2-style packed pool reserves. The two `u128` fields
// live inside one `Lazy<Reserves>` so the storage cell occupies exactly one
// slot — `getReserves` is a single SLOAD and `sync` is a single SSTORE,
// matching solc's gas profile for `uint128 reserve0; uint128 reserve1;`.
//
// Compare with `packed_handle`-style sibling `Lazy<u128>` fields: those also
// land in the same slot via the macro's auto-numbered slot walker, but each
// `.get()` / `.set()` issues its own SLOAD (and `.set()` does a full RMW),
// so two accesses cost two host calls. The struct-in-Lazy form below
// batches both fields into a single host round-trip.
#[pvm_contract_sdk::contract("AmmReserves.sol", buffer = 256)]
mod amm_reserves {
use pvm_contract_sdk::{EmptyError, Lazy, SolType};

#[derive(SolType)]
pub struct Reserves {
pub reserve0: u128,
pub reserve1: u128,
}

pub struct AmmReserves {
reserves: Lazy<Reserves>,
}

impl AmmReserves {
#[pvm_contract_sdk::constructor]
pub fn new(&mut self) -> Result<(), EmptyError> {
Ok(())
}

#[pvm_contract_sdk::method]
pub fn get_reserves(&self) -> (u128, u128) {
let r = self.reserves.get();
(r.reserve0, r.reserve1)
}

#[pvm_contract_sdk::method]
pub fn sync(&mut self, reserve0: u128, reserve1: u128) -> Result<(), EmptyError> {
self.reserves.set(&Reserves { reserve0, reserve1 });
Ok(())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#![no_main]
#![no_std]

// Simplified Uniswap V2-style packed pool reserves. The two `u128` fields
// live inside one `Lazy<Reserves>` so the storage cell occupies exactly one
// slot — `getReserves` is a single SLOAD and `sync` is a single SSTORE,
// matching solc's gas profile for `uint128 reserve0; uint128 reserve1;`.
#[pvm_contract_sdk::contract("AmmReserves.sol", allocator = "bump")]
mod amm_reserves {
use pvm_contract_sdk::{EmptyError, Lazy, SolType};

#[derive(SolType)]
pub struct Reserves {
pub reserve0: u128,
pub reserve1: u128,
}

pub struct AmmReserves {
reserves: Lazy<Reserves>,
}

impl AmmReserves {
#[pvm_contract_sdk::constructor]
pub fn new(&mut self) -> Result<(), EmptyError> {
Ok(())
}

#[pvm_contract_sdk::method]
pub fn get_reserves(&self) -> (u128, u128) {
let r = self.reserves.get();
(r.reserve0, r.reserve1)
}

#[pvm_contract_sdk::method]
pub fn sync(&mut self, reserve0: u128, reserve1: u128) -> Result<(), EmptyError> {
self.reserves.set(&Reserves { reserve0, reserve1 });
Ok(())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface MyToken {
event Transfer(address indexed from, address indexed to, uint256 value);
error InsufficientBalance();

function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);

function transfer(address to, uint256 amount) external;
function mint(address to, uint256 amount) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#![no_main]
#![no_std]

use pvm_contract_sdk::U256;
#[pvm_contract_sdk::contract("MyToken.sol", allocator = "bump")]
mod my_token {
use super::*;
use pvm_contract_sdk::{Address, HostApi, Lazy, Mapping, SolDefaultError, SolError};

#[derive(Debug, pvm_contract_sdk::SolError)]
pub struct InsufficientBalance;

#[derive(Debug, pvm_contract_sdk::SolError)]
pub enum TokenError {
InsufficientBalance(InsufficientBalance),
SolDefaultError(SolDefaultError),
}

pub struct MyToken {
#[slot(0)]
total_supply: Lazy<U256>,
#[slot(1)]
balances: Mapping<Address, U256>,
}

impl MyToken {
#[pvm_contract_sdk::constructor]
pub fn new(&mut self) -> Result<(), TokenError> {
Ok(())
}

#[pvm_contract_sdk::method]
pub fn total_supply(&self) -> U256 {
self.total_supply.get()
}

#[pvm_contract_sdk::method]
pub fn balance_of(&self, account: Address) -> U256 {
self.balances.get(&account)
}

#[pvm_contract_sdk::method]
pub fn transfer(&mut self, to: Address, amount: U256) -> Result<(), TokenError> {
let caller = self.caller();

let mut sender_cell = self.balances.entry(&caller);
let sender_balance = sender_cell.get();
if sender_balance < amount {
return Err(InsufficientBalance.into());
}
sender_cell.set(&(sender_balance - amount));

let mut recipient_cell = self.balances.entry(&to);
let recipient_balance = recipient_cell.get();
recipient_cell.set(&(recipient_balance + amount));

Ok(())
}

#[pvm_contract_sdk::method]
pub fn mint(&mut self, to: Address, amount: U256) -> Result<(), TokenError> {
let mut recipient_cell = self.balances.entry(&to);
let new_balance = recipient_cell.get().saturating_add(amount);
recipient_cell.set(&new_balance);

let new_supply = self.total_supply.get().saturating_add(amount);
self.total_supply.set(&new_supply);
Ok(())
}

#[pvm_contract_sdk::fallback]
pub fn fallback(&mut self) -> Result<(), TokenError> {
Ok(())
}

fn caller(&self) -> Address {
let mut caller = [0u8; 20];
self.host().caller(&mut caller);
Address(caller)
}
}
}
Loading
Loading