Skip to content

Openzeppelin contract requirements #71

@smiasojed

Description

@smiasojed

Critical Gap Analysis: cargo-pvm-contract vs openzeppelin-stylus
Below is a critical, ordered list of what's missing.

  1. Storage primitives — only single-slot Lazy and Mapping<K,V> exist
    Blocking impact: ~80% of the OZ Stylus contracts cannot be ported as-is.
    What pvm-storage supports today (crates/pvm-storage/src/lib.rs):
    Lazy and Mapping<K, V> — and both are hard-gated to T: StaticEncodedLen with ENCODED_SIZE == 32. A const assert in Lazy::new rejects any other size.
    A #[slot(N)] field attribute on the contract struct. Slots are hand-numbered by the author.
    What stylus-sdk provides and OZ uses pervasively:
    StorageVec — used in Trace/Checkpoint, EnumerableSet, ERC721Enumerable, ERC721Consecutive.
    StorageString, StorageBytes — required for Erc20Metadata (name/symbol), Erc721URIStorage, Erc1155URIStorage, Erc1155MetadataURI.
    StorageMap<K, StorageMap<K2, ...>> with non-32-byte values (e.g. nested map values that are themselves arrays/strings).
    Composable typed storage where any type that implements StorageType can be a field — see Trace<S: Size> which parameterizes the storage element.
    StorageGuard / StorageGuardMut — borrow-checker handles for nested setters (positions.setter(value).set(position)).
    StorageSlot::get_slot::(IMPLEMENTATION_SLOT) — slot-by-keccak access used by all ERC-1967/UUPS/Beacon proxies.
    What's blocking it in PVM: the Solidity-compatible derivation for dynamic types (bytes/string use keccak256(slot) as the data root with length packed into the slot; arrays use keccak256(slot) + i). The AsStorageKey macro is explicitly limited to 32-byte scalars and a comment forbids extending it. No abstraction for length-prefixed slots, no StorageType-style trait that types like StorageVec could implement.
  2. Storage composition / contract inheritance — no equivalent of #[storage] field embedding
    Blocking impact: Core to the entire OZ design pattern. Every extension is implemented as a struct with a pub erc20: Erc20 field.
    Example from token/erc20/extensions/burnable.rs and the README:
    #[storage]
    struct MyContract { pub erc20: Erc20 }
    The stylus #[storage] macro recursively assigns slot ranges based on each field's StorageType::SLOT_BYTES. You can nest Erc20 inside another #[storage] and the slots compose automatically.
    In PVM, #[slot(N)] is per-leaf-field on the single contract struct. There is no way to say "this field is a sub-component starting at slot N and consuming the next M slots." If you wanted Erc20Capped, you'd have to flatten all of Erc20's fields plus the cap field onto one struct and re-implement every method by hand. There is no #[contract] attribute that lets one contract delegate dispatch to another struct's methods.
    The macro at pvm-contract-macros/src/codegen/contract.rs is built around a single pub struct Foo with all impl Foo { #[method] fn ... } methods. No #[implements(IErc20)], no method import from another module/struct.
  3. Events — no encoding abstraction, contracts hand-roll selectors and topics
    Blocking impact: Every event-emitting contract is harder; large surface for bugs.
    pvm-contract-types::HostApi::deposit_event(&topics, &data) is the only API. The Events example shows what a developer must do today: hardcode VALUE_CHANGED_SIG as a [u8; 32] literal, manually pad the indexed address into a 32-byte topic, manually encode the non-indexed body. No compile-time selector computation, no derive macro.
    Stylus uses alloy_sol_macro::sol! { event Transfer(address indexed from, ...); }. The macro produces SolEvent impls with SIGNATURE_HASH, topics, and encoded body; emit is one call. Required for every ERC-20/721/1155/AccessControl/Ownable/Pausable contract.
    What's needed in PVM: a #[derive(SolEvent)] or sol_event! macro that parses the event signature, computes the topic-0 keccak at compile time, generates indexed-vs-data field splitting, and an event.emit(host) helper.
  4. Trait-based interfaces & #[interface_id] — no ERC-165 introspection
    Blocking impact: ERC-165 is required by ERC-721, ERC-1155, AccessControl, Ownable, ERC-4626, and the supportsInterface check is part of safe-transfer flows (onERC721Received).
    OZ provides openzeppelin-stylus-proc::#[interface_id] which sums Keccak selectors over a trait's methods to produce ::interface_id() -> B32. It's applied to every interface trait (IErc20, IErc721, IErc1155, IAccessControl, IErc20Permit, IErc4626, IErc721Receiver, IErc1155Receiver, IErc1822Proxiable, etc).
    PVM has no proc macro for this and no trait-based dispatch pattern. The Solidity interface lives in a .sol file, not in Rust traits. There's no concept of "implement multiple interfaces and merge dispatch."
  5. EVM precompile reachability — ECDSA / EIP-712 / P256 fundamentally blocked
    Blocking impact: Hard blocker for ECDSA, EIP-712, ERC20Permit, ERC2612, P256Verify precompile, and anything using ecrecover (signatures, meta-transactions, ERC-1271-style flows).
    OZ Stylus uses:
    ECRECOVER_ADDR = 0x0...01 via call::static_call.
    P256_VERIFY_ADDRESS = 0x0...0100 (RIP-7212).
    For PVM/pallet-revive, this depends on what precompiles the runtime actually exposes; pallet-revive-uapi advertises a precompiles-sol-interfaces feature but the PVM SDK provides no Rust wrapper, no fixed-address helpers, and no equivalent of OZ's Precompiles trait. Until (a) pallet-revive confirms which Ethereum precompiles are present at which addresses, and (b) the SDK ships typed wrappers, every signature-checking OZ contract is non-portable.
    EIP-712 also needs chain_id and the contract's own address baked into the domain separator — HostApi::chain_id exists, so the EIP-712 primitive itself is implementable, but ECRecover-based signature verification on top of it is not.
  6. Static calls / typed cross-contract calls with &self view callees
    Partial gap, ergonomic blocker.
    OZ pattern: interface.supports_interface(Call::new_in(self), interface_id) from a &self method. Call::new_in builds a StaticCallContext.
    PVM has CallBuilder with View/Pure/NonPayable/Payable typestates and abi_import! for typed wrappers, which is conceptually similar. What's missing from the OZ porting perspective:
    A generated Erc20Interface::new(address) style that's analogous to the alloy-sol!-generated IErc20Solidity types used everywhere in OZ.
    The MethodError ergonomics: stylus errors flatten into Vec via MethodError::encode; PVM SolRevert writes into a caller-provided buffer. Not a hard blocker but every error type defined by OZ would need re-derivation.
  7. alloy-sol-types / sol! macro — only partial parity
    Partial gap. PVM has #[derive(SolType)] and #[derive(SolError)] and supports primitives + arrays + tuples + custom structs. But:
    No sol! { … } block macro that parses Solidity-flavored declarations of struct, event, error, function together. Every OZ file uses sol! { error ERC20InsufficientBalance(...); } for revert types.
    No &[u8] SolEncode impl (acknowledged in "Known Gaps").
    No Bytes / FixedBytes distinction beyond [u8; N].
  8. ABI export from Rust source — only .sol-driven
    OZ Stylus has the export-abi feature: compile your Rust contract with --features export-abi and it prints the Solidity ABI/interface from the trait + method signatures. PVM's flow is the opposite — it consumes .sol and validates Rust matches. This means OZ-style "Rust is the source of truth, ABI is derived" workflows aren't possible without writing a .sol first. Not strictly blocking but a friction point for adoption.
  9. Testing framework — no multi-contract motsu equivalent
    OZ Stylus uses motsu heavily for unit tests with multi-account fakes, contract-to-contract orchestration, pranking msg::sender, simulating multiple deployments. PVM's MockHost handles single-contract host-side mocking only. Cross-contract tests have to go through the e2e test harness at crates/pvm-contract-e2e-tests, which is much heavier than a unit test.
  10. Reentrancy guard — explicitly absent
    CLAUDE.md states: "reentrancy-sensitive method needs an explicit guard (not provided by the SDK yet)." OZ Stylus has a reentrant cargo feature flag (stylus-sdk/reentrant) and patterns assuming it's available. For PVM, pallet-revive rejects reentrancy by default, so the OZ guard is less necessary — but anything that opts into allow_reentry (flash loans, ERC-777-style hooks, callback patterns) loses the explicit nonReentrant modifier semantics OZ provides.
  11. Convenience accessors — msg::sender(), block::timestamp(), Address::has_code()
    Ergonomic only, not blocking.
    OZ contracts call msg::sender(), msg::value(), block::timestamp(), block::number(), block::chainid() as free functions and addr.has_code() as an Address method. PVM forces:
    let mut caller = [0u8; 20]; self.host().caller(&mut caller);
    Every call requires a stack buffer and a typed re-wrap. AddressUtils helpers don't exist. This isn't a blocker but multiplies the line count of every ported contract by ~2×.
  12. Bytes / MethodError::encode interop type
    Many OZ functions return Result<Bytes, Error> where Bytes is stylus_sdk::abi::Bytes — used for proxy delegate-call returns, ERC-1271 sig checks, EIP-712 hooks. PVM uses Vec or a bytes no-alloc inline decode path; no equivalent newtype, and the dynamic encoding path requires the alloc feature.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions