Skip to content
Draft
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
99 changes: 93 additions & 6 deletions content/contracts-sui/1.x/access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Access
---

On Sui, `sui::transfer::transfer` is instant and irreversible. There is no built-in way to require the recipient to confirm, to enforce a waiting period, or to cancel a transfer in progress. For privileged capability objects, such as admin caps, treasury caps, upgrade authorities, this is a meaningful risk. A single mistaken or malicious transaction can permanently transfer control with no recourse. The `openzeppelin_access` package provides two wrapper policies that add those guardrails.

The `openzeppelin_access` package provides ownership-transfer wrappers for privileged Sui objects (`T: key + store`), such as admin and treasury capabilities.

Use this package when direct object transfer is too permissive for your protocol. It gives you explicit transfer workflows that are easier to review, monitor, and constrain with policy.
Expand All @@ -16,7 +18,7 @@ Add the dependency in `Move.toml`:

```toml
[dependencies]
openzeppelin_access = { r.mvr = "@openzeppelin-move/access" }
openzeppelin_access = { r.mvr = "@pkg/openzeppelin_access" }
```

Import the transfer policy module you want to use:
Expand All @@ -25,17 +27,27 @@ Import the transfer policy module you want to use:
use openzeppelin_access::two_step_transfer;
```

## What's included

- **`two_step_transfer`**: Requires the designated recipient to explicitly accept before the wrapper completes the transfer. The initiator retains cancel authority until acceptance. No time delay, so execution happens immediately once the recipient accepts.
- **`delayed_transfer`**: Requires all transfers and unwraps to be scheduled and enforces a configurable minimum delay before execution. Gives monitoring systems and governance processes time to detect and respond before a transfer is finalized.

## How wrapping works

Wrapping a capability gives up direct transfer access to it. The wrapper becomes the custody object: you hold the wrapper, not the capability directly. The underlying capability is still discoverable by off-chain indexers via dynamic object field, but it cannot be transferred or used until it is extracted through the wrapper's policy. To move or recover the capability, you must go through the wrapper. This means satisfying the policy's conditions: either the recipient's explicit acceptance or the enforced time delay.

## Examples

### Two-step transfer

Wrapping an `AdminCap` creates a `TwoStepTransferWrapper<AdminCap>` owned by the caller. The original cap is stored inside it as a dynamic object field. The caller holds the wrapper, not the cap directly. A `WrapExecuted` event is emitted.

```move
module my_sui_app::admin;

use openzeppelin_access::two_step_transfer;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this empty lines hurts readability


public struct AdminCap has key, store {
id: object::UID,
}

public fun wrap_admin_cap(
cap: AdminCap,
ctx: &mut TxContext,
Expand All @@ -45,10 +57,85 @@ public fun wrap_admin_cap(
}
```

Calling `initiate_transfer` consumes the wrapper by value and creates a shared `PendingOwnershipTransfer<AdminCap>` object. Wrapper custody moves to that request via transfer-to-object. The sender's address is recorded as `from`, the cancel authority, and the recipient's address is recorded as `to`. A `TransferInitiated` event is emitted.

```move
// Called by the current wrapper owner. Consumes the wrapper.
wrapper.initiate_transfer(new_admin_address, ctx);
```

When the designated recipient calls `accept_transfer`, the wrapper is delivered to their address and a `TransferAccepted` event is emitted. Only the address recorded as `to` can complete this step.

```move
// Called by new_admin_address.
// request: shared PendingOwnershipTransfer<AdminCap>
// wrapper_ticket: Receiving<TwoStepTransferWrapper<AdminCap>>
two_step_transfer::accept_transfer(request, wrapper_ticket, ctx);
```

### Delayed transfer

Wrapping a `TreasuryCap` with `delayed_transfer` embeds a minimum delay at construction time. The delay is enforced on every subsequent transfer and unwrap and cannot be changed after wrapping. A `WrapExecuted` event is emitted.

```move
module my_sui_app::treasury;

use openzeppelin_access::delayed_transfer::{Self, DelayedTransferWrapper};

public struct TreasuryCap has key, store { id: UID }

const MIN_DELAY_MS: u64 = 86_400_000; // 24 hours

public fun wrap_treasury_cap(cap: TreasuryCap, ctx: &mut TxContext): DelayedTransferWrapper<TreasuryCap> {
delayed_transfer::wrap(cap, MIN_DELAY_MS, ctx)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is now different. It accepts a recipient and doesn't return the DelayedTransferWrapper

}
```

`DelayedTransferWrapper` has only the `key` ability, so it cannot be transferred with `transfer::transfer` from outside its defining module. This restriction is intentional and enforces that all ownership changes go through the delay mechanism. Return the wrapper from your function and transfer it to the sender in the PTB instead:

```typescript
const [wrapper] = tx.moveCall({
target: `${PKG}::treasury::wrap_treasury_cap`,
arguments: [treasuryCapArg],
});
tx.transferObjects([wrapper], tx.pure.address(sender));
```

`schedule_transfer` records the recipient's address and computes `execute_after_ms` as `clock.timestamp_ms() + min_delay_ms`. The wrapper must not have an action already pending. A `TransferScheduled` event is emitted.

```move
// Called by the current wrapper owner.
wrapper.schedule_transfer(new_owner_address, &clock, ctx);
```

Once the delay has elapsed, `execute_transfer` consumes the wrapper and delivers it to the scheduled recipient. Calling it before `execute_after_ms` aborts with `EDelayNotElapsed`. An `OwnershipTransferred` event is emitted on success.

```move
// Callable after the delay window has passed.
wrapper.execute_transfer(&clock, ctx);
```

## Choosing a transfer policy

- Use `two_step_transfer` when the signer triggering transfer initiation is the same principal that should retain cancel authority.
- Use `delayed_transfer` when protocol safety requires on-chain lead time before transfer or unwrap execution, and when initial wrapper custody should be assigned explicitly at wrap time.
If your protocol needs confirmation from the new owner but can execute the transfer immediately upon that confirmation, use `two_step_transfer`. This is the right choice when the principal initiating the transfer is a known, controlled key. For example, a multisig or a hot wallet operated by the same team that holds the capability.

If your protocol requires on-chain lead time before a capability changes hands, such as scenarios where stakeholders such as a monitoring system, or an incident response team need processing time to detect and respond accordingly, then use `delayed_transfer`. The delay is set at wrap time and cannot be shortened afterward, making it a reliable commitment visible to anyone inspecting the wrapper object.

If you are wrapping a capability in a shared-object flow where arbitrary signers can call `initiate_transfer`, read the security note below before choosing either policy.

<Callout>
For a deeper walkthrough of both policies with background on why controlled capability transfers matter on Sui, see the Access walkthrough coming soon.
</Callout>

## Security note

<Callout type="warn">
`two_step_transfer` records `ctx.sender()` as the cancel authority (`from`) when `initiate_transfer` is called. In normal single-owner usage, `ctx.sender()` is the wallet that holds the wrapper, the correct principal to hold cancel authority.

However, if `initiate_transfer` is invoked inside a shared-object executor, a module where any user can be the transaction sender, then the cancel authority becomes that arbitrary user's address, not the protocol's. A malicious user could call `initiate_transfer` targeting their own address as the recipient. They would become both the pending recipient and the only party with cancel authority, locking out the legitimate owner: no one but the attacker can cancel the transfer, and the attacker can choose to accept it at any time.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A malicious user could call initiate_transfer targeting their own address as the recipient

They don't even need to target their own address, they can target the right address and still get the inner object by cancelling later, as they are the cancel authority.


This failure mode applies specifically to `two_step_transfer`, because its cancel authority is established at initiation time from the transaction signer. Avoid using it directly in shared-object executor flows unless your design explicitly maps signer identity to cancel authority.
</Callout>

## API Reference

Expand Down
43 changes: 35 additions & 8 deletions content/contracts-sui/1.x/index.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
---
title: Contracts for Sui 1.x
title: Contracts for Sui v1.0.0
---

**OpenZeppelin Contracts for Sui v1.x** ships two core packages:
**OpenZeppelin Contracts for Sui v1.0.0** is the first release of OpenZeppelin's library for the Sui ecosystem. It ships two core packages:

- `openzeppelin_math` for deterministic arithmetic, configurable rounding, and decimal scaling.
- `openzeppelin_access` for ownership-transfer wrappers around privileged `key + store` objects.

More packages and features are actively being developed and released.

## Packages

### `openzeppelin_math`

Deterministic integer arithmetic with explicit rounding, overflow-safe operations, and decimal precision conversion across multiple unsigned widths.

- [Package guide](/contracts-sui/1.x/math)
- [Learn walkthrough](TODO-AddLink)
- [API reference](/contracts-sui/1.x/api/math)

### `openzeppelin_access`

Ownership-transfer wrappers that enforce two-step confirmation or mandatory time delays before privileged capability objects change hands.

- [Package guide](/contracts-sui/1.x/access)
- [Learn walkthrough](TODO-AddLink)
- [API reference](/contracts-sui/1.x/api/access)

## Quickstart

### Prerequisites
Expand All @@ -22,26 +42,30 @@ sui move new my_sui_app
cd my_sui_app
```

This creates a standard Move package directory structure with a `Move.toml` manifest and a `sources/` directory.

### 2. Add OpenZeppelin Dependencies from MVR

MVR (Move Registry) is the on-chain package registry for the Sui ecosystem. Using an MVR dependency instead of a git dependency means your builds resolve against published, immutable bytecode. This gives you a reproducible and verifiable dependency chain.

```bash
mvr add @openzeppelin-move/access
mvr add @openzeppelin-move/integer-math
mvr add openzeppelin-move/integer-math
```

### 3. Verify `Move.toml`

`mvr add` updates `Move.toml` automatically. It should include:
`mvr add` updates `Move.toml` automatically. The `r.mvr` key is the MVR resolver. It tells the Sui CLI to look up the package by its registry name and resolve the correct on-chain address, so you do not need to hardcode a package address.

```toml
[dependencies]
openzeppelin_access = { r.mvr = "@openzeppelin-move/access" }
openzeppelin_math = { r.mvr = "@openzeppelin-move/integer-math" }
openzeppelin_math = { r.mvr = "openzeppelin-move/integer-math" }
```

### 4. Add a Minimal Module

Create `sources/quickstart.move`:
Create `sources/quickstart.move`. This module calls `mul_div` with an explicit rounding policy. The result is wrapped in `Option`. If the intermediate product overflows, `mul_div` returns `None` and you decide how to handle it at the call site.

```move
module my_sui_app::quickstart;
Expand Down Expand Up @@ -71,5 +95,8 @@ sui move test

## Next Steps

- Read package guides: [Integer Math](/contracts-sui/1.x/math), [Access](/contracts-sui/1.x/access).
- Use API docs: [Integer Math](/contracts-sui/1.x/api/math), [Access](/contracts-sui/1.x/api/access).
If your protocol performs fee calculations, token amount arithmetic, or any precision-sensitive computation, start with the [Integer Math](/contracts-sui/1.x/math) package. It makes rounding policy, overflow behavior, and decimal precision explicit rather than implicit.

If you need to transfer a privileged capability object, such as an admin cap, treasury cap, or upgrade authority, without giving the recipient instant, irreversible control, start with the [Access](/contracts-sui/1.x/access) package instead.

Both packages can be used independently or together. Full function-level references are available in the [Integer Math API](/contracts-sui/1.x/api/math) and [Access API](/contracts-sui/1.x/api/access).
36 changes: 25 additions & 11 deletions content/contracts-sui/1.x/math.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---
title: Integer Math
---

The `openzeppelin_math` package is the numeric foundation for OpenZeppelin Contracts for Sui. It provides deterministic arithmetic across unsigned integer widths, explicit rounding controls, and helpers for decimal normalization.

Use this package when your app needs arithmetic behavior that is predictable, auditable, and safe around overflow and precision boundaries. Instead of hiding rounding and truncation inside implementation details, `openzeppelin_math` makes those decisions explicit so they can be part of your protocol rules.
Expand All @@ -12,7 +11,7 @@ Add the dependency in `Move.toml`:

```toml
[dependencies]
openzeppelin_math = { r.mvr = "@openzeppelin-move/integer-math" }
openzeppelin_math = { r.mvr = "@pkg/openzeppelin_math" }
```

Import the modules you need:
Expand All @@ -21,37 +20,46 @@ Import the modules you need:
use openzeppelin_math::{rounding, u64};
```

## What's included

- **`rounding`** — Shared rounding policy (`Down`, `Up`, `Nearest`) passed explicitly to arithmetic functions that need to decide how to handle fractional results. Centralizing this as a named value means the rounding direction appears in code as a reviewable protocol decision, not a hidden truncation.
- **`u8`, `u16`, `u32`, `u64`, `u128`, `u256`** — Uniform arithmetic API across all unsigned widths: `mul_div`, `sqrt`, `log2`, `log10`, `average`, `inv_mod`, `mul_mod`, and shift operations. Overflow-returning variants return `Option<T>` rather than aborting.
- **`decimal_scaling`** — Safe conversion of token amounts between different decimal precisions (e.g., from a 6-decimal token to a 9-decimal internal system). Uses `u512` internally to avoid overflow during the scaling multiplication.
- **`u512`** — Wide-integer helper for intermediate calculations that would overflow `u256`. Available for high-precision arithmetic paths that carry full precision before a final truncation.

## Examples

### Fee quote with explicit rounding

`mul_div` computes `(amount * numerator) / denominator`, performing the overflow check on the intermediate product before dividing.

```move
module my_sui_app::pricing;

use openzeppelin_math::{rounding, u64};

const EMathOverflow: u64 = 0;

public fun quote_with_fee(amount: u64): u64 {
u64::mul_div(amount, 1025u64, 1000u64, rounding::nearest())
.destroy_or!(abort EMathOverflow)
}
```

`rounding::nearest()` rounds to the nearest integer, breaking ties toward even. Using `nearest()` here means the protocol does not systematically favor the buyer or seller across a large number of small trades — that choice is intentional and visible in code. If you want fees to always round in your protocol's favor, use `rounding::up()` instead.


### Square root with deterministic rounding

```move
module my_sui_app::analytics;

use openzeppelin_math::{rounding, u64};

public fun sqrt_floor(value: u64): u64 {
u64::sqrt(value, rounding::down())
}
```

### Decimal normalization between precisions

`safe_upcast_balance` converts an amount from a lower-precision representation to a higher one; `safe_downcast_balance` does the reverse, truncating any fractional remainder at the lower boundary.

```move
module my_sui_app::scaling;

Expand All @@ -68,12 +76,18 @@ public fun scale_down_9_to_6(amount: u256): u64 {
}
```

`safe_downcast_balance` discards any fractional remainder in the lower digits — if your protocol must account for that remainder rather than silently dropping it, capture it before the downcast.

<Callout>
For a deeper walkthrough of every function with background on why safe integer math matters on-chain, see the Integer Math guide — coming soon.
</Callout>

## Picking the right primitives

- `rounding`: shared rounding policy (`Down`, `Up`, `Nearest`) for value-sensitive paths.
- `u8`, `u16`, `u32`, `u64`, `u128`, `u256`: same API surface across widths for portability.
- `decimal_scaling`: decimal conversion between systems using different precision.
- `u512`: wide intermediate support for high-precision arithmetic paths.
- **`rounding`**: The rounding policy type. Pass `Down`, `Up`, or `Nearest` explicitly for value-sensitive paths. This makes the rounding direction a named, reviewable protocol decision rather than a consequence of integer truncation.
- **`u8` – `u256`**: The per-width arithmetic modules. They share the same API surface, so code that starts on `u64` can be ported to `u128` or `u256` without relearning the interface. Reach for a wider type when intermediate products risk overflowing your current width.
- **`decimal_scaling`**: Use this when crossing decimal precision boundaries. For example, bridging between a 6-decimal stablecoin and a 9-decimal internal accounting system. It handles the multiplier arithmetic and guards against intermediate overflow using `u512` internally.
- **`u512`**: Use this directly when intermediate calculations exceed the `u256` range and you need to carry the full precision through before truncating. `decimal_scaling` uses it internally, but it is also available for custom high-precision paths.

## API Reference

Expand Down
Loading