The design decisions behind OpenZeppelin Contracts for Sui and the reasoning
that holds them together. This document answers why the library is shaped
the way it is. For how to write code that fits - naming, ordering, idioms -
see STYLEGUIDE.md.
This file is read by humans and by AI agents (every Dev3 stage, Codex, Cursor, Copilot, Gemini) to ground design choices in the project's conventions rather than generic internet patterns.
OpenZeppelin Contracts for Sui is a collection of secure, reusable Move libraries - not a dApp. Components are designed to be composed into downstream applications via Programmable Transaction Blocks (PTBs), audited independently, and adopted piecemeal. Every design choice favours integrator safety and composability over end-to-end convenience.
The library targets the Sui CLI version pinned in README.md and
every package is on Move 2024 (edition = "2024").
Packages are grouped by domain (contracts/, math/, collections/). Each leaf directory with
a Move.toml is an independently buildable, independently publishable package.
The package catalog is owned by the READMEs, not this file. What each package and module does, install snippets, and docs links live in the group READMEs and each package's own README - the single source of truth for that information:
contracts/README.md→ per-package READMEs (e.g.contracts/access/README.md)math/README.md→ per-package READMEscollections/README.md→ theopenzeppelin_collectionspackage README (modulessorted_map,sorted_set)
This document covers only the principles behind the layout, never the catalog.
Why split into many small packages? Each package has its own dependency set, its own audit surface, and its own published address. Integrators depend only on the components they use, and a vulnerability in one package never forces a republish of the others - the package boundary is an audit boundary.
Conventional package layout. Every package follows the same internal shape:
sources/- public modules;sources/internal/for package-private helperstests/- unit tests (mirrors thesources/grouping)
Authorization is carried by capability objects (*Cap), not by address
allow-lists stored in shared state. Holding the object is the permission. This
keeps authorization checks local, composable, and free of global mutable lists
that grow unboundedly. See openzeppelin_access for the canonical pattern.
- Owned objects for 1-to-1 relationships - cheaper, no consensus ordering.
- Shared objects for multi-user / concurrent access.
- Shared-object creation is two functions: one creates and returns the object, one shares it - so callers can run setup logic before the object becomes shared.
These are defaults that keep components reusable in PTBs - not absolutes. Some functions legitimately transfer (when transferring is the operation's semantic); treat those as documented exceptions, not reasons to drop the guideline.
- Prefer functions that return objects over self-transferring them. Returning
lets the caller chain the result into the next PTB command; an internal
transfer::transferends the value's life and breaks composition. - Mint/create functions should return the created object rather than transferring it internally.
- Return excess coins (even zero-value) rather than transferring them internally.
- Keep core logic mostly free of
transfer::*- push transfers to the edges (entry functions / the integrator) where practical.
Sui objects are capped at 250 KB and abort the transaction if exceeded. The library therefore:
- uses
vector/VecSet/VecMaponly for bounded collections (≤ 1000 items), - uses
Table/Bag/ObjectBag/ObjectTable/LinkedTablefor large or unbounded collections, - never embeds an ever-growing vector inside a long-lived object.
Prefer explicit, typed storage keys (positional *Key structs) for dynamic
fields over ad-hoc primitive keys - they document intent and prevent key
collisions across features in the same object.
For fungible-asset custody, prefer Sui Address Balances - an account-level balance model that eliminates coin selection, merges deposits automatically, and enables stateless transaction construction and direct gas payment. See the Sui asset-custody docs.
This is a library: downstream packages depend on published bytecode that cannot be retracted. Design for forward compatibility from day one.
publicfunction signatures are permanent. Once published, apublicsignature can never change. Expose only the stable external API aspublic; keep anything you may want to evolvepublic(package)or private (the rule lives inSTYLEGUIDE.md).- Struct types are immutable across upgrades - they cannot be deleted, redefined, or have their abilities changed once published. Introduce a struct only when its shape is settled.
- Error codes are append-only. Never renumber an existing
#[error(code = N)]- integrators match on the numeric abort code (the rule lives inSTYLEGUIDE.md). - Version long-lived shared state so a module published in a later upgrade can reject calls made against a stale version of the object.
- Keep extension-package interfaces unchanging so a change never breaks the packages that depend on or extend them.
- Unit tests live in
tests/; in-module test code is limited to#[test_only]helpers and#[test]functions that reach private items (seeSTYLEGUIDE.md). - Property-style tests use Sui's
#[random_test]attribute (see thesd29x9/ud30x9test suites for the established pattern). - New code must meet the coverage gate in
CONTRIBUTING.md.
When extending this library with AI assistance:
- Match existing patterns in the touched package before introducing new ones; consistency across the library outranks local cleverness.
- Treat this file and
STYLEGUIDE.mdas authoritative over generic Move/Sui patterns learned from the wider internet. - The package split is an audit boundary, not just an organisational one - which
is why changing it (or adding dependencies) requires sign-off; see the policy
in
CONTRIBUTING.md. - Prefer the boring, explicit solution; this is security-critical library code.