diff --git a/content/contracts-sui/1.x/access.mdx b/content/contracts-sui/1.x/access.mdx
index d2672cee..13542e74 100644
--- a/content/contracts-sui/1.x/access.mdx
+++ b/content/contracts-sui/1.x/access.mdx
@@ -2,6 +2,10 @@
title: Access
---
+
+The example code snippets used in this guide are experimental and have not been audited. They are meant to illustrate usage of the OpenZeppelin Sui Package.
+
+
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.
@@ -25,7 +29,7 @@ Import the transfer policy module you want to use:
use openzeppelin_access::two_step_transfer;
```
-## Examples
+## Quickstart example
```move
module my_sui_app::admin;
@@ -47,9 +51,306 @@ public fun wrap_admin_cap(
## 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.
+**Use `two_step_transfer` when:**
+
+- The transfer can execute immediately once confirmed.
+- The principal initiating the transfer is a known, controlled key.
+- The risk you are guarding against is human error (wrong or non-existent address), not timing.
+
+**Use `delayed_transfer` when:**
+
+- Your protocol requires on-chain lead time before authority changes.
+- Users, DAOs, or monitoring systems need a window to detect and respond.
+- The delay should be a reliable, inspectable commitment visible to anyone.
+
+**Combining both:** the modules accept any `T: key + store`, so they compose. You could wrap a capability in `delayed_transfer` for the timing guarantee and use a `two_step_transfer` flow at the scheduling step for address-confirmation safety.
+
+---
+
+## Why controlled transfers matter
+
+On Sui, `sui::transfer::transfer` is instant and irreversible. There is no confirmation step, no waiting period, and no cancel mechanism. For everyday objects this is fine. For privileged capability objects, such as admin caps, treasury caps, or upgrade authorities, a single mistaken or malicious transfer permanently moves control with no recourse.
+
+The `openzeppelin_access` package adds two transfer policies that sit between you and that irreversible `transfer` call:
+
+| Module | What it enforces | Analogy from Solidity |
+| --- | --- | --- |
+| `two_step_transfer` | Recipient must explicitly accept before the transfer completes | `Ownable2Step` |
+| `delayed_transfer` | Mandatory time delay before execution; anyone can observe the pending action | `TimelockController` on ownership |
+
+If you already know which policy you need, jump directly to [two\_step\_transfer](#two_step_transfer) or [delayed\_transfer](#delayed_transfer).
+
+## Wrapping and transfer policies
+
+Both modules use the same underlying mechanism: wrapping a capability inside a new object that enforces a transfer policy on it.
+
+When you call `wrap`, the capability is stored as a dynamic object field inside the wrapper. This means:
+
+- **The wrapper becomes the custody object.** You hold the wrapper, not the capability directly. To transfer or recover the capability, you go through the wrapper's policy.
+- **The underlying capability retains its on-chain ID.** Off-chain indexers and explorers can still discover and track it via the dynamic object field. Wrapping does not make the capability invisible.
+- **The wrapper intentionally omits the `store` ability.** Without `store`, the wrapper cannot be moved via `transfer::public_transfer`. Only the module's own functions (which use the privileged `transfer::transfer` internally) can move it. This is a deliberate design choice that prevents accidental transfers outside the policy.
+
+A `WrapExecuted` event is emitted when a capability is wrapped, creating an on-chain record of when the policy was applied.
+
+### Borrowing without unwrapping
+
+Both modules provide three ways to use the wrapped capability without changing ownership:
+
+**Immutable borrow** for read-only access:
+
+```move
+let cap_ref = wrapper.borrow(); // &AdminCap
+```
+
+**Mutable borrow** for updating the capability's internal state:
+
+```move
+let cap_mut = wrapper.borrow_mut(); // &mut AdminCap
+```
+
+**Temporary move** for functions that require the capability by value. This uses the hot potato pattern: `borrow_val` returns a `Borrow` struct with no abilities (`copy`, `drop`, `store`, `key` are all absent). The Move compiler enforces that it must be consumed by `return_val` before the transaction ends.
+
+```move
+let (cap, borrow_token) = wrapper.borrow_val();
+
+/// Use cap in functions that require it by value.
+wrapper.return_val(cap, borrow_token); // compiler enforces this call
+```
+
+If you try to drop the borrow token, return a different capability, or return it to the wrong wrapper, the transaction either won't compile or will abort at runtime.
+
+## `two_step_transfer`
+
+A transfer policy that requires the designated recipient to explicitly accept before the wrapper changes hands. The initiator retains cancel authority until acceptance. There is no time delay. The transfer executes immediately once the recipient accepts.
+
+This is the right choice when the principal initiating the transfer is a known, controlled key (a multisig, a hot wallet operated by the same team) and the risk you are guarding against is sending the capability to a wrong or non-existent address, which would permanently lock the capability with no way to recover it.
+
+### Step 1: Wrap the capability
+
+```move
+module my_sui_app::admin;
+
+use openzeppelin_access::two_step_transfer;
+
+public struct AdminCap has key, store { id: UID }
+
+/// Wrap and immediately initiate a transfer to `new_admin`.
+/// The wrapper is consumed by `initiate_transfer` and held
+/// inside the shared `PendingOwnershipTransfer` until the
+/// recipient accepts or the initiator cancels.
+public fun wrap_and_transfer(cap: AdminCap, new_admin: address, ctx: &mut TxContext) {
+ let wrapper = two_step_transfer::wrap(cap, ctx);
+ // Emits WrapExecuted
+
+ wrapper.initiate_transfer(new_admin, ctx);
+ // Emits TransferInitiated
+}
+```
+
+`wrap` stores the `AdminCap` inside a `TwoStepTransferWrapper` and emits a `WrapExecuted` event. Because the wrapper lacks the `store` ability, it cannot be sent via `transfer::public_transfer`. The intended next step is to call `initiate_transfer`, which consumes the wrapper and creates a shared `PendingOwnershipTransfer` object that both parties can interact with.
+
+### Step 2: Initiate a transfer
+
+`initiate_transfer` consumes the wrapper by value and creates a shared `PendingOwnershipTransfer` object. The sender's address is recorded as `from` (the cancel authority), and the recipient's address is recorded as `to`.
+
+```move
+/// Called by the current wrapper owner. Consumes the wrapper.
+wrapper.initiate_transfer(new_admin_address, ctx);
+/// Emits TransferInitiated { wrapper_id, from, to }
+```
+
+After this call, the wrapper is held inside the pending request via transfer-to-object. The original owner no longer has it in their scope, but they retain the ability to cancel because their address is recorded as `from`.
+
+The `TransferInitiated` event contains the pending request's ID and the wrapper ID, allowing off-chain indexers to discover the shared `PendingOwnershipTransfer` object for the next step.
+
+### Step 3: Recipient accepts (or initiator cancels)
+
+The designated recipient calls `accept_transfer` to complete the handoff. This step uses Sui's [transfer-to-object (TTO)](https://docs.sui.io/guides/developer/objects/transfers/transfer-to-object) pattern: the wrapper was transferred to the `PendingOwnershipTransfer` object in Step 2, so the recipient must provide a `Receiving>` ticket to claim it. The `Receiving` type is Sui's mechanism for retrieving objects that were sent to another object rather than to a wallet.
+
+```move
+/// Called by the address recorded as `to` (new_admin_address).
+/// `request` is the shared PendingOwnershipTransfer object.
+/// `wrapper_ticket` is the Receiving> for the wrapper
+/// that was transferred to the request object.
+two_step_transfer::accept_transfer(request, wrapper_ticket, ctx);
+// Emits TransferAccepted { wrapper_id, from, to }
+```
+
+If the initiator changes their mind before the recipient accepts, they can cancel. The cancel call also requires the `Receiving` ticket for the wrapper:
+
+```move
+/// Called by the address recorded as `from` (the original initiator).
+two_step_transfer::cancel_transfer(request, wrapper_ticket, ctx);
+/// Wrapper is returned to the `from` address.
+```
+
+### Unwrapping
+
+To permanently reclaim the raw capability and destroy the wrapper:
+
+```move
+let admin_cap = wrapper.unwrap(ctx);
+```
+
+This bypasses the transfer flow entirely. Only the current wrapper owner can call it.
+
+### Security note on shared-object flows
+
+`initiate_transfer` records `ctx.sender()` as the cancel authority. In normal single-owner usage, this is the wallet holding the wrapper. However, if `initiate_transfer` is called inside a shared-object executor where any user can be the transaction sender, a malicious user could call `initiate_transfer` targeting their own address as recipient. They would become both the pending recipient and the sole cancel authority, locking out the legitimate owner.
+
+Avoid using `two_step_transfer` in shared-object executor flows unless your design explicitly maps signer identity to cancel authority.
+
+## `delayed_transfer`
+
+A transfer policy that enforces a configurable minimum delay between scheduling and executing a transfer. The delay is set at wrap time and cannot be changed afterward. This creates a publicly visible window before any authority change takes effect, giving monitoring systems, DAOs, and individual users time to detect and respond.
+
+This is the right choice when your protocol requires on-chain lead time before a capability changes hands, for example, to allow an incident response process to detect a compromised key, or to give depositors time to exit before governance parameters change.
+
+### Step 1: Wrap with a delay
+
+```move
+module my_sui_app::treasury;
+
+use openzeppelin_access::delayed_transfer;
+
+public struct TreasuryCap has key, store { id: UID }
+
+const MIN_DELAY_MS: u64 = 86_400_000; // 24 hours
+
+/// Creates the wrapper and transfers it to ctx.sender() internally
+public fun wrap_treasury_cap(cap: TreasuryCap, ctx: &mut TxContext) {
+ delayed_transfer::wrap(cap, MIN_DELAY_MS, ctx.sender(), ctx);
+}
+```
+
+`wrap` creates a `DelayedTransferWrapper`, stores the capability inside it as a dynamic object field, and transfers the wrapper to the specified `recipient` (here, the caller). A `WrapExecuted` event is emitted. Unlike `two_step_transfer::wrap` which returns the wrapper, `delayed_transfer::wrap` handles the transfer internally and has no return value.
+
+### Step 2: Schedule a transfer
+
+```move
+/// Called by the current wrapper owner.
+wrapper.schedule_transfer(new_owner_address, &clock, ctx);
+/// Emits TransferScheduled with execute_after_ms = clock.timestamp_ms() + min_delay_ms
+```
+
+The `Clock` object is Sui's shared on-chain clock. The deadline is computed as `clock.timestamp_ms() + min_delay_ms` and stored in the wrapper. Only one action can be pending at a time; scheduling a second without canceling the first aborts with `ETransferAlreadyScheduled`.
+
+During the delay window, the `TransferScheduled` event is visible on-chain. Monitoring systems, governance dashboards, or individual users watching the chain can detect the pending transfer and take action (e.g., withdrawing funds from the protocol) before it executes.
+
+
+The `recipient` in `schedule_transfer` must be a wallet address, not an object ID. If the wrapper is transferred to an object via transfer-to-object (TTO), both the wrapper and the capability inside it become permanently locked. The `delayed_transfer` module does not implement a `Receiving`-based retrieval mechanism, so there is no way to borrow, unwrap, or further transfer a wrapper that has been sent to an object. Always verify that the scheduled recipient is an address controlled by a keypair.
+
+
+### Step 3: Wait, then execute
+
+```move
+/// Callable after the delay window has passed.
+wrapper.execute_transfer(&clock, ctx);
+/// Emits OwnershipTransferred. Consumes the wrapper and delivers it to the recipient.
+```
+
+`execute_transfer` consumes the wrapper by value. After this call, the wrapper has been transferred to the scheduled recipient and no longer exists in the caller's scope. Calling it before `execute_after_ms` aborts with `EDelayNotElapsed`.
+
+### Scheduling an unwrap
+
+The same delay enforcement applies to recovering the raw capability:
+
+```move
+/// Schedule the unwrap
+wrapper.schedule_unwrap(&clock, ctx);
+/// Emits UnwrapScheduled
+
+/// After the delay has elapsed, executes the unwrap: Emits UnwrapExecuted, wrapper is consumed, and capability is returned.
+let treasury_cap = wrapper.unwrap(&clock, ctx);
+```
+
+### Canceling
+
+The owner can cancel a pending action at any time before execution:
+
+```move
+wrapper.cancel_schedule();
+```
+
+This clears the pending slot immediately, allowing a new action to be scheduled.
+
+## Putting it together
+
+Here is a protocol example that uses `delayed_transfer` to wrap its admin capability, ensuring any ownership change is visible on-chain for 24 hours before it takes effect:
+
+```move
+module my_sui_app::governed_protocol;
+
+use openzeppelin_access::delayed_transfer::{Self, DelayedTransferWrapper};
+use openzeppelin_math::rounding;
+use openzeppelin_math::u64 as math_u64;
+use sui::clock::Clock;
+
+const MIN_DELAY_MS: u64 = 86_400_000; // 24 hours
+const EMathOverflow: u64 = 0;
+
+public struct ProtocolAdmin has key, store {
+ id: UID,
+ fee_bps: u64,
+}
+
+/// Initialize: create the admin cap and wrap it with a 24-hour transfer delay.
+/// `delayed_transfer::wrap` transfers the wrapper to the deployer internally.
+fun init(ctx: &mut TxContext) {
+ let admin = ProtocolAdmin {
+ id: object::new(ctx),
+ fee_bps: 30, // 0.3%
+ };
+ delayed_transfer::wrap(admin, MIN_DELAY_MS, ctx.sender(), ctx);
+}
+
+/// Update the fee rate. Borrows the admin cap mutably without changing ownership.
+public fun update_fee(
+ wrapper: &mut DelayedTransferWrapper,
+ new_fee_bps: u64,
+) {
+ let admin = delayed_transfer::borrow_mut(wrapper);
+ admin.fee_bps = new_fee_bps;
+}
+
+/// Compute a fee using the admin-configured rate and safe math.
+public fun compute_fee(
+ wrapper: &DelayedTransferWrapper,
+ amount: u64,
+): u64 {
+ let admin = delayed_transfer::borrow(wrapper);
+ math_u64::mul_div(amount, admin.fee_bps, 10_000, rounding::up())
+ .destroy_or!(abort EMathOverflow)
+}
+
+/// Schedule a transfer to a new admin. Visible on-chain for 24 hours.
+public fun schedule_admin_transfer(
+ wrapper: &mut DelayedTransferWrapper,
+ new_admin: address,
+ clock: &Clock,
+ ctx: &mut TxContext,
+) {
+ wrapper.schedule_transfer(new_admin, clock, ctx);
+}
+```
+
+This module combines both packages: `openzeppelin_math` for the fee calculation (explicit rounding, overflow handling) and `openzeppelin_access` for the ownership policy (24-hour delay, on-chain observability). Users monitoring the chain see the `TransferScheduled` event and can exit before a new admin takes over.
+
+Build and test:
+
+```bash
+sui move build
+sui move test
+```
## API Reference
-Use the full function-level reference here: [Access API](/contracts-sui/1.x/api/access).
+For function-level signatures and parameters, see the [Access API reference](/contracts-sui/1.x/api/access).
+
+## Next steps
+
+- [Access API reference](/contracts-sui/1.x/api/access) for full function signatures and error codes
+- [Integer Math](/contracts-sui/1.x/math) for safe arithmetic primitives used in protocol math
+- [Fixed-Point Math](/contracts-sui/1.x/fixed-point) for fractional values, prices, and signed deltas
+- [GitHub issue tracker](https://github.com/OpenZeppelin/contracts-sui/issues) to report bugs or request features
+- [Sui Discord](https://discord.gg/sui) and [Sui Developer Forum](https://forums.sui.io/) to connect with other builders
diff --git a/content/contracts-sui/1.x/api/access.mdx b/content/contracts-sui/1.x/api/access.mdx
index fe819308..da80b6aa 100644
--- a/content/contracts-sui/1.x/api/access.mdx
+++ b/content/contracts-sui/1.x/api/access.mdx
@@ -8,7 +8,7 @@ This page documents the public API of `openzeppelin_access` for OpenZeppelin Con
```move
@@ -307,7 +307,7 @@ Raised when caller is not the designated prospective owner in `accept_transfer`.
```move
diff --git a/content/contracts-sui/1.x/api/fixed-point.mdx b/content/contracts-sui/1.x/api/fixed-point.mdx
new file mode 100644
index 00000000..7f136f32
--- /dev/null
+++ b/content/contracts-sui/1.x/api/fixed-point.mdx
@@ -0,0 +1,1119 @@
+---
+title: Fixed-Point Math API Reference
+---
+
+This page documents the public API of `openzeppelin_fp_math` for OpenZeppelin Contracts for Sui `v1.x`.
+
+The package provides two decimal fixed-point types with 9 decimals (matching Sui coin precision):
+
+- `UD30x9`: unsigned, backed by `u128`.
+- `SD29x9`: signed (two's complement), backed by `u128`.
+
+Each type has a base module that holds the arithmetic, comparison, and bitwise functions, and a conversion module that scales whole integers in and out of fixed-point form.
+
+### `ud30x9` [toc] [#ud30x9]
+
+
+
+```move
+use openzeppelin_fp_math::ud30x9;
+```
+
+Defines the `UD30x9` unsigned decimal fixed-point type. Values represent unsigned real numbers as a `u128` scaled by `10^9`. The 9-decimal scale matches Sui coin precision and keeps offchain reasoning straightforward (`1.0` is `1_000_000_000`).
+
+This module owns the type, raw casting helpers (`wrap` / `unwrap`), and constant constructors. Arithmetic, comparison, and bitwise operations live in [`ud30x9_base`](#ud30x9_base). Whole-integer conversions live in [`ud30x9_convert`](#ud30x9_convert).
+
+Types
+
+- [`UD30x9`](#ud30x9-UD30x9)
+
+Functions
+
+- [`zero()`](#ud30x9-zero)
+- [`one()`](#ud30x9-one)
+- [`max()`](#ud30x9-max)
+- [`wrap(x)`](#ud30x9-wrap)
+- [`unwrap(x)`](#ud30x9-unwrap)
+
+#### Types [!toc] [#ud30x9-Types]
+
+
+Unsigned decimal fixed-point type. The single `u128` field stores the raw bits of the value scaled by `10^9`.
+
+
+#### Functions [!toc] [#ud30x9-Functions]
+
+
+Returns the `UD30x9` representation of `0`.
+
+
+
+Returns the `UD30x9` representation of `1.0` (raw bits `10^9`).
+
+
+
+Returns the largest representable `UD30x9` value (raw bits `u128::MAX`).
+
+
+
+Wraps raw `u128` bits into a `UD30x9` value without scaling. The input is interpreted as fixed-point bits already scaled by `10^9`.
+
+INFO: Use [`ud30x9_convert::from_u64`](#ud30x9_convert-from_u64) / [`from_u128`](#ud30x9_convert-from_u128) when the input is a whole integer that you mean as `x.0`.
+
+
+
+Returns the raw `u128` bits of a `UD30x9` value.
+
+
+### `ud30x9_base` [toc] [#ud30x9_base]
+
+
+
+```move
+use openzeppelin_fp_math::ud30x9_base;
+```
+
+Arithmetic, comparison, and bitwise functions for `UD30x9`. All entries are exported as method-style calls on `UD30x9` via `public use fun` declarations in `ud30x9`, so `x.add(y)` and `ud30x9_base::add(x, y)` are equivalent.
+
+Functions
+
+- Arithmetic: [`add`](#ud30x9_base-add), [`sub`](#ud30x9_base-sub), [`mul`](#ud30x9_base-mul), [`mul_trunc`](#ud30x9_base-mul_trunc), [`mul_away`](#ud30x9_base-mul_away), [`div`](#ud30x9_base-div), [`div_trunc`](#ud30x9_base-div_trunc), [`div_away`](#ud30x9_base-div_away), [`mod`](#ud30x9_base-mod), [`pow`](#ud30x9_base-pow), [`abs`](#ud30x9_base-abs), [`ceil`](#ud30x9_base-ceil), [`floor`](#ud30x9_base-floor), [`unchecked_add`](#ud30x9_base-unchecked_add), [`unchecked_sub`](#ud30x9_base-unchecked_sub)
+- Comparison: [`eq`](#ud30x9_base-eq), [`neq`](#ud30x9_base-neq), [`gt`](#ud30x9_base-gt), [`gte`](#ud30x9_base-gte), [`lt`](#ud30x9_base-lt), [`lte`](#ud30x9_base-lte), [`is_zero`](#ud30x9_base-is_zero)
+- Bitwise: [`and`](#ud30x9_base-and), [`and2`](#ud30x9_base-and2), [`or`](#ud30x9_base-or), [`xor`](#ud30x9_base-xor), [`not`](#ud30x9_base-not), [`lshift`](#ud30x9_base-lshift), [`unchecked_lshift`](#ud30x9_base-unchecked_lshift), [`rshift`](#ud30x9_base-rshift), [`unchecked_rshift`](#ud30x9_base-unchecked_rshift)
+- Cross-type casts: [`into_SD29x9`](#ud30x9_base-into_SD29x9), [`try_into_SD29x9`](#ud30x9_base-try_into_SD29x9)
+
+Errors
+
+- [`EOverflow`](#ud30x9_base-EOverflow)
+- [`EUnderflow`](#ud30x9_base-EUnderflow)
+- [`EDivideByZero`](#ud30x9_base-EDivideByZero)
+- [`ECannotBeConvertedToSD29x9`](#ud30x9_base-ECannotBeConvertedToSD29x9)
+- [`EInvalidShiftSize`](#ud30x9_base-EInvalidShiftSize)
+
+#### Functions [!toc] [#ud30x9_base-Functions]
+
+
+Returns the sum `x + y`.
+
+Aborts with `EOverflow` if the sum exceeds the representable `UD30x9` range.
+
+
+
+Returns the difference `x - y`.
+
+Aborts with `EUnderflow` if `y > x` (the result would be negative, which is unrepresentable in `UD30x9`).
+
+
+
+Returns the product `x * y`, rounded toward zero. Equivalent to [`mul_trunc`](#ud30x9_base-mul_trunc).
+
+Aborts with `EOverflow` if the result exceeds the representable `UD30x9` range.
+
+
+
+Returns the product `x * y`, rounded toward zero (truncating the fractional part).
+
+Aborts with `EOverflow` if the result exceeds the representable `UD30x9` range.
+
+
+
+Returns the product `x * y`, rounded away from zero when inexact.
+
+Aborts with `EOverflow` if the rounded result exceeds the representable `UD30x9` range.
+
+
+
+Returns the quotient `x / y`, rounded toward zero. Equivalent to [`div_trunc`](#ud30x9_base-div_trunc).
+
+Aborts with `EDivideByZero` if `y` is zero.
+Aborts with `EOverflow` if the result exceeds the representable `UD30x9` range.
+
+
+
+Returns the quotient `x / y`, rounded toward zero (truncating the fractional part).
+
+Aborts with `EDivideByZero` if `y` is zero.
+Aborts with `EOverflow` if the result exceeds the representable `UD30x9` range.
+
+
+
+Returns the quotient `x / y`, rounded away from zero when inexact. For example `1.0 / 3.0` returns `0.333333334`.
+
+Aborts with `EDivideByZero` if `y` is zero.
+Aborts with `EOverflow` if the rounded result exceeds the representable `UD30x9` range.
+
+
+
+Returns the remainder of `x` divided by `y`.
+
+Aborts with `EDivideByZero` if `y` is zero.
+
+
+
+Returns an approximation of `x^exp` computed using binary exponentiation with fixed-point multiplication. Each intermediate multiply or square applies fixed-point truncation.
+
+Aborts with `EOverflow` if any intermediate or the final result exceeds the representable `UD30x9` range.
+
+NOTE: Because truncation is applied at intermediate steps, `pow` is approximate for most fractional values. Rounding error compounds as `exp` grows; results are biased toward zero; for `0 < x < 1` intermediate values can reach zero before the final mathematically scaled result would. Fixed-point multiplication is not associative under truncation, so the grouping used by binary exponentiation can affect the value.
+
+
+
+Returns `x` unchanged. `UD30x9` is unsigned, so the absolute value is identity. Provided for symmetry with `SD29x9::abs`.
+
+
+
+Rounds toward positive infinity to the next integer multiple of `10^9`. Returns `x` if it is already a whole multiple.
+
+Aborts with `EOverflow` if the rounded result exceeds the representable `UD30x9` range.
+
+
+
+Rounds toward zero to the nearest integer multiple of `10^9`.
+
+
+
+Returns the wrapping sum `x + y` modulo `2^128`. Does not abort on overflow.
+
+
+
+Returns the wrapping difference `x - y` modulo `2^128`. Does not abort on underflow.
+
+
+
+Returns `true` if `x == y`.
+
+
+
+Returns `true` if `x != y`.
+
+
+
+Returns `true` if `x > y`.
+
+
+
+Returns `true` if `x >= y`.
+
+
+
+Returns `true` if `x < y`.
+
+
+
+Returns `true` if `x <= y`.
+
+
+
+Returns `true` if `x` is exactly zero.
+
+
+
+Returns the bitwise AND of `x`'s raw bits and the `u128` mask `bits`.
+
+
+
+Returns the bitwise AND of two `UD30x9` raw bit patterns.
+
+
+
+Returns the bitwise OR of two `UD30x9` raw bit patterns.
+
+
+
+Returns the bitwise XOR of two `UD30x9` raw bit patterns.
+
+
+
+Returns the bitwise NOT of `x`'s raw bits.
+
+
+
+Logical left shift on the underlying 128-bit representation by `bits` positions.
+
+Aborts with `EInvalidShiftSize` if `bits >= 128`.
+Aborts with `EOverflow` if the shift would consume non-zero high bits.
+
+
+
+Logical left shift that truncates high bits past the 128-bit boundary and returns zero when `bits >= 128`.
+
+NOTE: Use [`lshift`](#ud30x9_base-lshift) when you want shift-size and overflow checks to abort.
+
+
+
+Logical right shift on the underlying 128-bit representation by `bits` positions.
+
+Aborts with `EInvalidShiftSize` if `bits >= 128`.
+
+
+
+Logical right shift that returns zero when `bits >= 128`. Vacated high bits are filled with zeros.
+
+
+
+Casts a `UD30x9` value to `SD29x9` while preserving the scaled numeric meaning.
+
+Aborts with `ECannotBeConvertedToSD29x9` if `x` exceeds the maximum positive `SD29x9` magnitude (`2^127 - 1`).
+
+
+
+Returns `some(SD29x9)` when the cast fits in the positive `SD29x9` range, otherwise `none`.
+
+
+#### Errors [!toc] [#ud30x9_base-Errors]
+
+
+Raised when an arithmetic or shift result exceeds the representable `UD30x9` range.
+
+
+
+Raised when subtraction would produce a negative result, which is unrepresentable in `UD30x9`.
+
+
+
+Raised when a division or modulo operation receives a zero divisor.
+
+
+
+Raised when [`into_SD29x9`](#ud30x9_base-into_SD29x9) is called on a value above the `SD29x9` positive range.
+
+
+
+Raised when [`lshift`](#ud30x9_base-lshift) or [`rshift`](#ud30x9_base-rshift) is called with `bits >= 128`.
+
+
+### `ud30x9_convert` [toc] [#ud30x9_convert]
+
+
+
+```move
+use openzeppelin_fp_math::ud30x9_convert;
+```
+
+Scale-aware conversions between whole unsigned integers and `UD30x9`. These functions apply or remove the `10^9` scale factor, so use them when your input or output represents a whole-integer quantity (e.g. `42` as `42.0`).
+
+For raw, non-scaling casts, use [`ud30x9::wrap`](#ud30x9-wrap) and [`ud30x9::unwrap`](#ud30x9-unwrap) instead.
+
+Functions
+
+- [`from_u64(x)`](#ud30x9_convert-from_u64)
+- [`from_u128(x)`](#ud30x9_convert-from_u128)
+- [`try_from_u128(x)`](#ud30x9_convert-try_from_u128)
+- [`to_u128_trunc(x)`](#ud30x9_convert-to_u128_trunc)
+- [`to_u64_trunc(x)`](#ud30x9_convert-to_u64_trunc)
+- [`try_to_u64_trunc(x)`](#ud30x9_convert-try_to_u64_trunc)
+
+Errors
+
+- [`EOverflow`](#ud30x9_convert-EOverflow)
+- [`EIntegerOverflow`](#ud30x9_convert-EIntegerOverflow)
+
+#### Functions [!toc] [#ud30x9_convert-Functions]
+
+
+Converts a whole `u64` integer into `UD30x9` by multiplying it by `10^9`. Cannot overflow because `u64::MAX * 10^9` fits in `u128`.
+
+
+
+Converts a whole `u128` integer into `UD30x9` by multiplying it by `10^9`.
+
+Aborts with `EOverflow` if `x * 10^9` would overflow the `UD30x9` raw representation.
+
+
+
+Returns `some(UD30x9)` when `x * 10^9` fits in the raw representation, otherwise `none`.
+
+
+
+Returns the whole-number portion of `x`, computed as `floor(x)`.
+
+
+
+Returns the truncated whole-number portion of `x` as `u64`.
+
+Aborts with `EIntegerOverflow` if the truncated value exceeds `u64::MAX`.
+
+
+
+Returns `some(u64)` when the truncated whole-number portion fits in `u64`, otherwise `none`.
+
+
+#### Errors [!toc] [#ud30x9_convert-Errors]
+
+
+Raised when a whole integer cannot be scaled into the `UD30x9` raw representation.
+
+
+
+Raised when a truncated whole part does not fit in `u64`.
+
+
+### `sd29x9` [toc] [#sd29x9]
+
+
+
+```move
+use openzeppelin_fp_math::sd29x9;
+```
+
+Defines the `SD29x9` signed decimal fixed-point type. Values represent signed real numbers as a two's complement `u128` scaled by `10^9`. Useful for balance deltas and any quantity that can dip below zero.
+
+This module owns the type, raw casting helpers (`wrap` / `unwrap`), and constant constructors (`zero`, `one`, `min`, `max`). Arithmetic, comparison, and cross-type casts live in [`sd29x9_base`](#sd29x9_base). Whole-integer conversions live in [`sd29x9_convert`](#sd29x9_convert).
+
+Types
+
+- [`SD29x9`](#sd29x9-SD29x9)
+
+Functions
+
+- [`zero()`](#sd29x9-zero)
+- [`one()`](#sd29x9-one)
+- [`min()`](#sd29x9-min)
+- [`max()`](#sd29x9-max)
+- [`wrap(x, is_negative)`](#sd29x9-wrap)
+- [`unwrap(x)`](#sd29x9-unwrap)
+
+Errors
+
+- [`EOverflow`](#sd29x9-EOverflow)
+
+#### Types [!toc] [#sd29x9-Types]
+
+
+Signed decimal fixed-point type. The single `u128` field stores a two's complement representation scaled by `10^9`.
+
+
+#### Functions [!toc] [#sd29x9-Functions]
+
+
+Returns the `SD29x9` representation of `0`.
+
+
+
+Returns the `SD29x9` representation of `1.0` (raw bits `10^9`).
+
+
+
+Returns the smallest representable `SD29x9` value, namely `-2^127`.
+
+NOTE: `+2^127` is not representable, so several arithmetic operations (such as `abs` and `negate`) abort when called on `min`.
+
+
+
+Returns the largest representable `SD29x9` value, namely `2^127 - 1`.
+
+
+
+Wraps a `u128` magnitude plus a sign flag into a raw `SD29x9` value. The input must be a pure magnitude and must not already include a sign bit. When `is_negative` is `true`, the value is converted to its two's complement form.
+
+Aborts with `EOverflow` if `x` exceeds the maximum positive magnitude (`2^127 - 1`).
+
+NOTE: Cannot construct the minimum value. Use [`min`](#sd29x9-min) for that.
+
+
+
+Returns the raw `u128` two's complement bits of an `SD29x9` value.
+
+
+#### Errors [!toc] [#sd29x9-Errors]
+
+
+Raised when [`wrap`](#sd29x9-wrap) is called with a magnitude that exceeds `2^127 - 1`.
+
+
+### `sd29x9_base` [toc] [#sd29x9_base]
+
+
+
+```move
+use openzeppelin_fp_math::sd29x9_base;
+```
+
+Arithmetic, comparison, and cross-type casts for `SD29x9`. All entries are exported as method-style calls on `SD29x9` via `public use fun` declarations in `sd29x9`, so `x.add(y)` and `sd29x9_base::add(x, y)` are equivalent.
+
+Types
+
+- [`Components`](#sd29x9_base-Components)
+
+Functions
+
+- Arithmetic: [`add`](#sd29x9_base-add), [`sub`](#sd29x9_base-sub), [`mul`](#sd29x9_base-mul), [`mul_trunc`](#sd29x9_base-mul_trunc), [`mul_away`](#sd29x9_base-mul_away), [`div`](#sd29x9_base-div), [`div_trunc`](#sd29x9_base-div_trunc), [`div_away`](#sd29x9_base-div_away), [`mod`](#sd29x9_base-mod), [`rem`](#sd29x9_base-rem), [`pow`](#sd29x9_base-pow), [`negate`](#sd29x9_base-negate), [`abs`](#sd29x9_base-abs), [`ceil`](#sd29x9_base-ceil), [`floor`](#sd29x9_base-floor), [`unchecked_add`](#sd29x9_base-unchecked_add), [`unchecked_sub`](#sd29x9_base-unchecked_sub)
+- Comparison: [`eq`](#sd29x9_base-eq), [`neq`](#sd29x9_base-neq), [`gt`](#sd29x9_base-gt), [`gte`](#sd29x9_base-gte), [`lt`](#sd29x9_base-lt), [`lte`](#sd29x9_base-lte), [`is_zero`](#sd29x9_base-is_zero)
+- Cross-type casts: [`into_UD30x9`](#sd29x9_base-into_UD30x9), [`try_into_UD30x9`](#sd29x9_base-try_into_UD30x9)
+
+Errors
+
+- [`EOverflow`](#sd29x9_base-EOverflow)
+- [`EDivideByZero`](#sd29x9_base-EDivideByZero)
+- [`ECannotBeConvertedToUD30x9`](#sd29x9_base-ECannotBeConvertedToUD30x9)
+
+#### Types [!toc] [#sd29x9_base-Types]
+
+
+Signed decomposition of an `SD29x9` value into a sign flag and a non-negative magnitude as `u256`. Returned by internal helpers; surfaced publicly so downstream tooling can pattern-match on it.
+
+
+#### Functions [!toc] [#sd29x9_base-Functions]
+
+
+Returns the sum `x + y`.
+
+Aborts with `EOverflow` if the resulting magnitude exceeds the representable `SD29x9` range.
+
+
+
+Returns the difference `x - y`.
+
+Aborts with `EOverflow` if the resulting magnitude exceeds the representable `SD29x9` range.
+
+
+
+Returns the product `x * y`, rounded toward zero. Equivalent to [`mul_trunc`](#sd29x9_base-mul_trunc).
+
+Aborts with `EOverflow` if the result exceeds the representable `SD29x9` range.
+
+
+
+Returns the product `x * y`, rounded toward zero (truncating).
+
+Aborts with `EOverflow` if the result exceeds the representable `SD29x9` range.
+
+
+
+Returns the product `x * y`, rounded away from zero when inexact. For example `1.000000001 * 1.000000001` returns `1.000000003`, and `-1.000000001 * 1.000000001` returns `-1.000000003`.
+
+Aborts with `EOverflow` if the rounded magnitude exceeds the representable `SD29x9` range.
+
+
+
+Returns the quotient `x / y`, rounded toward zero. Equivalent to [`div_trunc`](#sd29x9_base-div_trunc).
+
+Aborts with `EDivideByZero` if `y` is zero.
+Aborts with `EOverflow` if the result exceeds the representable `SD29x9` range.
+
+
+
+Returns the quotient `x / y`, rounded toward zero (truncating).
+
+Aborts with `EDivideByZero` if `y` is zero.
+Aborts with `EOverflow` if the result exceeds the representable `SD29x9` range.
+
+
+
+Returns the quotient `x / y`, rounded away from zero when inexact. For example `1.0 / 3.0` returns `0.333333334`, and `-1.0 / 3.0` returns `-0.333333334`.
+
+Aborts with `EDivideByZero` if `y` is zero.
+Aborts with `EOverflow` if the rounded magnitude exceeds the representable `SD29x9` range.
+
+
+
+Returns the truncating remainder of `x` divided by `y`. The magnitude is `abs(x) % abs(y)` and the sign of the result follows the dividend `x`.
+
+Aborts with `EDivideByZero` if `y` is zero.
+
+NOTE: `rem` follows truncating remainder semantics. Use [`mod`](#sd29x9_base-mod) for Euclidean (always non-negative) remainder.
+
+
+
+Returns the Euclidean remainder of `x` divided by `y`. The result is always non-negative and satisfies `0 <= result < abs(y)`.
+
+Aborts with `EDivideByZero` if `y` is zero.
+
+
+
+Returns an approximation of `x^exp` computed using binary exponentiation with fixed-point multiplication. Each intermediate multiply or square applies fixed-point truncation.
+
+Aborts with `EOverflow` if any intermediate or the final result exceeds the representable `SD29x9` range.
+
+NOTE: As with `UD30x9::pow`, results are biased toward zero, error compounds with `exp`, and binary-exponentiation grouping affects the value because fixed-point multiplication is not associative under truncation.
+
+
+
+Returns `-x`.
+
+Aborts with `EOverflow` if `x` is the minimum representable value (`-2^127`), because `+2^127` is not representable.
+
+
+
+Returns the absolute value of `x`.
+
+Aborts with `EOverflow` if `x` is the minimum representable value (`-2^127`).
+
+
+
+Rounds toward positive infinity to the nearest integer multiple of `10^9`.
+
+Aborts with `EOverflow` if the rounded result exceeds the representable `SD29x9` range.
+
+
+
+Rounds toward negative infinity to the nearest integer multiple of `10^9`.
+
+Aborts with `EOverflow` if the rounded negative result magnitude exceeds the representable `SD29x9` range.
+
+
+
+Returns the wrapping sum of the raw bit patterns modulo `2^128`. Does not abort on overflow.
+
+
+
+Returns the wrapping difference of the raw bit patterns modulo `2^128`. Does not abort on underflow.
+
+
+
+Returns `true` if `x` and `y` have identical underlying bits.
+
+
+
+Returns `true` if `x != y`.
+
+
+
+Returns `true` if `x > y` under signed comparison.
+
+
+
+Returns `true` if `x >= y` under signed comparison.
+
+
+
+Returns `true` if `x < y` under signed comparison.
+
+
+
+Returns `true` if `x <= y` under signed comparison.
+
+
+
+Returns `true` if `x` is exactly zero.
+
+
+
+Casts a non-negative `SD29x9` value to `UD30x9` while preserving the scaled numeric meaning.
+
+Aborts with `ECannotBeConvertedToUD30x9` if `x` is negative.
+
+
+
+Returns `some(UD30x9)` when `x` is non-negative, otherwise `none`.
+
+
+#### Errors [!toc] [#sd29x9_base-Errors]
+
+
+Raised when an arithmetic result exceeds the representable `SD29x9` range.
+
+
+
+Raised when a division, modulo, or remainder operation receives a zero divisor.
+
+
+
+Raised when [`into_UD30x9`](#sd29x9_base-into_UD30x9) is called on a negative value.
+
+
+### `sd29x9_convert` [toc] [#sd29x9_convert]
+
+
+
+```move
+use openzeppelin_fp_math::sd29x9_convert;
+```
+
+Scale-aware conversions between whole signed magnitudes and `SD29x9`. Inputs use an unsigned magnitude plus a sign flag because Move does not provide a native signed integer type for this package; outputs return either a single magnitude (when sign is asserted) or a `(magnitude, is_negative)` pair.
+
+For raw, non-scaling casts, use [`sd29x9::wrap`](#sd29x9-wrap) and [`sd29x9::unwrap`](#sd29x9-unwrap) instead.
+
+Functions
+
+- [`from_u64(x, is_negative)`](#sd29x9_convert-from_u64)
+- [`from_u128(x, is_negative)`](#sd29x9_convert-from_u128)
+- [`try_from_u128(x, is_negative)`](#sd29x9_convert-try_from_u128)
+- [`to_parts_trunc(x)`](#sd29x9_convert-to_parts_trunc)
+- [`to_u128_trunc(x)`](#sd29x9_convert-to_u128_trunc)
+- [`try_to_u128_trunc(x)`](#sd29x9_convert-try_to_u128_trunc)
+- [`to_u64_trunc(x)`](#sd29x9_convert-to_u64_trunc)
+- [`try_to_u64_trunc(x)`](#sd29x9_convert-try_to_u64_trunc)
+
+Errors
+
+- [`EOverflow`](#sd29x9_convert-EOverflow)
+- [`ENegativeValue`](#sd29x9_convert-ENegativeValue)
+- [`EIntegerOverflow`](#sd29x9_convert-EIntegerOverflow)
+
+#### Functions [!toc] [#sd29x9_convert-Functions]
+
+
+Converts a whole `u64` magnitude into `SD29x9` by multiplying it by `10^9` and applying the provided sign.
+
+
+
+Converts a whole `u128` magnitude into `SD29x9` by multiplying it by `10^9` and applying the provided sign.
+
+Aborts with `EOverflow` if `x * 10^9` would overflow the `SD29x9` raw representation.
+
+
+
+Returns `some(SD29x9)` when the scaled magnitude fits, otherwise `none`.
+
+
+
+Returns `(magnitude, is_negative)` where `magnitude` is the truncated whole-number portion of `abs(x)`. `is_negative` is always `false` when the magnitude is zero.
+
+
+
+Returns the truncated whole-number portion of `x` as `u128`.
+
+Aborts with `ENegativeValue` if `x` is negative.
+
+
+
+Returns `some(u128)` when `x` is non-negative, otherwise `none`.
+
+
+
+Returns the truncated whole-number portion of `x` as `u64`.
+
+Aborts with `ENegativeValue` if `x` is negative.
+Aborts with `EIntegerOverflow` if the truncated magnitude exceeds `u64::MAX`.
+
+
+
+Returns `some(u64)` when `x` is non-negative and the truncated whole-number portion fits in `u64`, otherwise `none`.
+
+
+#### Errors [!toc] [#sd29x9_convert-Errors]
+
+
+Raised when a whole signed magnitude cannot be scaled into the `SD29x9` raw representation.
+
+
+
+Raised when a negative `SD29x9` value is converted to an unsigned integer type.
+
+
+
+Raised when a truncated whole part does not fit in `u64`.
+
diff --git a/content/contracts-sui/1.x/api/math.mdx b/content/contracts-sui/1.x/api/math.mdx
index 3f9456e6..43611dd9 100644
--- a/content/contracts-sui/1.x/api/math.mdx
+++ b/content/contracts-sui/1.x/api/math.mdx
@@ -8,7 +8,7 @@ This page documents the public API of `openzeppelin_math` for OpenZeppelin Contr
```move
@@ -67,7 +67,7 @@ Returns `RoundingMode::Nearest` (round-half-up behavior).
```move
@@ -134,12 +134,12 @@ Each module wraps shared arithmetic helpers with width-specific bit limits and r
Source modules:
-- [`u8.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/math/core/sources/u8.move)
-- [`u16.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/math/core/sources/u16.move)
-- [`u32.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/math/core/sources/u32.move)
-- [`u64.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/math/core/sources/u64.move)
-- [`u128.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/math/core/sources/u128.move)
-- [`u256.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/math/core/sources/u256.move)
+- [`u8.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.1.0/math/core/sources/u8.move)
+- [`u16.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.1.0/math/core/sources/u16.move)
+- [`u32.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.1.0/math/core/sources/u32.move)
+- [`u64.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.1.0/math/core/sources/u64.move)
+- [`u128.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.1.0/math/core/sources/u128.move)
+- [`u256.move`](https://github.com/OpenZeppelin/contracts-sui/blob/v1.1.0/math/core/sources/u256.move)
Functions
@@ -156,6 +156,7 @@ Functions
- [`sqrt(value, rounding_mode)`](#width-sqrt)
- [`inv_mod(value, modulus)`](#width-inv_mod)
- [`mul_mod(a, b, modulus)`](#width-mul_mod)
+- [`is_power_of_ten(n)`](#width-is_power_of_ten)
#### Functions [!toc] [#width-Functions]
@@ -271,11 +272,19 @@ Computes `(a * b) mod modulus`.
Aborts when `modulus` is zero.
+
+Returns `true` when `n` is a power of ten within the width's range. The check is implemented as a width-specific lookup table, so it is O(1) and never aborts.
+
+
### `u512` [toc] [#u512]
```move
@@ -420,3 +429,43 @@ Raised when `div_rem_u256` is called with `divisor = 0`.
>
Raised when internal division remainder invariants are violated.
+
+### `vector` [toc] [#vector]
+
+
+
+```move
+use openzeppelin_math::vector;
+```
+
+Generic in-place sorting macros built on iterative quicksort with three-way partitioning (Dutch National Flag) and median-of-three pivot selection. The macros use an explicit stack so they work on arbitrarily large vectors without hitting Move macro recursion limits.
+
+Functions
+
+- [`quick_sort!(vec)`](#vector-quick_sort)
+- [`quick_sort_by!(vec, le)`](#vector-quick_sort_by)
+
+#### Functions [!toc] [#vector-Functions]
+
+
+Sorts an unsigned integer vector in ascending order in place. Generic over any unsigned integer type (`u8`, `u16`, `u32`, `u64`, `u128`, `u256`).
+
+NOTE: Unstable sort. Average time complexity is `O(n log n)`; theoretical worst case is `O(n^2)`, mitigated by median-of-three pivot selection and three-way partitioning.
+
+
+
+Sorts a vector in place using a caller-supplied comparison function. The comparator must implement **non-strict** ordering (`<=` for ascending, `>=` for descending). Using a strict comparator (`<` or `>`) defeats three-way partitioning and degrades performance to `O(n^2)` when duplicates are present.
+
+NOTE: Unstable sort. The same complexity caveats as [`quick_sort`](#vector-quick_sort) apply.
+
diff --git a/content/contracts-sui/1.x/fixed-point.mdx b/content/contracts-sui/1.x/fixed-point.mdx
new file mode 100644
index 00000000..6316cbdc
--- /dev/null
+++ b/content/contracts-sui/1.x/fixed-point.mdx
@@ -0,0 +1,485 @@
+---
+title: Fixed-Point Math
+---
+
+
+The example code snippets used in this guide are experimental and have not been audited. They are meant to illustrate usage of the OpenZeppelin Sui Package.
+
+
+The `openzeppelin_fp_math` package adds two decimal fixed-point types with 9 decimals of precision, matching Sui's native coin precision (`10^9`). It is the right tool whenever you need real-valued arithmetic such as prices, fees, rates, ratios, and balance deltas, without giving up determinism or onchain auditability.
+
+The package complements `openzeppelin_math`, which covers integer arithmetic. Use `openzeppelin_math` when your values are whole numbers; reach for `openzeppelin_fp_math` when fractional values are part of the protocol.
+
+## Why a 9-decimal scale
+
+- Matches Sui's native coin decimals, so converting between token amounts and fixed-point values is straightforward.
+- Decimal scale is intuitive for humans, UIs, and offchain systems. `1.5` is `1_500_000_000`, with no binary fixed-point surprises.
+- Fits in `u128`, keeping storage and arithmetic light compared to `u256`-based decimal types.
+
+## The two types
+
+- [`UD30x9`](/contracts-sui/1.x/api/fixed-point#ud30x9): unsigned. Decimal range from `0` to roughly `3.4 × 10^29`.
+- [`SD29x9`](/contracts-sui/1.x/api/fixed-point#sd29x9): signed (two's complement). Decimal range from roughly `-1.7 × 10^29` to `1.7 × 10^29`. Useful for balance deltas, signed adjustments, and any quantity that can dip below zero.
+
+The names encode the layout: `UD30x9` uses up to 30 integer digits with 9 fractional digits, `SD29x9` is the signed counterpart with one bit reserved for the sign. Both fit in 16 bytes of storage, so swapping `u128` for either type costs nothing in storage or arithmetic budget.
+
+Pick **`UD30x9`** for any quantity that cannot be negative: token balances, prices, fees, exchange rates, supply totals. Pick **`SD29x9`** when the value can dip below zero: balance deltas, signed adjustments, P&L, position sides.
+
+## Usage
+
+Add the dependency in `Move.toml`:
+
+```toml
+[dependencies]
+openzeppelin_fp_math = { r.mvr = "@openzeppelin-move/fixed-point-math" }
+```
+
+Import the modules you need:
+
+```move
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+```
+
+## Quickstart examples
+
+### Build a fixed-point value from a whole integer
+
+```move
+module my_sui_app::pricing;
+
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+
+public fun reference_price(): ud30x9::UD30x9 {
+ // 1.5 = 1.0 + 0.500000000
+ ud30x9_convert::from_u128(1).add(ud30x9::wrap(500_000_000))
+}
+```
+
+### Multiply with explicit rounding
+
+```move
+module my_sui_app::quote;
+
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+
+public fun apply_fee(amount: ud30x9::UD30x9, fee_rate: ud30x9::UD30x9): ud30x9::UD30x9 {
+ // Round away from zero so quoted fees never under-charge.
+ amount.mul_away(fee_rate)
+}
+```
+
+### Track a signed balance delta
+
+```move
+module my_sui_app::ledger;
+
+use openzeppelin_fp_math::{sd29x9, sd29x9_convert};
+
+public fun apply_adjustment(
+ balance: sd29x9::SD29x9,
+ magnitude: u128,
+ is_negative: bool,
+): sd29x9::SD29x9 {
+ let delta = sd29x9_convert::from_u128(magnitude, is_negative);
+ balance.add(delta)
+}
+```
+
+### Convert back to a whole integer
+
+```move
+module my_sui_app::settlement;
+
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+
+/// Truncate a `UD30x9` value to a `u64` token amount.
+public fun settle(amount: ud30x9::UD30x9): u64 {
+ amount.to_u64_trunc()
+}
+```
+
+## Choosing a function
+
+- `mul`, `div`: round toward zero. Equivalent to `mul_trunc`, `div_trunc`. Use as the default when rounding direction does not matter.
+- `mul_trunc`, `div_trunc`: round toward zero (truncate). Use when you want to spell the rounding direction out at the call site.
+- `mul_away`, `div_away`: round away from zero when inexact. Use for fees, slippage caps, and protocol-side accounting where rounding should never favor the user against the protocol.
+- `unchecked_add`, `unchecked_sub`: wrap modulo `2^128` instead of aborting on overflow/underflow. Use only when you have already proved the operation is safe and you want to avoid the abort path. Otherwise prefer `add` / `sub`.
+- `pow`: approximate, biased toward zero, and not associative under truncation. Reasonable for small, integer exponents over typical fixed-point ranges. Verify expected behavior at your application's exponent and operand magnitudes before relying on it.
+
+For `SD29x9` specifically, prefer `mod` (Euclidean, always non-negative) over `rem` (truncating, sign follows dividend) unless your protocol explicitly wants the truncating variant.
+
+
+`SD29x9::min()` is `-2^127`. There is no representable `+2^127`, so calling `abs` or `negate` on `min()` aborts with `EOverflow`. Same caveat for `wrap(_, true)` at maximum magnitude.
+
+
+---
+
+## Why fixed-point on-chain
+
+Move has no native fractional type. Real-valued protocol math (prices, fees, interest rates, signed balance deltas) has to be encoded in integers. The naive approach is to scale by a power of two ("Q-notation": Q64.64, Q96, Q128, etc.) and use bit shifts. That works, but it has two persistent drawbacks:
+
+1. **Binary scales misalign with the units protocols actually report.** Token amounts, exchange rates, and oracle prices are quoted as decimals. Converting to and from a binary scale at every boundary creates rounding seams that are easy to get wrong and hard to audit.
+2. **Bit-shift arithmetic puts the rounding decision in the operator instead of the call site.** A `>>` is silent. A `mul_div`-shaped helper makes the rounding direction visible. The Cetus exploit hinged on a `checked_shl`-shaped function that silently passed a value it should have rejected; the same class of bug is harder to write when rounding is a named choice at every call site.
+
+`openzeppelin_fp_math` picks **decimal** fixed-point with **9 decimals** (`10^9`), matching the precision Sui uses for native coin amounts. `1.5` is `1_500_000_000`, `0.000_000_001` is `1`. Conversions between token amounts and fixed-point values become a single multiply or divide by `10^9`, with no bit-pattern reasoning involved.
+
+The package complements `openzeppelin_math`: integer arithmetic stays in `openzeppelin_math`, fractional arithmetic lives here. They use the same explicit-rounding philosophy.
+
+## Casting vs converting: the most important distinction
+
+The package draws a sharp line between two kinds of "convert this number to fixed-point" operations. Mixing them up is the most common way to introduce a `10^9`-sized bug, so it is worth getting clear on before writing any arithmetic.
+
+### Casting (preserves raw scaled bits)
+
+Casting reinterprets a value that is **already in fixed-point form**. It does not multiply or divide by `10^9`.
+
+The core cast helpers live on the `sd29x9` and `ud30x9` modules:
+
+```move
+use openzeppelin_fp_math::{sd29x9, ud30x9};
+
+let one = ud30x9::wrap(1_000_000_000); // 1.0
+let raw = ud30x9::wrap(42); // 0.000000042, NOT 42.0
+
+let positive_one = sd29x9::wrap(1_000_000_000, false); // 1.0
+let small_negative = sd29x9::wrap(42, true); // -0.000000042
+```
+
+Cross-type casts also count as casting: they preserve the scaled numeric meaning and only validate signedness or range.
+
+```move
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+
+let unsigned = ud30x9_convert::from_u128(42); // 42.0
+let signed = unsigned.into_SD29x9(); // 42.0 as SD29x9
+let roundtrip = signed.into_UD30x9(); // 42.0 as UD30x9
+```
+
+Use casting when your input is already fixed-point bits (e.g., loaded from storage, returned by another fixed-point function, or hand-encoded in a constant).
+
+### Converting (applies or removes the `10^9` scale)
+
+Converting changes between **whole-integer** semantics and **fixed-point** semantics. Use the `*_convert` modules for this.
+
+```move
+use openzeppelin_fp_math::{sd29x9_convert, ud30x9_convert};
+
+let whole = ud30x9_convert::from_u128(42); // 42.0, multiplies by 10^9
+let back = whole.to_u128_trunc(); // 42, divides by 10^9, truncates fractional
+
+let delta = sd29x9_convert::from_u128(5, true); // -5.0
+let (magnitude, is_negative) = delta.to_parts_trunc();
+// magnitude == 5, is_negative == true
+```
+
+Use converting whenever the input or output represents a whole-number quantity in your domain (a token amount the user typed, a count, a percentage point).
+
+### The mental rule
+
+> If `42` means `42`, use `*_convert::from_u128` / `to_u*_trunc`.
+> If `42` means `0.000_000_042`, use `wrap` / `unwrap`.
+
+Mixing the two collapses to a `10^9` scale error somewhere downstream, usually far from where it was introduced.
+
+### Negative values: why the sign flag
+
+Move does not provide a native signed integer, so `SD29x9` conversions take an unsigned magnitude plus a `bool` sign flag. `to_parts_trunc` returns the same shape on the way out:
+
+```move
+use openzeppelin_fp_math::sd29x9_convert;
+
+// Build -5.0
+let neg_five = sd29x9_convert::from_u128(5, true);
+
+// Read it back as (magnitude, is_negative)
+let (mag, neg) = neg_five.to_parts_trunc();
+// mag == 5, neg == true
+```
+
+Two consequences worth knowing:
+
+1. `to_parts_trunc` always reports `is_negative = false` when the magnitude is zero, regardless of the underlying bit pattern. This avoids "negative zero" surprises in callers that branch on the flag.
+2. `to_u128_trunc` and `to_u64_trunc` abort on negative input. Use the `try_*` variants when you want a `none()` instead, or call `to_parts_trunc` to get sign and magnitude in one go.
+
+## Rounding direction in fixed-point arithmetic
+
+Like `openzeppelin_math`, this package treats rounding as a protocol decision. But the spelling is different: the rounding mode is encoded in the **function name**, not in a `RoundingMode` parameter.
+
+| Function pair | Rounding | When to use |
+|---|---|---|
+| `mul`, `mul_trunc` | toward zero | Default. The two are aliases. Pick `mul_trunc` if you want the rounding direction visible at the call site. |
+| `mul_away` | away from zero | Fees, slippage caps, anywhere the protocol should never undercharge or under-reserve. |
+| `div`, `div_trunc` | toward zero | Default for division. Same alias relationship as `mul`. |
+| `div_away` | away from zero | When the divisor is user-supplied or quote-related and the protocol must bound itself conservatively. |
+
+The same vault-lifecycle logic from the integer-math guide applies here, just with finer granularity. If your vault holds `UD30x9` value internally:
+
+```move
+use openzeppelin_fp_math::ud30x9::UD30x9;
+
+/// Convert tokens to shares, rounding toward zero so the vault keeps the dust.
+public fun deposit_shares(amount: UD30x9, total_assets: UD30x9, total_supply: UD30x9): UD30x9 {
+ amount.mul_trunc(total_supply).div_trunc(total_assets)
+}
+
+/// Convert shares to tokens, rounding toward zero so the vault keeps the dust.
+public fun withdraw_assets(shares: UD30x9, total_assets: UD30x9, total_supply: UD30x9): UD30x9 {
+ shares.mul_trunc(total_assets).div_trunc(total_supply)
+}
+```
+
+Both directions truncate toward zero, so the vault never gives away fractional remainders. Flipping either to `mul_away` / `div_away` opens the same round-trip extraction attack discussed in the integer-math guide.
+
+### When `_trunc` and `_away` differ
+
+`mul` and `mul_trunc` differ only when the exact mathematical product cannot be represented in 9 decimals. For values that already fit cleanly, `mul_trunc(x, y) == mul_away(x, y)`. The choice only matters at the precision boundary, which in fee-and-rate land is exactly where it matters most.
+
+`div_away` is the helper you want for inverse-rate conversions. `1.0 / 3.0` is `0.333333333` with `div_trunc` and `0.333333334` with `div_away`. If your protocol owes the user some fraction of `x / 3`, rounding down on each individual settlement systematically under-pays the user.
+
+## Overflow and underflow
+
+Unlike `openzeppelin_math`, the fixed-point package does **not** return `Option` from arithmetic. It aborts. The error constants come in three families:
+
+- **`EOverflow`**: the result exceeds the representable range of the target type.
+- **`EUnderflow`**: applies to `UD30x9::sub` only. Subtraction would produce a negative value, which is unrepresentable in an unsigned type.
+- **`EDivideByZero`**: applies to `div`, `div_trunc`, `div_away`, `mod`, and `rem`.
+
+There are also the unchecked-add and unchecked-subtract escape hatches, plus `unchecked_lshift` / `unchecked_rshift` on `UD30x9`. They wrap modulo `2^128` instead of aborting.
+
+```move
+use openzeppelin_fp_math::ud30x9;
+
+let a = ud30x9::max();
+let b = ud30x9_convert::from_u128(1);
+
+let bad = a.add(b); // aborts with EOverflow
+let wrapped = a.unchecked_add(b); // wraps to a small value, no abort
+```
+
+Reach for `unchecked_*` only when you have already proved the operation is safe and want to skip the abort path (rare). For normal application code, prefer the checked versions and let aborts surface programmer bugs.
+
+If you need an `Option`-style API, build it locally:
+
+```move
+use openzeppelin_fp_math::ud30x9::UD30x9;
+
+public fun try_add(x: UD30x9, y: UD30x9): Option {
+ let raw_sum = (x.unwrap() as u256) + (y.unwrap() as u256);
+ if (raw_sum > std::u128::max_value!() as u256) {
+ option::none()
+ } else {
+ option::some(ud30x9::wrap(raw_sum as u128))
+ }
+}
+```
+
+## Comparison and equality
+
+Comparison operators on `UD30x9` and `SD29x9` work as you would expect (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `is_zero`). There is one subtle point on the signed type:
+
+```move
+use openzeppelin_fp_math::sd29x9;
+
+let a = sd29x9::wrap(0, false);
+let b = sd29x9::wrap(0, true);
+// Both encode 0; eq compares raw bits, but `wrap` normalizes 0 magnitude to a single representation.
+let same = a.eq(b); // true
+```
+
+`SD29x9::wrap` rejects "negative zero" by mapping `(0, true)` and `(0, false)` to the same underlying bits, so equality on zeros behaves as expected. If you ever construct an `SD29x9` from raw bits via the package-internal `from_bits`, this normalization does not happen, but `from_bits` is not part of the public API surface, so most callers never encounter it.
+
+## Cross-type casts
+
+`UD30x9` and `SD29x9` represent the same scaled meaning when their values overlap in the non-negative range, so the package provides explicit casts both ways:
+
+```move
+use openzeppelin_fp_math::{sd29x9_convert, ud30x9_convert};
+
+// UD30x9 -> SD29x9
+let unsigned = ud30x9_convert::from_u128(42); // 42.0 UD30x9
+let signed = unsigned.into_SD29x9(); // 42.0 SD29x9
+// `into_SD29x9` aborts with `ECannotBeConvertedToSD29x9` if the magnitude exceeds 2^127 - 1.
+
+// SD29x9 -> UD30x9
+let positive = sd29x9_convert::from_u128(42, false); // 42.0 SD29x9
+let back = positive.into_UD30x9(); // 42.0 UD30x9
+// `into_UD30x9` aborts with `ECannotBeConvertedToUD30x9` if the value is negative.
+```
+
+Each direction has a `try_into_*` counterpart that returns `Option` instead of aborting. Use the `try_*` variants on values that come from external input or untrusted callers; use the abort variants in internal protocol code where a failed cast is a program bug.
+
+## Bitwise operations on `UD30x9`
+
+`UD30x9` exposes a full set of bitwise helpers: `and`, `and2`, `or`, `xor`, `not`, plus `lshift` / `rshift` and their `unchecked_*` siblings. `SD29x9` does **not** expose bitwise operations, on purpose: bit-twiddling on a two's complement representation is rarely what protocol authors mean.
+
+The most common use for these is packing flags or scale-shifting raw bit patterns inside a fixed-point pipeline:
+
+```move
+use openzeppelin_fp_math::ud30x9;
+
+// Mask to keep only the low 64 bits of the raw representation.
+let truncated = ud30x9::wrap(some_value).and(0xFFFFFFFFFFFFFFFF);
+
+// Shift the raw representation right by 9 bits. Note: this is a bit shift,
+// not a divide-by-2^9, and not a divide-by-10^9.
+let shifted = ud30x9::wrap(some_value).rshift(9);
+```
+
+Two cautions:
+
+1. `lshift` aborts when shifting by `>= 128` bits or when the shift would consume non-zero high bits. `unchecked_lshift` truncates instead of aborting and returns `0` for `bits >= 128`.
+2. The bitwise operators work on **raw bits**, not on the decimal value. `x.lshift(1)` doubles the underlying `u128`, but it does **not** double the decimal value in any meaningful sense (you'd want `x.add(x)` for that).
+
+If you need a `*` 2 or `/` 2 operation, use arithmetic. The bitwise helpers are for explicit bit manipulation, not for fast arithmetic.
+
+## `pow`: useful but approximate
+
+Both types expose `pow(x, exp: u8)` for integer exponents. The implementation uses binary exponentiation with fixed-point multiplication, which means **every intermediate multiply applies a fixed-point truncation by `10^9`**.
+
+The consequences:
+
+- Results are biased toward zero. Rounding error compounds as `exp` grows.
+- For `0 < x < 1` (and the analogous range in `SD29x9`), intermediate values can reach zero before the final mathematically-correct result would.
+- Because truncation is applied at intermediate steps, fixed-point multiplication is **not associative under truncation**. The grouping that binary exponentiation uses can change the result.
+
+In practice:
+
+```move
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+
+let two = ud30x9_convert::from_u128(2);
+let eight = two.pow(3); // 8.0, exact for whole-number bases.
+
+let half = ud30x9::wrap(500_000_000); // 0.5
+let q = half.pow(20); // ≈ 0.000000953... but truncation may bias the result.
+```
+
+For small integer exponents over operands that are not far from `1.0`, `pow` is fine. For long compounded products (interest accrual over many periods, statistical computations), prefer to:
+
+- Restructure the computation to reduce the number of intermediate truncations, or
+- Switch to a pre-computed lookup or higher-precision representation in the worst-case region, or
+- Bound the rounding error analytically and check whether your protocol can absorb it.
+
+## The `min()` gotcha on `SD29x9`
+
+`SD29x9::min()` returns `-2^127`. There is no representable `+2^127`, so several operations abort when called on `min()`:
+
+- `abs(min())`: aborts with `EOverflow`.
+- `negate(min())`: aborts with `EOverflow`.
+- `wrap(2^127, true)` is fine (it produces `min`), but `wrap(2^127, false)` aborts with `EOverflow`. There is no way to construct `min` via `wrap`. Use `min()` directly.
+
+Most protocol code never sees `min()`. If your design naturally produces values near the signed-integer boundary, plan for it explicitly:
+
+```move
+use openzeppelin_fp_math::sd29x9;
+
+public fun safe_negate(x: sd29x9::SD29x9): sd29x9::SD29x9 {
+ if (x.eq(sd29x9::min())) {
+ // Domain decision: clamp, abort with a domain error, or saturate to max.
+ sd29x9::max()
+ } else {
+ x.negate()
+ }
+}
+```
+
+## `mod` vs `rem` on `SD29x9`
+
+`UD30x9::mod` is straightforward: it is the unsigned remainder, always non-negative. `SD29x9` exposes **two** remainder functions because the signed semantics differ:
+
+- **`rem(x, y)`**: truncating remainder. The magnitude is `abs(x) % abs(y)`, and the sign of the result follows the dividend `x`. `rem(-7, 3) = -1`.
+- **`mod(x, y)`**: Euclidean remainder. Always non-negative. Satisfies `0 <= mod(x, y) < abs(y)`. `mod(-7, 3) = 2`.
+
+Most protocol code wants `mod` (Euclidean), because it gives a stable representative of the residue class regardless of the dividend's sign. `rem` is the right choice when you specifically want the remainder to follow the sign of the dividend (e.g., to preserve sign in a pipeline of signed adjustments).
+
+```move
+use openzeppelin_fp_math::sd29x9_convert;
+
+let dividend = sd29x9_convert::from_u128(7, true); // -7.0
+let divisor = sd29x9_convert::from_u128(3, false); // 3.0
+
+let r = dividend.rem(divisor); // -1.0
+let m = dividend.mod(divisor); // 2.0
+```
+
+Both abort with `EDivideByZero` if `y` is zero.
+
+## Putting it together
+
+Here is a pricing module example that combines several functions from the package:
+
+```move
+module my_sui_app::pricing;
+
+use openzeppelin_fp_math::{ud30x9, ud30x9_convert};
+use openzeppelin_fp_math::ud30x9::UD30x9;
+
+const ONE_HUNDRED_PERCENT_BPS: u128 = 10_000;
+
+/// Apply a fee rate (in basis points) to an amount. Round away from zero
+/// so the protocol never undercharges.
+public fun apply_fee_bps(amount: UD30x9, fee_bps: u128): UD30x9 {
+ let bps_one = ud30x9_convert::from_u128(ONE_HUNDRED_PERCENT_BPS);
+ let fee = ud30x9_convert::from_u128(fee_bps);
+ amount.mul_away(fee).div_away(bps_one)
+}
+
+/// Quote a constant-product swap output, less a fee.
+public fun quote_swap_with_fee(
+ amount_in: UD30x9,
+ reserve_in: UD30x9,
+ reserve_out: UD30x9,
+ fee_bps: u128,
+): UD30x9 {
+ let fee = apply_fee_bps(amount_in, fee_bps);
+ let effective_in = amount_in.sub(fee);
+
+ // Constant-product output, rounded toward zero (in the user's favor as the
+ // taker, against the user as a fee recipient. Model your protocol explicitly).
+ reserve_out.mul_trunc(effective_in).div_trunc(reserve_in.add(effective_in))
+}
+
+/// Settle a `UD30x9` quote into a whole `u64` token amount.
+public fun settle_to_u64(amount: UD30x9): u64 {
+ amount.to_u64_trunc()
+}
+```
+
+And the same pattern with signed deltas:
+
+```move
+module my_sui_app::ledger;
+
+use openzeppelin_fp_math::{sd29x9, sd29x9_convert};
+use openzeppelin_fp_math::sd29x9::SD29x9;
+
+/// Apply a signed adjustment to a running balance.
+public fun apply_delta(balance: SD29x9, magnitude: u128, is_negative: bool): SD29x9 {
+ let delta = sd29x9_convert::from_u128(magnitude, is_negative);
+ balance.add(delta)
+}
+
+/// Read out the absolute value as a positive UD30x9.
+public fun absolute(balance: SD29x9): ud30x9::UD30x9 {
+ balance.abs().into_UD30x9()
+}
+```
+
+Build and test:
+
+```bash
+sui move build
+sui move test
+```
+
+## API Reference
+
+For function-level signatures and parameters, see the [Fixed-Point Math API reference](/contracts-sui/1.x/api/fixed-point).
+
+## Next steps
+
+- [Fixed-Point Math API reference](/contracts-sui/1.x/api/fixed-point) for full function signatures
+- [Integer Math](/contracts-sui/1.x/math) for integer arithmetic primitives
+- [Access](/contracts-sui/1.x/access) for ownership-transfer policies on privileged capabilities
+- [GitHub issue tracker](https://github.com/OpenZeppelin/contracts-sui/issues) to report bugs or request features
+- [Sui Discord](https://discord.gg/sui) and [Sui Developer Forum](https://forums.sui.io/) to connect with other builders
diff --git a/content/contracts-sui/1.x/index.mdx b/content/contracts-sui/1.x/index.mdx
index 9bae3e79..7b58f54c 100644
--- a/content/contracts-sui/1.x/index.mdx
+++ b/content/contracts-sui/1.x/index.mdx
@@ -2,9 +2,10 @@
title: Contracts for Sui 1.x
---
-**OpenZeppelin Contracts for Sui v1.x** ships two core packages:
+**OpenZeppelin Contracts for Sui v1.x** ships three core packages:
-- `openzeppelin_math` for deterministic arithmetic, configurable rounding, and decimal scaling.
+- `openzeppelin_math` for deterministic integer arithmetic, configurable rounding, and decimal scaling.
+- `openzeppelin_fp_math` for 9-decimal fixed-point arithmetic (`UD30x9`, `SD29x9`) backed by `u128`.
- `openzeppelin_access` for ownership-transfer wrappers around privileged `key + store` objects.
## Quickstart
@@ -27,16 +28,20 @@ cd my_sui_app
```bash
mvr add @openzeppelin-move/access
mvr add @openzeppelin-move/integer-math
+mvr add @openzeppelin-move/fixed-point-math
```
+You only need the dependencies your app actually uses. Add what you need and drop the others.
+
### 3. Verify `Move.toml`
-`mvr add` updates `Move.toml` automatically. It should include:
+`mvr add` updates `Move.toml` automatically. With all three installed it should include:
```toml
[dependencies]
openzeppelin_access = { r.mvr = "@openzeppelin-move/access" }
openzeppelin_math = { r.mvr = "@openzeppelin-move/integer-math" }
+openzeppelin_fp_math = { r.mvr = "@openzeppelin-move/fixed-point-math" }
```
### 4. Add a Minimal Module
@@ -53,7 +58,7 @@ use openzeppelin_math::u64::{mul_div, sqrt};
public fun quote_with_fee(amount: u64): u64 {
// 2.5% fee, rounded to nearest.
- let quoted = mul_div(amount,1025u64, 1000u64, rounding::nearest());
+ let quoted = mul_div(amount, 1025u64, 1000u64, rounding::nearest());
quoted.destroy_some()
}
@@ -69,7 +74,15 @@ sui move build
sui move test
```
+## Picking a package
+
+- Need integer arithmetic with safe overflow and explicit rounding? Use [Integer Math](/contracts-sui/1.x/math).
+- Need fractional values like prices, fees, rates, or signed deltas? Use [Fixed-Point Math](/contracts-sui/1.x/fixed-point).
+- Need controlled transfer of admin/treasury/upgrade capabilities? Use [Access](/contracts-sui/1.x/access).
+
+The packages compose. A typical protocol module imports `openzeppelin_math` for share math, `openzeppelin_fp_math` for rate and fee math, and `openzeppelin_access` for the admin capability that governs both.
+
## 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).
+- Package guides: [Integer Math](/contracts-sui/1.x/math), [Fixed-Point Math](/contracts-sui/1.x/fixed-point), [Access](/contracts-sui/1.x/access).
+- API reference: [Integer Math](/contracts-sui/1.x/api/math), [Fixed-Point Math](/contracts-sui/1.x/api/fixed-point), [Access](/contracts-sui/1.x/api/access).
diff --git a/content/contracts-sui/1.x/learn/access-walkthrough.mdx b/content/contracts-sui/1.x/learn/access-walkthrough.mdx
deleted file mode 100644
index f4fc9a66..00000000
--- a/content/contracts-sui/1.x/learn/access-walkthrough.mdx
+++ /dev/null
@@ -1,331 +0,0 @@
----
-title: Access Walkthrough
----
-
-
-The example code snippets used within this walkthrough are experimental and have not been audited. They simply help exemplify the OpenZeppelin Sui package usage.
-
-
-This guide provides a detailed walkthrough of the `openzeppelin_access` package. It explains the design behind each transfer policy, walks through the full lifecycle of wrapping and transferring capabilities, and covers the borrow patterns, events, and error handling you need to integrate these modules into a protocol. For a quick overview and getting-started examples, see the [Access package guide](https://docs.openzeppelin.com/contracts-sui/1.x/access). For function-level signatures, see the [Access API reference](https://docs.openzeppelin.com/contracts-sui/1.x/api/access).
-
-[Source Code](https://github.com/OpenZeppelin/contracts-sui/tree/main/contracts/access)
-
----
-
-## Why controlled transfers matter
-
-On Sui, `sui::transfer::transfer` is instant and irreversible. There is no confirmation step, no waiting period, and no cancel mechanism. For everyday objects this is fine. For privileged capability objects, such as admin caps, treasury caps, or upgrade authorities, a single mistaken or malicious transfer permanently moves control with no recourse.
-
-The `openzeppelin_access` package adds two transfer policies that sit between you and that irreversible `transfer` call:
-
-| Module | What it enforces | Analogy from Solidity |
-| --- | --- | --- |
-| `two_step_transfer` | Recipient must explicitly accept before the transfer completes | `Ownable2Step` |
-| `delayed_transfer` | Mandatory time delay before execution; anyone can observe the pending action | `TimelockController` on ownership |
-
-If you already know which policy you need, skip to [Choosing a transfer policy](#choosing-a-transfer-policy) or jump directly to [two\_step\_transfer](#two_step_transfer) or [delayed\_transfer](#delayed_transfer).
-
----
-
-## Wrapping and transfer policies
-
-Both modules use the same underlying mechanism: wrapping a capability inside a new object that enforces a transfer policy on it.
-
-When you call `wrap`, the capability is stored as a dynamic object field inside the wrapper. This means:
-
-- **The wrapper becomes the custody object.** You hold the wrapper, not the capability directly. To transfer or recover the capability, you go through the wrapper's policy.
-- **The underlying capability retains its on-chain ID.** Off-chain indexers and explorers can still discover and track it via the dynamic object field. Wrapping does not make the capability invisible.
-- **The wrapper intentionally omits the `store` ability.** Without `store`, the wrapper cannot be moved via `transfer::public_transfer`. Only the module's own functions (which use the privileged `transfer::transfer` internally) can move it. This is a deliberate design choice that prevents accidental transfers outside the policy.
-
-A `WrapExecuted` event is emitted when a capability is wrapped, creating an on-chain record of when the policy was applied.
-
-### Borrowing without unwrapping
-
-Both modules provide three ways to use the wrapped capability without changing ownership:
-
-**Immutable borrow** for read-only access:
-
-```move
-let cap_ref = wrapper.borrow(); // &AdminCap
-```
-
-**Mutable borrow** for updating the capability's internal state:
-
-```move
-let cap_mut = wrapper.borrow_mut(); // &mut AdminCap
-```
-
-**Temporary move** for functions that require the capability by value. This uses the hot potato pattern: `borrow_val` returns a `Borrow` struct with no abilities (`copy`, `drop`, `store`, `key` are all absent). The Move compiler enforces that it must be consumed by `return_val` before the transaction ends.
-
-```move
-let (cap, borrow_token) = wrapper.borrow_val();
-
-/// Use cap in functions that require it by value.
-wrapper.return_val(cap, borrow_token); // compiler enforces this call
-```
-
-If you try to drop the borrow token, return a different capability, or return it to the wrong wrapper, the transaction either won't compile or will abort at runtime.
-
----
-
-## `two_step_transfer`
-
-[Source Code](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/contracts/access/sources/ownership_transfer/two_step.move)
-
-A transfer policy that requires the designated recipient to explicitly accept before the wrapper changes hands. The initiator retains cancel authority until acceptance. There is no time delay, thus the transfer executes immediately once the recipient accepts.
-
-This is the right choice when the principal initiating the transfer is a known, controlled key (a multisig, a hot wallet operated by the same team) and the risk you are guarding against is sending the capability to a wrong or non-existent address, which would permanently lock the capability with no way to recover it.
-
-### Step 1: Wrap the capability
-
-```move
-module my_sui_app::admin;
-
-use openzeppelin_access::two_step_transfer;
-
-public struct AdminCap has key, store { id: UID }
-
-/// Wrap and immediately initiate a transfer to `new_admin`.
-/// The wrapper is consumed by `initiate_transfer` and held
-/// inside the shared `PendingOwnershipTransfer` until the
-/// recipient accepts or the initiator cancels.
-public fun wrap_and_transfer(cap: AdminCap, new_admin: address, ctx: &mut TxContext) {
- let wrapper = two_step_transfer::wrap(cap, ctx);
- // Emits WrapExecuted
-
- wrapper.initiate_transfer(new_admin, ctx);
- // Emits TransferInitiated
-}
-```
-
-`wrap` stores the `AdminCap` inside a `TwoStepTransferWrapper` and emits a `WrapExecuted` event. Because the wrapper lacks the `store` ability, it cannot be sent via `transfer::public_transfer`. The intended next step is to call `initiate_transfer`, which consumes the wrapper and creates a shared `PendingOwnershipTransfer` object that both parties can interact with.
-
-### Step 2: Initiate a transfer
-
-`initiate_transfer` consumes the wrapper by value and creates a shared `PendingOwnershipTransfer` object. The sender's address is recorded as `from` (the cancel authority), and the recipient's address is recorded as `to`.
-
-```move
-/// Called by the current wrapper owner. Consumes the wrapper.
-wrapper.initiate_transfer(new_admin_address, ctx);
-/// Emits TransferInitiated { wrapper_id, from, to }
-```
-
-After this call, the wrapper is held inside the pending request via transfer-to-object. The original owner no longer has it in their scope, but they retain the ability to cancel because their address is recorded as `from`.
-
-The `TransferInitiated` event contains the pending request's ID and the wrapper ID, allowing off-chain indexers to discover the shared `PendingOwnershipTransfer` object for the next step.
-
-### Step 3: Recipient accepts (or initiator cancels)
-
-The designated recipient calls `accept_transfer` to complete the handoff. This step uses Sui's [transfer-to-object (TTO)](https://docs.sui.io/guides/developer/objects/transfers/transfer-to-object) pattern: the wrapper was transferred to the `PendingOwnershipTransfer` object in Step 2, so the recipient must provide a `Receiving>` ticket to claim it. The `Receiving` type is Sui's mechanism for retrieving objects that were sent to another object rather than to a wallet.
-
-```move
-/// Called by the address recorded as `to` (new_admin_address).
-/// `request` is the shared PendingOwnershipTransfer object.
-/// `wrapper_ticket` is the Receiving> for the wrapper
-/// that was transferred to the request object.
-two_step_transfer::accept_transfer(request, wrapper_ticket, ctx);
-// Emits TransferAccepted { wrapper_id, from, to }
-```
-
-If the initiator changes their mind before the recipient accepts, they can cancel. The cancel call also requires the `Receiving` ticket for the wrapper:
-
-```move
-/// Called by the address recorded as `from` (the original initiator).
-two_step_transfer::cancel_transfer(request, wrapper_ticket, ctx);
-/// Wrapper is returned to the `from` address.
-```
-
-### Unwrapping
-
-To permanently reclaim the raw capability and destroy the wrapper:
-
-```move
-let admin_cap = wrapper.unwrap(ctx);
-```
-
-This bypasses the transfer flow entirely. Only the current wrapper owner can call it.
-
-### Security note on shared-object flows
-
-`initiate_transfer` records `ctx.sender()` as the cancel authority. In normal single-owner usage, this is the wallet holding the wrapper. However, if `initiate_transfer` is called inside a shared-object executor where any user can be the transaction sender, a malicious user could call `initiate_transfer` targeting their own address as recipient. They would become both the pending recipient and the sole cancel authority, locking out the legitimate owner.
-
-Avoid using `two_step_transfer` in shared-object executor flows unless your design explicitly maps signer identity to cancel authority.
-
----
-
-## `delayed_transfer`
-
-[Source Code](https://github.com/OpenZeppelin/contracts-sui/blob/v1.0.0/contracts/access/sources/ownership_transfer/delayed.move)
-
-A transfer policy that enforces a configurable minimum delay between scheduling and executing a transfer. The delay is set at wrap time and cannot be changed afterward. This creates a publicly visible window before any authority change takes effect, giving monitoring systems, DAOs, and individual users time to detect and respond.
-
-This is the right choice when your protocol requires on-chain lead time before a capability changes hands, for example, to allow an incident response process to detect a compromised key, or to give depositors time to exit before governance parameters change.
-
-### Step 1: Wrap with a delay
-
-```move
-module my_sui_app::treasury;
-
-use openzeppelin_access::delayed_transfer;
-
-public struct TreasuryCap has key, store { id: UID }
-
-const MIN_DELAY_MS: u64 = 86_400_000; // 24 hours
-
-/// Creates the wrapper and transfers it to ctx.sender() internally
-public fun wrap_treasury_cap(cap: TreasuryCap, ctx: &mut TxContext) {
- delayed_transfer::wrap(cap, MIN_DELAY_MS, ctx.sender(), ctx);
-}
-```
-
-`wrap` creates a `DelayedTransferWrapper`, stores the capability inside it as a dynamic object field, and transfers the wrapper to the specified `recipient` (here, the caller). A `WrapExecuted` event is emitted. Unlike `two_step_transfer::wrap` which returns the wrapper, `delayed_transfer::wrap` handles the transfer internally and has no return value.
-
-### Step 2: Schedule a transfer
-
-```move
-/// Called by the current wrapper owner.
-wrapper.schedule_transfer(new_owner_address, &clock, ctx);
-/// Emits TransferScheduled with execute_after_ms = clock.timestamp_ms() + min_delay_ms
-```
-
-The `Clock` object is Sui's shared on-chain clock. The deadline is computed as `clock.timestamp_ms() + min_delay_ms` and stored in the wrapper. Only one action can be pending at a time; scheduling a second without canceling the first aborts with `ETransferAlreadyScheduled`.
-
-During the delay window, the `TransferScheduled` event is visible on-chain. Monitoring systems, governance dashboards, or individual users watching the chain can detect the pending transfer and take action (e.g., withdrawing funds from the protocol) before it executes.
-
-
-The `recipient` in `schedule_transfer` must be a wallet address, not an object ID. If the wrapper is transferred to an object via transfer-to-object (TTO), both the wrapper and the capability inside it become permanently locked. The `delayed_transfer` module does not implement a `Receiving`-based retrieval mechanism, so there is no way to borrow, unwrap, or further transfer a wrapper that has been sent to an object. Always verify that the scheduled recipient is an address controlled by a keypair.
-
-
-### Step 3: Wait, then execute
-
-```move
-/// Callable after the delay window has passed.
-wrapper.execute_transfer(&clock, ctx);
-/// Emits OwnershipTransferred. Consumes the wrapper and delivers it to the recipient.
-```
-
-`execute_transfer` consumes the wrapper by value. After this call, the wrapper has been transferred to the scheduled recipient and no longer exists in the caller's scope. Calling it before `execute_after_ms` aborts with `EDelayNotElapsed`.
-
-### Scheduling an unwrap
-
-The same delay enforcement applies to recovering the raw capability:
-
-```move
-/// Schedule the unwrap
-wrapper.schedule_unwrap(&clock, ctx);
-/// Emits UnwrapScheduled
-
-/// After the delay has elapsed, executes the unwrap: Emits UnwrapExecuted, wrapper is consumed, and capability is returned.
-let treasury_cap = wrapper.unwrap(&clock, ctx);
-```
-
-### Canceling
-
-The owner can cancel a pending action at any time before execution:
-
-```move
-wrapper.cancel_schedule();
-```
-
-This clears the pending slot immediately, allowing a new action to be scheduled.
-
----
-
-## Choosing a transfer policy
-
-**Use `two_step_transfer` when:**
-
-- The transfer can execute immediately once confirmed.
-- The principal initiating the transfer is a known, controlled key.
-- The risk you are guarding against is human error (wrong or non-existent address), not timing.
-
-**Use `delayed_transfer` when:**
-
-- Your protocol requires on-chain lead time before authority changes.
-- Users, DAOs, or monitoring systems need a window to detect and respond.
-- The delay should be a reliable, inspectable commitment visible to anyone.
-
-**Combining both:** The modules accept any `T: key + store`, so they compose. You could wrap a capability in `delayed_transfer` for the timing guarantee and use a `two_step_transfer` flow at the scheduling step for address-confirmation safety.
-
----
-
-## Putting it together
-
-Here is a protocol example that uses `delayed_transfer` to wrap its admin capability, ensuring any ownership change is visible on-chain for 24 hours before it takes effect:
-
-```move
-module my_sui_app::governed_protocol;
-
-use openzeppelin_access::delayed_transfer::{Self, DelayedTransferWrapper};
-use openzeppelin_math::rounding;
-use openzeppelin_math::u64 as math_u64;
-use sui::clock::Clock;
-
-const MIN_DELAY_MS: u64 = 86_400_000; // 24 hours
-const EMathOverflow: u64 = 0;
-
-public struct ProtocolAdmin has key, store {
- id: UID,
- fee_bps: u64,
-}
-
-/// Initialize: create the admin cap and wrap it with a 24-hour transfer delay.
-/// `delayed_transfer::wrap` transfers the wrapper to the deployer internally.
-fun init(ctx: &mut TxContext) {
- let admin = ProtocolAdmin {
- id: object::new(ctx),
- fee_bps: 30, // 0.3%
- };
- delayed_transfer::wrap(admin, MIN_DELAY_MS, ctx.sender(), ctx);
-}
-
-/// Update the fee rate. Borrows the admin cap mutably without changing ownership.
-public fun update_fee(
- wrapper: &mut DelayedTransferWrapper,
- new_fee_bps: u64,
-) {
- let admin = delayed_transfer::borrow_mut(wrapper);
- admin.fee_bps = new_fee_bps;
-}
-
-/// Compute a fee using the admin-configured rate and safe math.
-public fun compute_fee(
- wrapper: &DelayedTransferWrapper,
- amount: u64,
-): u64 {
- let admin = delayed_transfer::borrow(wrapper);
- math_u64::mul_div(amount, admin.fee_bps, 10_000, rounding::up())
- .destroy_or!(abort EMathOverflow)
-}
-
-/// Schedule a transfer to a new admin. Visible on-chain for 24 hours.
-public fun schedule_admin_transfer(
- wrapper: &mut DelayedTransferWrapper,
- new_admin: address,
- clock: &Clock,
- ctx: &mut TxContext,
-) {
- wrapper.schedule_transfer(new_admin, clock, ctx);
-}
-```
-
-This module combines both packages: `openzeppelin_math` for the fee calculation (explicit rounding, overflow handling) and `openzeppelin_access` for the ownership policy (24-hour delay, on-chain observability). Users monitoring the chain see the `TransferScheduled` event and can exit before a new admin takes over.
-
-Build and test:
-
-```bash
-sui move build
-sui move test
-```
-
----
-
-## Next steps
-
-- [Access package guide](https://docs.openzeppelin.com/contracts-sui/1.x/access) for a quick overview and examples
-- [Integer Math package guide](/contracts-sui/1.x/math) for a quick overview and examples
-- [Access API reference](https://docs.openzeppelin.com/contracts-sui/1.x/api/access) for full function signatures and error codes
-- [Math Walkthrough](/contracts-sui/1.x/learn/math-walkthrough) for a detailed walkthrough of the math library
-- [Source code](https://github.com/OpenZeppelin/contracts-sui/tree/main/contracts/access) for inline documentation and implementation details
-- [GitHub issue tracker](https://github.com/OpenZeppelin/contracts-sui/issues) to report bugs or request features
-- [Sui Discord](https://discord.gg/sui) and [Sui Developer Forum](https://forums.sui.io/) to connect with other builders
diff --git a/content/contracts-sui/1.x/learn/index.mdx b/content/contracts-sui/1.x/learn/index.mdx
deleted file mode 100644
index 8b8908a4..00000000
--- a/content/contracts-sui/1.x/learn/index.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Learn
----
-
-Comprehensive guides for building with OpenZeppelin Contracts for Sui.
-
-* [Math Walkthrough](/contracts-sui/1.x/learn/math-walkthrough) - A detailed walkthrough of the `openzeppelin_math` package: rounding modes, overflow handling, and safe arithmetic primitives
-* [Access Walkthrough](/contracts-sui/1.x/learn/access-walkthrough) - A detailed walkthrough of the `openzeppelin_access` package: two-step and delayed ownership transfer policies for privileged capabilities
diff --git a/content/contracts-sui/1.x/learn/math-walkthrough.mdx b/content/contracts-sui/1.x/learn/math-walkthrough.mdx
deleted file mode 100644
index ae77622e..00000000
--- a/content/contracts-sui/1.x/learn/math-walkthrough.mdx
+++ /dev/null
@@ -1,342 +0,0 @@
----
-title: Math Walkthrough
----
-
-
-The example code snippets used within this walkthrough are experimental and have not been audited. They simply help exemplify the OpenZeppelin Sui Package usage.
-
-
-This walkthrough provides a detailed walkthrough of the `openzeppelin_math` package. It is intended for developers who want to understand not just *what* each function does, but *when* and *why* to use it. For a quick overview and getting-started examples, see the [Integer Math package guide](https://docs.openzeppelin.com/contracts-sui/1.x/math). For function-level signatures and parameters, see the [Integer Math API reference](https://docs.openzeppelin.com/contracts-sui/1.x/api/math).
-
-[Source Code](https://github.com/OpenZeppelin/contracts-sui/tree/main/math/core)
-
----
-
-## Why explicit math matters on-chain
-
-Move's native integer arithmetic aborts on overflow and truncates on division. Both behaviors are silent protocol decisions. An abort at an unexpected boundary can lock a transaction. A truncation that always rounds against your users creates a systematic value leak.
-
-In May 2025, a single flawed overflow check in a shared math library led to the Cetus exploit, where approximately $223 million was drained from the largest DEX on Sui. The bug was not in novel math. It was in a `checked_shl`-type function that silently passed a value it should have rejected, corrupting a fixed-point intermediate that multiple protocols depended on.
-
-`openzeppelin_math` addresses these problems by making every boundary decision explicit: rounding is a named parameter you pass in, overflow returns `Option` that you then handle, and intermediate products are computed in wider types so they don't silently overflow before the final result is computed.
-
----
-
-## Rounding as a protocol decision
-
-The package guide introduces the three rounding modes (`Down`, `Up`, `Nearest`). Here we'll dig deeper into *why* this matters and how to think about rounding in practice.
-
-Every function that divides, shifts, or roots a value requires a `RoundingMode` argument. There is no default, and the choice is not cosmetic. Rounding direction determines which side of a transaction absorbs fractional remainders, and in modern day blockchain applications, that determines where value leaks.
-
-### The vault example
-
-Consider a vault that issues shares for deposits:
-
-```move
-use openzeppelin_math::{rounding, u64 as math_u64};
-
-const EMathOverflow: u64 = 0;
-
-/// Deposit: convert tokens to shares, rounding DOWN.
-/// The depositor receives slightly fewer shares than the exact ratio.
-/// The vault keeps the fractional remainder.
-public fun deposit_shares(amount: u64, total_assets: u64, total_supply: u64): u64 {
- math_u64::mul_div(amount, total_supply, total_assets, rounding::down())
- .destroy_or!(abort EMathOverflow)
-}
-
-/// Withdrawal: convert shares to tokens, rounding DOWN.
-/// The withdrawer receives slightly fewer tokens than the exact ratio.
-/// The vault keeps the fractional remainder.
-public fun withdraw_assets(shares: u64, total_assets: u64, total_supply: u64): u64 {
- math_u64::mul_div(shares, total_assets, total_supply, rounding::down())
- .destroy_or!(abort EMathOverflow)
-}
-```
-
-Both directions round down so the vault never gives away more than it holds. If you rounded up on deposits (giving the depositor extra shares) or up on withdrawals (giving the withdrawer extra tokens), an attacker could repeatedly deposit and withdraw small amounts, extracting the rounding remainder each time. Making rounding explicit in every call forces you to make this decision consciously and makes it visible in code review.
-
-### When to use each mode
-
-- **`rounding::down()`** rounds toward zero (truncation). Use when the caller should absorb the remainder. This is the safe default for most protocol-to-user calculations.
-- **`rounding::up()`** rounds away from zero (ceiling). Use when the protocol should absorb the remainder, or when you need a conservative upper bound.
-- **`rounding::nearest()`** rounds to the nearest integer, with ties rounded up (round-half-up). Use for fee quotes and display calculations where rounding against either party on non-tie values is undesirable.
-
----
-
-## Handling `Option` at the boundary
-
-The package guide shows `Option` returns briefly. Here we'll cover the patterns you'll use in practice.
-
-Every function whose result could exceed the type range returns `Option`. This includes `mul_div`, `mul_shr`, `checked_shl`, `checked_shr`, and `inv_mod`. Functions that cannot overflow (`average`, `sqrt`, logarithms) return the value directly.
-
-### Pattern: abort with a domain-specific error
-
-The most common pattern. Define an error constant in your module and abort if the math overflows:
-
-```move
-use openzeppelin_math::rounding;
-use openzeppelin_math::u64::{mul_div};
-
-const EMathOverflow: u64 = 0;
-
-public fun compute_fee(amount: u64, fee_bps: u64): u64 {
- mul_div(amount, fee_bps, 10_000u64, rounding::up())
- .destroy_or!(abort EMathOverflow)
-}
-```
-
-### Pattern: fallback to a safe value
-
-Useful when overflow means "cap at maximum" rather than "this is an error":
-
-```move
-use openzeppelin_math::{rounding};
-use openzeppelin_math::u64::{mul_div};
-
-public fun capped_scale(value: u64, multiplier: u64): u64 {
- let result = mul_div(value, multiplier, 1_000u64, rounding::down());
- if (result.is_some()) {
- result.destroy_some()
- } else {
- 18_446_744_073_709_551_615u64 // u64::MAX
- }
-}
-```
-
-### Pattern: propagate to the caller
-
-When your function is itself a building block, return `Option` and let the caller decide:
-
-```move
-use openzeppelin_math::{rounding};
-use openzeppelin_math::u64::{mul_div};
-
-public fun try_compute_shares(amount: u64, total_assets: u64, total_supply: u64): Option {
- mul_div(amount, total_supply, total_assets, rounding::down())
-}
-```
-
----
-
-## Core arithmetic: `mul_div`, `mul_shr`, `average`
-
-These three functions cover the fundamental operations behind swap pricing, fee calculations, interest accrual, and share-based accounting.
-
-
-The functions shown in this walkthrough are simplified to illustrate their usage. In a real implementation, they must be defined within proper Move function syntax.
-
-
-### `mul_div`
-
-Computes `(a * b) / denominator` with explicit rounding. The intermediate product is computed in a wider type (up to `u512` for `u256` inputs) so it never silently overflows. Returns `Option`.
-
-```move
-use openzeppelin_math::{rounding, u256};
-use openzeppelin_math::u256::{mul_div};
-
-// Price calculation: (reserve_out * amount_in) / (reserve_in + amount_in)
-let output = mul_div(reserve_out, amount_in, reserve_in + amount_in, rounding::down());
-```
-
-Instead of carrying out any multiply-then-divide operation manually (e.g., `(a * b) / c`), you can simply use `mul_div` instead. The manual version overflows when `a * b` exceeds the type range, even if the final result after division would have been safe.
-
-### `mul_shr`
-
-Computes `(a * b) >> shift` with rounding. Equivalent to `mul_div` with a power-of-two denominator, but faster. Returns `Option`.
-
-```move
-use openzeppelin_math::{rounding, u128};
-use openzeppelin_math::u128::{mul_shr};
-
-// Fixed-point multiplication: (price * quantity) / 2^64
-let result = mul_shr(price, quantity, 64u8, rounding::down());
-```
-
-Use `mul_shr` instead of `mul_div` when your denominator is a power of two. This is common in Q-notation fixed-point math (Q64.64, Q96, etc.) and the tick-math used in concentrated liquidity AMMs. The Cetus exploit involved exactly this class of operation.
-
-### `average`
-
-Computes the arithmetic mean of two values without overflow. Does not return `Option` since the result always fits in the input type.
-
-```move
-use openzeppelin_math::{rounding, u64};
-use openzeppelin_math::u64::{average};
-
-let midpoint = average(price_a, price_b, rounding::nearest());
-```
-
-A simple operation of `(a + b) / 2` overflows when both values are near the type maximum. `average` uses bit manipulation to avoid the intermediate sum entirely, making it safe regardless of input size. Use it for TWAP midpoints, interpolation, and any pairwise averaging.
-
----
-
-## Safe bit operations: `checked_shl`, `checked_shr`
-
-Move's standard shift operators silently discard bits. `checked_shl` and `checked_shr` return `None` if any non-zero bit would be lost.
-
-```move
-use openzeppelin_math::u64::{checked_shl};
-
-let scaled = checked_shl(1u64, 63u8); // Some(9223372036854775808)
-let overflow = checked_shl(1u64, 64u8); // None: bit pushed out
-```
-
-This is the exact vulnerability class behind the Cetus exploit: a function that checked for overflow on a left shift had an off-by-one in its threshold comparison, allowing a corrupted value through. `checked_shl` catches this by shifting left and then shifting back; if the round-trip doesn't preserve the original value, it returns `None`.
-
-Use these anywhere you are scaling values via bit shifts and need a guarantee that no data is silently discarded.
-
----
-
-## Introspection and logarithms
-
-These functions tell you about the structure of a number. They are used internally by the library and are useful in your own code for tick-math, encoding, and storage optimization.
-
-### `clz` and `msb`
-
-`clz` counts leading zero bits. `msb` returns the position of the most significant bit. Both return 0 for input 0.
-
-```move
-use openzeppelin_math::u64::{clz, msb};
-
-let leading_zeros = clz(256u64); // 55 (bit 8 is set)
-let highest_bit = msb(256u64); // 8
-```
-
-Use these when you need to know the minimum bit width required to represent a value: packing, encoding, or choosing a type width for further computation. Return types vary by module width (`u16` for `u256::clz`, `u8` for narrower types).
-
-### `log2`, `log10`, `log256`
-
-Integer logarithms with configurable rounding. All return 0 for input 0.
-
-```move
-use openzeppelin_math::{rounding};
-use openzeppelin_math::u256::{log2, log10, log256};
-
-let bits_needed = log2(1000u256, rounding::up()); // 10
-let decimal_digits = log10(1000u256, rounding::down()); // 3
-let bytes_needed = log256(1000u256, rounding::up()); // 2
-```
-
-**`log2`** is used in tick-math for concentrated liquidity AMMs, where price ranges map to integer ticks via a logarithmic function. Return type is `u16` for `u256`, `u8` for narrower types.
-
-**`log10`** appears in decimal-aware scaling and display formatting. Returns `u8` across all types.
-
-**`log256`** determines byte-length for encoding and serialization. Returns `u8` across all types.
-
-### `sqrt`
-
-Integer square root with configurable rounding. Returns 0 for input 0.
-
-```move
-use openzeppelin_math::{rounding};
-use openzeppelin_math::u256::{sqrt};
-
-let root_down = sqrt(10u256, rounding::down()); // 3
-let root_up = sqrt(10u256, rounding::up()); // 4
-```
-
-Square roots appear in concentrated liquidity pricing (`sqrtPriceX96`), geometric mean oracles, and volatility calculations. Rounding direction matters: rounding the wrong way in a price calculation creates an exploitable discrepancy.
-
----
-
-## Modular arithmetic: `inv_mod`, `mul_mod`
-
-These functions operate in modular arithmetic, where values wrap around a modulus. They are essential for cryptographic verification, on-chain randomness schemes, and certain pricing algorithms.
-
-### `inv_mod`
-
-Returns the unique `x` where `value * x = 1 (mod modulus)` when the inputs are coprime. Returns `None` otherwise. Aborts if `modulus` is zero.
-
-```move
-use openzeppelin_math::u256::{inv_mod};
-
-let inv = inv_mod(19u256, 1_000_000_007u256); // Some(157_894_738)
-let none = inv_mod(50u256, 100u256); // None: not coprime
-```
-
-### `mul_mod`
-
-Multiplies two values modulo a modulus without intermediate overflow. Uses `u512` internally when both operands exceed `u128::MAX`.
-
-```move
-use openzeppelin_math::u256::{mul_mod};
-
-let product = mul_mod(a, b, 1_000_000_007u256);
-```
-
----
-
-## Decimal scaling
-
-When bridging between tokens with different decimal precisions, `decimal_scaling` handles the multiplier arithmetic safely: overflow-checked on upcasts, explicitly truncating on downcasts.
-
-```move
-use openzeppelin_math::decimal_scaling;
-
-// Convert a 6-decimal token amount to 9-decimal internal representation
-let scaled_up = decimal_scaling::safe_upcast_balance(amount, 6, 9);
-
-// Convert back, truncating any fractional remainder
-let scaled_down = decimal_scaling::safe_downcast_balance(amount_9, 9, 6);
-```
-
-`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.
-
----
-
-## A note on `u512` and integer width
-
-The `openzeppelin_math` package includes a `u512` module that provides 512-bit unsigned integer operations. This module exists to make `u256` arithmetic consistent and overflow-safe. The per-width modules (`u64`, `u128`, `u256`) use `u512` internally for operations like `mul_div`, `mul_shr`, and `mul_mod` where intermediate products can exceed `u256`.
-
-For most protocol development on Sui, `u64` is the standard and recommended integer width. It is what the Sui framework uses for coin balances, timestamps, and gas accounting. Reach for `u128` or `u256` only when your domain genuinely requires wider values (e.g., high-precision fixed-point representations or cross-chain interoperability with systems that use 256-bit integers).
-
-`u512` is not intended for direct use in application code. If you find yourself reaching for it, consider whether the computation can be restructured to use `mul_div` or `mul_shr` on a narrower type instead, which handle the widening internally.
-
----
-
-## Putting it together
-
-Here is a pricing module example that combines several functions from the library:
-
-```move
-use openzeppelin_math::{rounding};
-use openzeppelin_math::u64::{mul_div, sqrt};
-
-const EMathOverflow: u64 = 0;
-
-/// Quote a swap output with a 0.25% fee, rounded to nearest.
-public fun quote_with_fee(amount_in: u64, reserve_in: u64, reserve_out: u64): u64 {
- // Apply fee: amount_in * 997.5 / 1000 (approximated as 9975 / 10000)
- let effective_in = mul_div(amount_in, 9975u64, 10000u64, rounding::down())
- .destroy_or!(abort EMathOverflow);
-
- // Constant-product swap output
- mul_div(reserve_out, effective_in, reserve_in + effective_in, rounding::down())
- .destroy_or!(abort EMathOverflow)
-}
-
-/// Geometric mean of two prices, rounded down.
-public fun geometric_mean(price_a: u64, price_b: u64): u64 {
- let product = mul_div(price_a, price_b, 1u64, rounding::down())
- .destroy_or!(abort EMathOverflow);
- sqrt(product, rounding::down())
-}
-```
-
-Build and test:
-
-```bash
-sui move build
-sui move test
-```
-
----
-
-## Next steps
-
-- [Integer Math package guide](https://docs.openzeppelin.com/contracts-sui/1.x/math) for a quick overview and examples
-- [Integer Math API reference](https://docs.openzeppelin.com/contracts-sui/1.x/api/math) for full function signatures
-- [Access Walkthrough](/contracts-sui/1.x/learn/access-walkthrough) for the Access package walkthrough
-- [Source code](https://github.com/OpenZeppelin/contracts-sui/tree/main/math/core) for inline documentation and implementation details
-- [GitHub issue tracker](https://github.com/OpenZeppelin/contracts-sui/issues) to report bugs or request features
-- [Sui Discord](https://discord.gg/sui) and [Sui Developer Forum](https://forums.sui.io/) to connect with other builders
diff --git a/content/contracts-sui/1.x/math.mdx b/content/contracts-sui/1.x/math.mdx
index 352b11b3..85094d69 100644
--- a/content/contracts-sui/1.x/math.mdx
+++ b/content/contracts-sui/1.x/math.mdx
@@ -2,6 +2,10 @@
title: Integer Math
---
+
+The example code snippets used in this guide are experimental and have not been audited. They are meant to illustrate usage of the OpenZeppelin Sui Package.
+
+
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.
@@ -21,7 +25,7 @@ Import the modules you need:
use openzeppelin_math::{rounding, u64};
```
-## Examples
+## Quickstart examples
### Fee quote with explicit rounding
@@ -71,10 +75,321 @@ public fun scale_down_9_to_6(amount: u256): u64 {
## 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.
+- `u8`, `u16`, `u32`, `u64`, `u128`, `u256`: same API surface across widths for portability. Each module also exposes `is_power_of_ten` for checking decimal scale.
- `decimal_scaling`: decimal conversion between systems using different precision.
- `u512`: wide intermediate support for high-precision arithmetic paths.
+- `vector`: in-place sorting macros (`quick_sort!`, `quick_sort_by!`) for unsigned integer vectors and arbitrary types with a custom comparator.
+
+## Need fractional values?
+
+For prices, fees, ratios, and signed deltas, use the companion [Fixed-Point Math](/contracts-sui/1.x/fixed-point) package. It provides 9-decimal `UD30x9` and `SD29x9` types backed by `u128`, with the same explicit-rounding philosophy as the integer modules above.
+
+---
+
+## Why explicit math matters on-chain
+
+Move's native integer arithmetic aborts on overflow and truncates on division. Both behaviors are silent protocol decisions. An abort at an unexpected boundary can lock a transaction. A truncation that always rounds against your users creates a systematic value leak.
+
+In May 2025, a single flawed overflow check in a shared math library led to the Cetus exploit, where approximately $223 million was drained from the largest DEX on Sui. The bug was not in novel math. It was in a `checked_shl`-type function that silently passed a value it should have rejected, corrupting a fixed-point intermediate that multiple protocols depended on.
+
+`openzeppelin_math` addresses these problems by making every boundary decision explicit: rounding is a named parameter you pass in, overflow returns `Option` that you then handle, and intermediate products are computed in wider types so they don't silently overflow before the final result is computed.
+
+## Rounding as a protocol decision
+
+Every function that divides, shifts, or roots a value requires a `RoundingMode` argument. There is no default, and the choice is not cosmetic. Rounding direction determines which side of a transaction absorbs fractional remainders. In practice, that determines where value leaks.
+
+### The vault example
+
+Consider a vault that issues shares for deposits:
+
+```move
+use openzeppelin_math::{rounding, u64 as math_u64};
+
+const EMathOverflow: u64 = 0;
+
+/// Deposit: convert tokens to shares, rounding DOWN.
+/// The depositor receives slightly fewer shares than the exact ratio.
+/// The vault keeps the fractional remainder.
+public fun deposit_shares(amount: u64, total_assets: u64, total_supply: u64): u64 {
+ math_u64::mul_div(amount, total_supply, total_assets, rounding::down())
+ .destroy_or!(abort EMathOverflow)
+}
+
+/// Withdrawal: convert shares to tokens, rounding DOWN.
+/// The withdrawer receives slightly fewer tokens than the exact ratio.
+/// The vault keeps the fractional remainder.
+public fun withdraw_assets(shares: u64, total_assets: u64, total_supply: u64): u64 {
+ math_u64::mul_div(shares, total_assets, total_supply, rounding::down())
+ .destroy_or!(abort EMathOverflow)
+}
+```
+
+Both directions round down so the vault never gives away more than it holds. If you rounded up on deposits (giving the depositor extra shares) or up on withdrawals (giving the withdrawer extra tokens), an attacker could repeatedly deposit and withdraw small amounts, extracting the rounding remainder each time. Making rounding explicit in every call forces you to make this decision consciously and makes it visible in code review.
+
+### When to use each mode
+
+- **`rounding::down()`** rounds toward zero (truncation). Use when the caller should absorb the remainder. This is the safe default for most protocol-to-user calculations.
+- **`rounding::up()`** rounds away from zero (ceiling). Use when the protocol should absorb the remainder, or when you need a conservative upper bound.
+- **`rounding::nearest()`** rounds to the nearest integer, with ties rounded up (round-half-up). Use for fee quotes and display calculations where rounding against either party on non-tie values is undesirable.
+
+## Handling `Option` at the boundary
+
+Every function whose result could exceed the type range returns `Option`. This includes `mul_div`, `mul_shr`, `checked_shl`, `checked_shr`, and `inv_mod`. Functions that cannot overflow (`average`, `sqrt`, logarithms) return the value directly.
+
+### Pattern: abort with a domain-specific error
+
+The most common pattern. Define an error constant in your module and abort if the math overflows:
+
+```move
+use openzeppelin_math::rounding;
+use openzeppelin_math::u64::{mul_div};
+
+const EMathOverflow: u64 = 0;
+
+public fun compute_fee(amount: u64, fee_bps: u64): u64 {
+ mul_div(amount, fee_bps, 10_000u64, rounding::up())
+ .destroy_or!(abort EMathOverflow)
+}
+```
+
+### Pattern: fallback to a safe value
+
+Useful when overflow means "cap at maximum" rather than "this is an error":
+
+```move
+use openzeppelin_math::{rounding};
+use openzeppelin_math::u64::{mul_div};
+
+public fun capped_scale(value: u64, multiplier: u64): u64 {
+ let result = mul_div(value, multiplier, 1_000u64, rounding::down());
+ if (result.is_some()) {
+ result.destroy_some()
+ } else {
+ 18_446_744_073_709_551_615u64 // u64::MAX
+ }
+}
+```
+
+### Pattern: propagate to the caller
+
+When your function is itself a building block, return `Option` and let the caller decide:
+
+```move
+use openzeppelin_math::{rounding};
+use openzeppelin_math::u64::{mul_div};
+
+public fun try_compute_shares(amount: u64, total_assets: u64, total_supply: u64): Option {
+ mul_div(amount, total_supply, total_assets, rounding::down())
+}
+```
+
+## Core arithmetic: `mul_div`, `mul_shr`, `average`
+
+These three functions cover the fundamental operations behind swap pricing, fee calculations, interest accrual, and share-based accounting.
+
+
+The functions shown below are simplified to illustrate their usage. In a real implementation, they must be defined within proper Move function syntax.
+
+
+### `mul_div`
+
+Computes `(a * b) / denominator` with explicit rounding. The intermediate product is computed in a wider type (up to `u512` for `u256` inputs) so it never silently overflows. Returns `Option`.
+
+```move
+use openzeppelin_math::{rounding, u256};
+use openzeppelin_math::u256::{mul_div};
+
+// Price calculation: (reserve_out * amount_in) / (reserve_in + amount_in)
+let output = mul_div(reserve_out, amount_in, reserve_in + amount_in, rounding::down());
+```
+
+Instead of carrying out any multiply-then-divide operation manually (e.g., `(a * b) / c`), use `mul_div`. The manual version overflows when `a * b` exceeds the type range, even if the final result after division would have been safe.
+
+### `mul_shr`
+
+Computes `(a * b) >> shift` with rounding. Equivalent to `mul_div` with a power-of-two denominator, but faster. Returns `Option`.
+
+```move
+use openzeppelin_math::{rounding, u128};
+use openzeppelin_math::u128::{mul_shr};
+
+// Fixed-point multiplication: (price * quantity) / 2^64
+let result = mul_shr(price, quantity, 64u8, rounding::down());
+```
+
+Use `mul_shr` instead of `mul_div` when your denominator is a power of two. This is common in Q-notation fixed-point math (Q64.64, Q96, etc.) and the tick-math used in concentrated liquidity AMMs. The Cetus exploit involved exactly this class of operation.
+
+### `average`
+
+Computes the arithmetic mean of two values without overflow. Does not return `Option` since the result always fits in the input type.
+
+```move
+use openzeppelin_math::{rounding, u64};
+use openzeppelin_math::u64::{average};
+
+let midpoint = average(price_a, price_b, rounding::nearest());
+```
+
+A simple operation of `(a + b) / 2` overflows when both values are near the type maximum. `average` uses bit manipulation to avoid the intermediate sum entirely, making it safe regardless of input size. Use it for TWAP midpoints, interpolation, and any pairwise averaging.
+
+## Safe bit operations: `checked_shl`, `checked_shr`
+
+Move's standard shift operators silently discard bits. `checked_shl` and `checked_shr` return `None` if any non-zero bit would be lost.
+
+```move
+use openzeppelin_math::u64::{checked_shl};
+
+let scaled = checked_shl(1u64, 63u8); // Some(9223372036854775808)
+let overflow = checked_shl(1u64, 64u8); // None: bit pushed out
+```
+
+This is the exact vulnerability class behind the Cetus exploit: a function that checked for overflow on a left shift had an off-by-one in its threshold comparison, allowing a corrupted value through. `checked_shl` catches this by shifting left and then shifting back; if the round-trip doesn't preserve the original value, it returns `None`.
+
+Use these anywhere you are scaling values via bit shifts and need a guarantee that no data is silently discarded.
+
+## Introspection and logarithms
+
+These functions tell you about the structure of a number. They are used internally by the library and are useful in your own code for tick-math, encoding, and storage optimization.
+
+### `clz` and `msb`
+
+`clz` counts leading zero bits. `msb` returns the position of the most significant bit. Both return 0 for input 0.
+
+```move
+use openzeppelin_math::u64::{clz, msb};
+
+let leading_zeros = clz(256u64); // 55 (bit 8 is set)
+let highest_bit = msb(256u64); // 8
+```
+
+Use these when you need to know the minimum bit width required to represent a value: packing, encoding, or choosing a type width for further computation. Return types vary by module width (`u16` for `u256::clz`, `u8` for narrower types).
+
+### `log2`, `log10`, `log256`
+
+Integer logarithms with configurable rounding. All return 0 for input 0.
+
+```move
+use openzeppelin_math::{rounding};
+use openzeppelin_math::u256::{log2, log10, log256};
+
+let bits_needed = log2(1000u256, rounding::up()); // 10
+let decimal_digits = log10(1000u256, rounding::down()); // 3
+let bytes_needed = log256(1000u256, rounding::up()); // 2
+```
+
+**`log2`** is used in tick-math for concentrated liquidity AMMs, where price ranges map to integer ticks via a logarithmic function. Return type is `u16` for `u256`, `u8` for narrower types.
+
+**`log10`** appears in decimal-aware scaling and display formatting. Returns `u8` across all types.
+
+**`log256`** determines byte-length for encoding and serialization. Returns `u8` across all types.
+
+### `sqrt`
+
+Integer square root with configurable rounding. Returns 0 for input 0.
+
+```move
+use openzeppelin_math::{rounding};
+use openzeppelin_math::u256::{sqrt};
+
+let root_down = sqrt(10u256, rounding::down()); // 3
+let root_up = sqrt(10u256, rounding::up()); // 4
+```
+
+Square roots appear in concentrated liquidity pricing (`sqrtPriceX96`), geometric mean oracles, and volatility calculations. Rounding direction matters: rounding the wrong way in a price calculation creates an exploitable discrepancy.
+
+## Modular arithmetic: `inv_mod`, `mul_mod`
+
+These functions operate in modular arithmetic, where values wrap around a modulus. They are essential for cryptographic verification, on-chain randomness schemes, and certain pricing algorithms.
+
+### `inv_mod`
+
+Returns the unique `x` where `value * x = 1 (mod modulus)` when the inputs are coprime. Returns `None` otherwise. Aborts if `modulus` is zero.
+
+```move
+use openzeppelin_math::u256::{inv_mod};
+
+let inv = inv_mod(19u256, 1_000_000_007u256); // Some(157_894_738)
+let none = inv_mod(50u256, 100u256); // None: not coprime
+```
+
+### `mul_mod`
+
+Multiplies two values modulo a modulus without intermediate overflow. Uses `u512` internally when both operands exceed `u128::MAX`.
+
+```move
+use openzeppelin_math::u256::{mul_mod};
+
+let product = mul_mod(a, b, 1_000_000_007u256);
+```
+
+## Decimal scaling
+
+When bridging between tokens with different decimal precisions, `decimal_scaling` handles the multiplier arithmetic safely: overflow-checked on upcasts, explicitly truncating on downcasts.
+
+```move
+use openzeppelin_math::decimal_scaling;
+
+// Convert a 6-decimal token amount to 9-decimal internal representation
+let scaled_up = decimal_scaling::safe_upcast_balance(amount, 6, 9);
+
+// Convert back, truncating any fractional remainder
+let scaled_down = decimal_scaling::safe_downcast_balance(amount_9, 9, 6);
+```
+
+`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.
+
+## A note on `u512` and integer width
+
+The `openzeppelin_math` package includes a `u512` module that provides 512-bit unsigned integer operations. This module exists to make `u256` arithmetic consistent and overflow-safe. The per-width modules (`u64`, `u128`, `u256`) use `u512` internally for operations like `mul_div`, `mul_shr`, and `mul_mod` where intermediate products can exceed `u256`.
+
+For most protocol development on Sui, `u64` is the standard and recommended integer width. It is what the Sui framework uses for coin balances, timestamps, and gas accounting. Reach for `u128` or `u256` only when your domain genuinely requires wider values (e.g., high-precision fixed-point representations or cross-chain interoperability with systems that use 256-bit integers).
+
+`u512` is not intended for direct use in application code. If you find yourself reaching for it, consider whether the computation can be restructured to use `mul_div` or `mul_shr` on a narrower type instead, which handle the widening internally.
+
+## Putting it together
+
+Here is a pricing module example that combines several functions from the library:
+
+```move
+use openzeppelin_math::{rounding};
+use openzeppelin_math::u64::{mul_div, sqrt};
+
+const EMathOverflow: u64 = 0;
+
+/// Quote a swap output with a 0.25% fee, rounded to nearest.
+public fun quote_with_fee(amount_in: u64, reserve_in: u64, reserve_out: u64): u64 {
+ // Apply fee: amount_in * 997.5 / 1000 (approximated as 9975 / 10000)
+ let effective_in = mul_div(amount_in, 9975u64, 10000u64, rounding::down())
+ .destroy_or!(abort EMathOverflow);
+
+ // Constant-product swap output
+ mul_div(reserve_out, effective_in, reserve_in + effective_in, rounding::down())
+ .destroy_or!(abort EMathOverflow)
+}
+
+/// Geometric mean of two prices, rounded down.
+public fun geometric_mean(price_a: u64, price_b: u64): u64 {
+ let product = mul_div(price_a, price_b, 1u64, rounding::down())
+ .destroy_or!(abort EMathOverflow);
+ sqrt(product, rounding::down())
+}
+```
+
+Build and test:
+
+```bash
+sui move build
+sui move test
+```
## API Reference
-Use the full function-level reference here: [Integer Math API](/contracts-sui/1.x/api/math).
+For function-level signatures and parameters, see the [Integer Math API reference](/contracts-sui/1.x/api/math).
+
+## Next steps
+
+- [Integer Math API reference](/contracts-sui/1.x/api/math) for full function signatures
+- [Fixed-Point Math](/contracts-sui/1.x/fixed-point) for fractional values, prices, and signed deltas
+- [Access](/contracts-sui/1.x/access) for ownership-transfer policies on privileged capabilities
+- [GitHub issue tracker](https://github.com/OpenZeppelin/contracts-sui/issues) to report bugs or request features
+- [Sui Discord](https://discord.gg/sui) and [Sui Developer Forum](https://forums.sui.io/) to connect with other builders
diff --git a/content/contracts-sui/index.mdx b/content/contracts-sui/index.mdx
index 038dfaf9..a045cc1f 100644
--- a/content/contracts-sui/index.mdx
+++ b/content/contracts-sui/index.mdx
@@ -12,19 +12,19 @@ import { latestStable } from "./latest-versions.js";
Install packages, set up a Move project, and run a first end-to-end integration.
-
- In-depth walkthroughs covering safe arithmetic, rounding, overflow handling, and ownership transfer policies.
-
## Packages
- Package-level guide for math primitives, including intent, module map, and usage boundaries.
+ Deterministic integer arithmetic with explicit rounding, overflow-safe `mul_div`/`mul_shr`, and decimal scaling helpers.
+
+
+ 9-decimal fixed-point types (`UD30x9`, `SD29x9`) for prices, fees, rates, and signed balance deltas.
- Package-level guide for access primitives, including transfer policy selection and safety models.
+ Ownership-transfer policies (`two_step_transfer`, `delayed_transfer`) for privileged capabilities.
@@ -32,7 +32,10 @@ import { latestStable } from "./latest-versions.js";
- Explore the complete math API, including all functions and types.
+ Explore the complete integer math API, including all functions and types.
+
+
+ Explore the complete fixed-point math API, including the `UD30x9` and `SD29x9` types and their arithmetic, comparison, and conversion modules.
Explore the complete access API reference, including module-level functions, core types, emitted events, and expected error conditions for integration.
diff --git a/skills/docs-sync/SKILL.md b/skills/docs-sync/SKILL.md
new file mode 100644
index 00000000..b38855cd
--- /dev/null
+++ b/skills/docs-sync/SKILL.md
@@ -0,0 +1,237 @@
+---
+name: docs-sync
+description: Use this skill to update the centralized OpenZeppelin docs repo after a smart contracts library changes. Trigger after a contracts release, a merged PR in a contracts library, or any time a docs slice (library + version) needs to be brought back in line with the contracts repo. The skill takes two contract commits (BASE_COMMIT, HEAD_COMMIT) as the source of truth and produces docs edits that follow the Diátaxis framework. Use whenever the user says "sync docs for contracts-sui", "update docs after the contracts PR", "regenerate API reference for v1.x", or "docs-sync".
+allowed-tools: Read, Write, Edit, Glob, Grep, Bash
+inputs:
+ mode:
+ type: choice
+ prompt: "Run mode. interactive shows structured proposals at each gate and waits for approval; automatic infers from config and runs end-to-end without prompts."
+ options:
+ - interactive
+ - automatic
+ default: interactive
+ required: false
+ contracts_repo_path:
+ type: text
+ prompt: "Absolute filesystem path to the contracts repo (the source of truth). Must be a git repo, must NOT be the same as the docs repo."
+ required: true
+ docs_repo_path:
+ type: text
+ prompt: "Absolute filesystem path to the docs repo (where edits will land). Leave blank to use the current working directory."
+ required: false
+ base_commit:
+ type: text
+ prompt: "BASE_COMMIT — git SHA, tag, or ref in the contracts repo marking the START of the diff (typically the previous release tag)."
+ required: true
+ head_commit:
+ type: text
+ prompt: "HEAD_COMMIT — git SHA, tag, or ref in the contracts repo marking the END of the diff (typically the new release tag, or HEAD)."
+ required: true
+ library_id:
+ type: text
+ prompt: "LIBRARY_ID — the docs slice to update (e.g., contracts-sui). Must match a config file at skills/docs-sync/config/libraries/.yml."
+ required: true
+ docs_version:
+ type: text
+ prompt: "DOCS_VERSION — the version directory inside the slice to edit (e.g., 1.x)."
+ required: true
+ release_version:
+ type: text
+ prompt: "RELEASE_VERSION — the contracts release tag this sync targets (e.g., v1.2.0). Used for source-link version pins and release notes."
+ required: true
+ docs_update_scope:
+ type: choice
+ prompt: "Scope of docs to update. full = everything the matrix requires; api-only = API reference + nav + source links only; guides-only = guides/tutorials/explanations + snippets only."
+ options:
+ - full
+ - api-only
+ - guides-only
+ default: full
+ required: false
+ target_audience:
+ type: text
+ prompt: "TARGET_AUDIENCE override. Leave blank to use the value in the slice config."
+ required: false
+ docs_tone:
+ type: text
+ prompt: "DOCS_TONE override. Leave blank to use the value in the slice config."
+ required: false
+---
+
+# docs-sync
+
+Use this skill when the contracts library has changed and the centralized
+docs repo needs to be brought back in sync. The skill lives **inside the
+docs repo**; the contracts repo is supplied at runtime.
+
+## When to use
+
+- A contracts library has merged a PR or cut a release and the docs need
+ updating.
+- The user names a docs slice and two commits in the contracts repo.
+- The user asks to "sync docs", "update API reference for ``",
+ or "document the changes between commit A and commit B".
+
+Do **not** trigger this skill for:
+
+- Pure docs cleanup unrelated to contracts changes (use a regular edit).
+- Changes that have not yet landed in a commit on the contracts repo.
+
+## Inputs
+
+All required inputs are declared in the frontmatter `inputs` schema and
+gathered by the harness before the skill body runs. The skill body must
+not prompt for inputs interactively; if a required input is missing or
+fails validation, return a clear error message that names the field and
+let the caller re-invoke.
+
+The full input set is:
+
+| Input | Required | Notes |
+|----------------------|----------|--------------------------------------------------|
+| `mode` | no | `interactive` (default) or `automatic` |
+| `contracts_repo_path`| yes | absolute path; must be a git repo |
+| `docs_repo_path` | no | defaults to current working directory |
+| `base_commit` | yes | git SHA / tag / ref in the contracts repo |
+| `head_commit` | yes | git SHA / tag / ref in the contracts repo |
+| `library_id` | yes | must have `config/libraries/.yml` |
+| `docs_version` | yes | e.g. `1.x` |
+| `release_version` | yes | e.g. `v1.2.0` |
+| `docs_update_scope` | no | `full` (default), `api-only`, `guides-only` |
+| `target_audience` | no | overrides config |
+| `docs_tone` | no | overrides config |
+
+`process.md` Step 0 validates these and applies safe defaults. In
+`automatic` mode, missing or invalid required inputs are a hard failure
+— never invent SHAs, paths, or versions.
+
+## Source-of-truth constraint
+
+The contracts diff is computed by the agent itself, from two commits:
+
+```
+git -C diff ..
+```
+
+The skill must **not** accept any of the following as the primary source
+of truth:
+
+- Raw pasted diffs.
+- Patch files.
+- Uncommitted local changes.
+- PR descriptions or commit messages.
+- Informal change summaries.
+
+## Mode behavior
+
+- **interactive**: Runs through structured proposal gates (see below).
+ Stops after each gate and waits for the user to approve, refine, or
+ reject the plan. API reference edits within an approved plan proceed
+ deterministically; broad guide/tutorial/explanation rewrites need
+ per-page approval.
+- **automatic**: Skips all gates. Applies every matrix-required update
+ within scope, records every assumption in the report. If a required
+ decision cannot be inferred safely, stops or records
+ `needs-human-review` rather than fabricating content.
+
+## Interactive gates
+
+Interactive runs **MUST** stop at each of the following gates and wait
+for explicit user approval before continuing. The skill does this by
+emitting a structured proposal block and ending its turn — the user's
+next message is the approval (or refinement). Do not use `AskUserQuestion` or any other interactive tool at any gate; gates are pure text + end-of-turn.
+
+| Gate | Stop point | What is shown |
+|---------------------------|------------------------|--------------------------------------------------------------------------------------------|
+| **G1 — Inputs & config** | after Step 2 | resolved inputs, slice paths, automation defaults; flag missing/odd values |
+| **G2 — Public API delta** | after Step 9 | added / removed / changed modules, functions, structs, events, errors with signatures |
+| **G3 — Docs edit plan** | after Step 11 | every doc file to be **created**, **edited**, or **deleted**, grouped by category, with the API items each one covers and matrix-required pages that are out of scope |
+| **G4 — Per-page rewrites**| before each broad rewrite in Step 13 | the file path, the sections to be rewritten, and the rationale (Diátaxis category, matrix row) |
+
+Each proposal block uses the templates in `references/proposals/` (see
+that directory's `README.md`). Approval phrases the skill should accept:
+"approve", "looks good", "proceed", "yes". Anything else is treated as a
+refinement — apply the requested changes, re-emit the proposal, and wait
+again.
+
+In `automatic` mode, gates are skipped and the proposal contents are
+written to the final report instead.
+
+## Scope behavior
+
+- `full`: apply the full matrix.
+- `api-only`: update API reference, source links, stale identifiers in
+ API pages, and API navigation only. Report skipped guide/tutorial/
+ explanation/example/release-note work as out of scope.
+- `guides-only`: update guides, tutorials, explanations, and snippets.
+ Still inspect the API diff so stale prose can be found, but do not
+ regenerate reference pages.
+
+(Targeted scope by path list is not exposed as an input; if a caller
+needs it, they can refine the G3 plan before approving.)
+
+## Config memory
+
+Always load:
+
+```
+skills/docs-sync/config/libraries/.yml
+```
+
+This file is committed to the docs repo and is the canonical source of
+docs slice paths, navigation system, examples style, security rules, and
+automation defaults. Read `references/rules/config-rules.md` only when
+config is missing, malformed, or needs a durable convention update.
+
+All `docs.*` paths in config are **relative to ``**. Never
+hard-code absolute filesystem paths in config, prompts, reports, or
+generated docs.
+
+## Reference Files
+
+Read only what the run needs:
+
+1. `process.md` — the deterministic end-to-end workflow.
+2. `references/proposals/` — proposal-block templates for the G1–G4 gates.
+3. `references/rules/change-classification.md` — how to classify each
+ contract change.
+4. `references/rules/doc-update-matrix.md` — what docs each change type
+ forces.
+5. `references/rules/api-reference-rules.md` — required for API
+ reference updates.
+6. `references/rules/diataxis-rules.md` — required when creating or
+ reshaping tutorial, guide, explanation, or reference content.
+7. Templates under `references/templates/` — only when creating new
+ pages or replacing a broken structure.
+8. `references/rules/docs-pr-checklist.md` and
+ `references/reports/docs-sync-report-template.md` — final validation
+ and summary.
+
+Use the validation specs under `references/checks/` as check
+definitions. They are not executable scripts unless later replaced with
+real tooling.
+
+## Scope of edits
+
+Edit only:
+
+- Files inside the resolved docs slice
+ (`docs.library_root` and `docs.version_root` from config, recursively).
+- The matching navigation config (`docs.nav_config_path`).
+- Shared example files **only if** the matrix says they must change.
+
+Do **not**:
+
+- Touch other docs slices in the centralized repo.
+- Create or edit `meta.json` files. The docs repo uses centralized
+ navigation under `src/navigation/` — see the local conventions file
+ at `docs.local_conventions_path` for slice-specific rules.
+- Modify source/doc comments in the contracts repo unless
+ `automation.allow_source_comment_edits: true` **and** the user asked.
+
+## End of run
+
+Run through `references/rules/docs-pr-checklist.md` before claiming the
+run is done. End with a compact docs-sync report following
+`references/reports/docs-sync-report-template.md`. Do not create a
+report file unless the user asks for one.
diff --git a/skills/docs-sync/config/libraries/contracts-sui.yml b/skills/docs-sync/config/libraries/contracts-sui.yml
new file mode 100644
index 00000000..bd12f44f
--- /dev/null
+++ b/skills/docs-sync/config/libraries/contracts-sui.yml
@@ -0,0 +1,74 @@
+# docs-sync configuration for the OpenZeppelin Contracts for Sui docs slice.
+#
+# All paths are RELATIVE to . Local filesystem paths
+# must NOT be added to this file; they are runtime inputs to the agent.
+
+library:
+ id: "contracts-sui"
+ language: "MOVE_SUI"
+
+docs:
+ content_root: "content"
+ library_root: "content/contracts-sui"
+ version: "1.x"
+ version_root: "content/contracts-sui/1.x"
+ api_reference_path: "content/contracts-sui/1.x/api"
+ product_index_path: "content/contracts-sui/index.mdx"
+ version_index_path: "content/contracts-sui/1.x/index.mdx"
+ local_conventions_path: "content/contracts-sui/AGENTS.md"
+ nav_config_path: "src/navigation/sui/current.json"
+ tone: "clear, precise, security-conscious"
+ target_audience: "smart contract developers integrating OpenZeppelin libraries"
+
+navigation:
+ system: "central-json"
+ forbid_meta_json: true
+ # Categories the agent must keep in sync inside nav_config_path. New modules
+ # must appear under both "Packages" and "API Reference" folders.
+ required_folders:
+ - "Learn"
+ - "Packages"
+ - "API Reference"
+
+examples:
+ compile_command: ""
+ snippet_validation: "syntax"
+ # Where in-repo example projects live, if any. Empty when examples are
+ # only embedded as code fences in MDX files.
+ examples_root: ""
+
+security:
+ warning_style: "explicit warning callouts for security-sensitive behavior"
+ require_security_sections_for:
+ - access_control
+ - ownership
+ - authorization
+ - asset_transfer
+ - upgradeability
+ - cryptography
+ - signature_verification
+
+release:
+ # GitHub source URL used by APIGithubLinkHeader and similar components.
+ # Pinned per release tag at runtime by the agent.
+ source_repo_url: "https://github.com/OpenZeppelin/contracts-sui"
+
+automation:
+ default_mode: "interactive"
+ allow_source_comment_edits: false
+ fail_on_unresolved_placeholders: true
+
+# API Reference layout assumptions specific to this slice. Each Move package
+# is documented as a single MDX file under api_reference_path, named after
+# the package (kebab-case if multi-word). Modules inside that package render
+# as -style sections. See content/contracts-sui/AGENTS.md for the
+# canonical entry order (description, then Aborts, then Emits).
+api_reference_layout:
+ page_per: "package"
+ module_section_component: "APIItem"
+ github_link_component: "APIGithubLinkHeader"
+ entry_order:
+ - "description"
+ - "aborts"
+ - "emits"
+ - "callouts"
diff --git a/skills/docs-sync/process.md b/skills/docs-sync/process.md
new file mode 100644
index 00000000..72c4a08a
--- /dev/null
+++ b/skills/docs-sync/process.md
@@ -0,0 +1,561 @@
+# docs-sync process
+
+A deterministic, top-to-bottom workflow. Steps run in order. Do not
+reorder. If a step fails, stop and surface the failure rather than
+skipping ahead.
+
+All paths in this document are relative to `` unless
+explicitly prefixed with ``.
+
+In `interactive` mode, the workflow stops at four gates (G1–G4) and
+waits for explicit user approval. The skill MUST emit a structured
+proposal block (templates under `references/proposals/`) and end its
+turn at each gate. The skill MUST NOT call `AskUserQuestion` or any
+other interactive tool — inputs are gathered by the harness from the
+frontmatter `inputs` schema before this process starts. In `automatic`
+mode, every gate is skipped and its proposal contents are appended to
+the final report.
+
+---
+
+## Step 0 — Validate runtime inputs
+
+Inputs arrive from the frontmatter `inputs` schema (the harness gathers
+them upfront). Step 0 validates them and applies safe defaults — it
+does not prompt the user.
+
+### 0.1 Apply defaults
+
+For each input that arrived empty, apply:
+
+- `mode`: read `automation.default_mode` from the resolved slice config
+ if present; otherwise `interactive`.
+- `docs_repo_path`: current working directory, **only if** it is the
+ docs repo (verify by checking that `skills/docs-sync/SKILL.md` exists
+ inside it). Otherwise this is a hard error.
+- `docs_update_scope`: `full`.
+- `target_audience` / `docs_tone`: read from slice config; do not invent
+ values.
+
+Record every default that was applied so the G1 proposal and the final
+report can list them as assumptions.
+
+### 0.2 Validate
+
+For each input, validate inline. On failure, **stop** and return a
+single error message that names every failing field — do not partially
+proceed.
+
+- `contracts_repo_path` exists and is a git repo:
+ `git -C rev-parse --is-inside-work-tree`.
+- `docs_repo_path` exists and is a git repo.
+- `contracts_repo_path` is **not** the same path as `docs_repo_path`.
+- `library_id` has a config file at
+ `skills/docs-sync/config/libraries/.yml`.
+- `base_commit` and `head_commit` resolve in the contracts repo:
+ `git -C rev-parse --verify ^{commit}`.
+
+In `automatic` mode, any validation failure is a hard error — never
+invent SHAs, paths, or versions.
+
+In `interactive` mode, the validation error is the response — the user
+re-invokes with corrected inputs. The skill MUST NOT retry on its own.
+
+## Step 1 — Confirm mode
+
+The mode arrives as input. This step normalizes it for the rest of the
+process:
+
+- `interactive`: gates G1–G4 are active; broad rewrites need per-page
+ approval at G4.
+- `automatic`: gates are skipped; assumptions go into the final report.
+
+## Step 2 — Load config memory
+
+1. Resolve ``:
+ `skills/docs-sync/config/libraries/.yml`.
+2. If the file does not exist, **stop** with a clear error. Do not
+ fabricate a config.
+3. Parse the file as YAML.
+4. Read `library.id`, `library.language`, `docs.*`, `navigation.*`,
+ `examples.*`, `security.*`, `release.*`, `automation.*`.
+5. Treat all `docs.*` paths as relative to ``.
+6. Apply optional overrides from runtime: `target_audience` overrides
+ `docs.target_audience`; `docs_tone` overrides `docs.tone`.
+
+---
+
+## 🛑 Gate G1 — Inputs & config confirmation
+
+Active in `interactive` mode only. After Step 2 completes successfully,
+emit the **G1 proposal block** (template:
+`references/proposals/g1-inputs-and-config.md`) and **end the turn**.
+
+The proposal block MUST contain:
+
+- All resolved inputs, with each defaulted value clearly marked
+ `(default applied)`.
+- The resolved slice paths (``, ``,
+ ``, ``,
+ ``).
+- Mode and scope, with one-line consequences for each.
+- Anything missing, ambiguous, or surprising flagged with `⚠️`.
+- A trailing line: `Reply "approve" to proceed, or describe corrections.`
+
+When the user replies:
+
+- "approve" / "looks good" / "proceed" / "yes" → continue to Step 3.
+- Anything else → treat as refinement, apply the requested changes,
+ re-emit the G1 block, and wait again.
+
+In `automatic` mode, append the G1 contents to the final report's
+"Resolved inputs" section and continue to Step 3 without stopping.
+
+---
+
+## Step 3 — Resolve contracts repo and docs repo
+
+(Validation in Step 0.2 already covers most of this; redo the checks
+that depend on filesystem state at run time.)
+
+1. Confirm the docs repo's working tree is clean **enough** to make
+ reviewable edits (no unrelated unstaged changes that would muddy the
+ diff). Warn but continue if not.
+
+## Step 4 — Verify base and head commits ancestry
+
+Step 0.2 already verified both commits resolve. Now confirm
+`` is an ancestor of ``:
+
+```
+git -C merge-base --is-ancestor
+```
+
+If not, warn (the diff will still be computed, but the relationship is
+unusual — record it in the report).
+
+## Step 5 — Compute the contracts diff
+
+```
+git -C diff --stat ..
+git -C diff ..
+```
+
+Persist the output for use in Steps 6–11. Also gather:
+
+```
+git -C log --pretty=oneline ..
+```
+
+…to label classifications in the report.
+
+This diff is the **only** accepted source of truth for what changed.
+Anything in chat (PR descriptions, summaries, pasted snippets) is
+context, not authority.
+
+## Step 6 — Resolve the target docs slice
+
+Resolve and verify each slice path on disk:
+
+| Variable | Source |
+| ------------------------------ | ---------------------------------------------- |
+| `` | `docs.content_root` |
+| `` | `docs.library_root` |
+| `` | `docs.version_root` |
+| `` | `docs.api_reference_path` |
+| `` | `docs.nav_config_path` |
+| ``| `docs.local_conventions_path` |
+| `` | `docs.product_index_path` |
+| `` | `docs.version_index_path` |
+
+For each, check that the file or directory exists under
+``. For missing pieces:
+
+1. If `docs.library_root` exists but `docs.version_root` does not, stop;
+ a new version needs an explicit setup decision.
+2. If `docs.api_reference_path` is missing and the scoped run requires
+ API reference updates, create it and record the assumption.
+3. If `docs.nav_config_path` is missing, stop; do not invent navigation.
+4. If `docs.local_conventions_path` is missing, fall back to repo-root
+ `AGENTS.md` and record the fallback.
+
+## Step 7 — Read local docs conventions
+
+If `` exists, read it and treat it as
+authoritative for that docs slice (formatting, code style in snippets,
+section ordering, naming conventions, table formats, etc.).
+
+If it does not exist, fall back to the repo-root `AGENTS.md` (e.g.
+`/AGENTS.md`) for global rules and record the fallback.
+
+For Sui specifically:
+
+- Local conventions live at `content/contracts-sui/AGENTS.md`.
+- API Reference function entry order is **description, then `Aborts ...`,
+ then `Emits ...`**, with only NOTE/INFO/WARNING blocks afterwards.
+
+## Step 8 — Inspect contract changes
+
+From the diff in Step 5, group hunks by file and by language construct.
+For each changed file, record:
+
+- File path (relative to ``).
+- Whether it is part of the public API surface (sources/, package
+ manifests) or internal (tests/, scripts/, internal helpers).
+- The high-level construct affected: module, struct, function, constant,
+ event, error, ability, capability, doc comment.
+
+Skip files outside the contracts library's documented surface (CI,
+internal tooling, vendored libs). Record what was skipped.
+
+## Step 9 — Extract or compare the public API surface
+
+Use `references/checks/extract_public_api.md` as the extraction
+specification to extract the public API surface at both ``
+and ``. Use `references/checks/compare_public_api.md` as
+the comparison spec. If executable project tooling exists, prefer it and
+record the command. Produce a structured list:
+
+- Added modules / packages.
+- Removed modules / packages.
+- Added public functions, with full signatures.
+- Removed public functions, with full signatures.
+- Changed function signatures (parameter types/order, return type,
+ visibility, abilities) — show **before → after** signatures.
+- Added or changed structs/types and their fields/abilities.
+- Added or removed constants.
+- Added or removed events.
+- Added or removed errors / abort codes.
+- Changed doc comments on public items.
+
+The spec is language-agnostic; for `MOVE_SUI`, parse `module ...`
+declarations, `public fun`/`entry` signatures, `struct ... has ...`
+declarations, `const`s, error constants (`E*`), and event structs.
+
+This structured list is the input to Gate G2 below — make every entry
+self-explanatory (full signature, full module path) so the user can
+review without reopening source files.
+
+---
+
+## 🛑 Gate G2 — Public API delta confirmation
+
+Active in `interactive` mode only. After Step 9 completes, emit the
+**G2 proposal block** (template:
+`references/proposals/g2-api-delta.md`) and **end the turn**.
+
+The proposal block MUST contain, for each category, a bulleted list of
+items with full signatures:
+
+```
+### Added public functions
+- `module::path::function_name(arg: Type, ...): ReturnType`
+- ...
+
+### Removed public functions
+- `module::path::function_name(arg: Type, ...): ReturnType`
+- ...
+
+### Changed function signatures
+- `module::path::function_name`
+ - before: `(old_arg: OldType): OldReturn`
+ - after: `(new_arg: NewType): NewReturn`
+- ...
+
+### Added / removed / changed structs, events, errors, constants
+- ...
+```
+
+Also include:
+
+- Total counts per category (`Added: 5 functions, 2 structs, …`).
+- A "Skipped (out of public surface)" list of contracts files Step 8
+ excluded, so the user can spot a wrongly-skipped file.
+- A trailing line: `Reply "approve" to proceed to docs planning, or
+ describe corrections (e.g. "treat foo as private", "include bar/").`
+
+When the user replies:
+
+- "approve" / "looks good" / "proceed" / "yes" → continue to Step 10.
+- Anything else → treat as refinement to the API surface (re-classify,
+ re-extract, etc.), re-emit the G2 block, and wait again.
+
+In `automatic` mode, append the G2 contents to the final report's
+"Public API delta" section and continue to Step 10 without stopping.
+
+---
+
+## Step 10 — Classify changes
+
+Apply `references/rules/change-classification.md` to the structured
+list from Step 9 and to the raw diff from Step 5. Each change becomes
+one or more rows of:
+
+| File / construct | Classification | Evidence (commit, hunk) |
+
+Classification values are exactly the names in
+`references/rules/change-classification.md`.
+
+## Step 11 — Decide required docs updates
+
+For each classified change, look up the corresponding row in
+`references/rules/doc-update-matrix.md`. The matrix tells you:
+
+- Whether API reference must update.
+- Whether a guide, tutorial, or explanation must update.
+- Whether examples must update.
+- Whether navigation must update.
+- Whether changelog/release notes must update.
+- Whether a security warning is required.
+- The default behavior in automatic mode.
+- The questions to ask in interactive mode.
+
+Aggregate the required updates per docs page so the same page is not
+edited multiple times in incompatible ways.
+
+Apply ``:
+
+- `full`: keep every matrix-required update.
+- `api-only`: keep API reference, API source-link, API stale identifier,
+ and API navigation updates only; report other required work as skipped.
+- `guides-only`: keep guide, tutorial, explanation, and snippet updates;
+ do not regenerate API reference pages.
+
+Build a per-file edit plan with:
+
+- `action`: one of `create`, `edit`, `delete`.
+- `path` relative to ``.
+- `category`: API reference / guide / tutorial / explanation / example /
+ navigation / release notes / security warning.
+- `covers`: which API items from Step 9 this file addresses (so the
+ user can sanity-check coverage).
+- `reason`: which matrix row(s) forced this entry.
+
+Also build a parallel "skipped" list for matrix-required updates that
+fall outside `` — every skipped item must be
+attributed to the scope choice that excluded it.
+
+---
+
+## 🛑 Gate G3 — Docs edit plan confirmation
+
+Active in `interactive` mode only. After Step 11 completes, emit the
+**G3 proposal block** (template:
+`references/proposals/g3-docs-edit-plan.md`) and **end the turn**.
+
+The proposal block MUST contain, in this order:
+
+```
+### Will create
+- — — covers: — reason:
+- ...
+
+### Will edit
+- — — covers: — reason:
+- ...
+
+### Will delete
+- — — reason:
+- ...
+
+### Will leave alone (matrix-required but out of )
+- — would have been — excluded by scope=
+- ...
+
+### Navigation deltas (will apply at Step 15)
+- add: