Skip to main content

Module sui::allowance

Native allowances enable delegated, bounded, revocable spending from an address's live balance.

A transaction declares its funding source as a (funder, allowance) pair. Signing verifies that source against the shared Allowance and hands the transaction an AllowanceWithdrawal.

All policy checks (rate limits, lifetime caps, expiry) are enforced by this module.

Struct AllowanceWithdrawal

Created via a PTB Argument.

The inner Withdrawal is contained within this module and cannot be accessed directly.
An allowance's limits can never be charged without the funds actually moving.

public struct AllowanceWithdrawal<phantom T: store> has drop
Click to open
Fields
allowance: sui::object::ID
is_sponsor: bool
Today is always false. Opens the door for future ctx.sponsor() based allowances.
inner: sui::funds_accumulator::Withdrawal<T>

Struct Allowance

Enables withdrawing T from the funder's balance, within this allowance's limits.
Always kept as a shared object.

public struct Allowance<phantom T> has key
Click to open
Fields
id: sui::object::UID
settings: sui::allowance::Settings
current_spend: u256
Total cumulative spend from this allowance.

Struct Settings

Configuration of the Allowance, held by Allowance and AllowanceProposal

public struct Settings has drop, store
Click to open
Fields
funder: address
The address whose balance is debited by spends against this allowance.
spender: std::option::Option<address>
The spender of the allowance.
While it is currently always set, in the future this may become optional to allow for keyless app-bound withdrawals.
app: std::option::Option<std::type_name::TypeName>
When set, requires the app's SpendPermit to spend, and only that app can rotate the spender or issue the allowance in the first place.
lifetime_cap: std::option::Option<u256>
An optional lifetime cap on withdrawals using this allowance. Inclusive.
Amounts are u256, matching Withdrawal.limit.
start_timestamp_ms: std::option::Option<u64>
Optional activation time, in milliseconds. Inclusive.
expiration_timestamp_ms: std::option::Option<u64>
Optional expiration time, in milliseconds. Exclusive.
rate_limit: std::option::Option<sui::allowance::RateLimit>
An optional recurring limit, applied on top of lifetime_cap. At least one of the two must be set.
name: std::string::String
A label for off-chain use, never read by any check. At most 128 bytes.

Struct AllowanceCap

Revocation for an allowance, sent to the funder at issuance (soulbound).
Also used for discoverability (funder -> allowances).

Created with the allowance and destroyed with it by revoke.

public struct AllowanceCap<phantom T> has key
Click to open
Fields

Struct AllowanceProposal

A proposal that can only be issued by app A.

public struct AllowanceProposal<phantom T> has drop
Click to open
Fields

Struct SpendPermit

A SpendPermit<A> authorizes a single spend against an allowance bound to A. It is issued from an internal::Permit<A>, allowing the module that defines A to gate every withdrawal on its own logic.

public struct SpendPermit<phantom A> has drop

Struct SettingsPermit

A SettingsPermit<A> authorizes changing the configuration of an allowance bound to A: issuing one, or rotating its spender. It is issued from an internal::Permit<A>.

Kept distinct from SpendPermit so an app can hand out the right to spend without also handing out the right to reconfigure, and vice versa.

public struct SettingsPermit<phantom A> has drop

Enum RateLimit

A recurring cap on withdrawals, applied on top of any lifetime cap.

An enum so other mechanics, like sliding windows, can be added as variants.

public enum RateLimit has copy, drop, store
Click to open
Variants
Variant Windowed
At most limit per window. Windows only roll forward.
limit: u256
The most that can be spent within a window. Inclusive.
spent: u256
Amount spent so far within the current window.
anchor_ms: std::option::Option<u64>
Start of the first window, stamped by the first successful charge.
index: u64
Which window spent is accumulated in, numbered from the anchor (0 = first).
A spend landing in a later one resets spent.
window: sui::allowance::Window
The defining period of time for this rate limit.

Enum Window

The defining time period for a RateLimit::Windowed.

public enum Window has copy, drop, store
Click to open
Variants
Variant PeriodicMs
Windows of exactly this many milliseconds.
0: u64
Variant CalendarMonths
Windows of this many civil (UTC) months.
0: u8

Constants

#[error]
const ENotSpender: vector<u8> = b"Transaction sender is not this allowance's spender";
#[error]
const EWrongApp: vector<u8> = b"Allowance is not bound to this app";
#[error]
const EExpired: vector<u8> = b"Allowance has expired";
#[error]
const EExceedsLifetimeCap: vector<u8> = b"Spend would exceed the lifetime cap";
#[error]
const EExceedsRateLimit: vector<u8> = b"Spend would exceed the current rate-limit window";
#[error]
const ENoLimit: vector<u8> = b"Allowance must have a lifetime cap or a rate limit";
#[error]
const EWrongAllowance: vector<u8> = b"Withdrawal was issued for a different allowance";
#[error]
const EBadRateLimit: vector<u8> = b"Rate limit needs a positive period and limit";
#[error]
const ENotStarted: vector<u8> = b"Allowance is not active yet; it has a future start timestamp";
#[error]
const EHasApp: vector<u8> = b"App-bound allowance: spend through app_balance_spend";
#[error]
const EWrongFunder: vector<u8> = b"Withdrawal debits a different address than this allowance's funder";
#[error]
const EWrongCap: vector<u8> = b"Cap does not match this allowance";
#[error]
const ENameTooLong: vector<u8> = b"Name exceeds the 128-byte limit";
#[error]
const EZeroLifetimeCap: vector<u8> = b"Lifetime cap must be greater than zero";
#[error]
const EBadTimeWindow: vector<u8> = b"Expiration must be after the start time";
#[error]
const ENoExpiration: vector<u8> = b"Allowance must have an expiration or a rate limit";
#[error]
const ENotEnabled: vector<u8> = b"Allowances are not enabled";
#[error]
const ESponsorWithdrawalNotEnabled: vector<u8> = b"Sponsor allowance withdrawals are not enabled";
const MAX_NAME_LENGTH: u64 = 128;
const MS_PER_DAY: u64 = 86400000;

Function spend_permit

Issues a SpendPermit<A> from the privileged internal::Permit<A>.

Function settings_permit

Issues a SettingsPermit<A> from the privileged internal::Permit<A>.

Function periodic_rate_limit

At most limit per period_ms, counted from the first charge.

public fun periodic_rate_limit(period_ms: u64, limit: u256): sui::allowance::RateLimit

Function calendar_rate_limit

At most limit per months civil (UTC) months, counted from the first charge. Windows renew on the anchor's day-of-month at 00:00 UTC, clamped to shorter months (a Jan 31 anchor renews.
Feb 28, then Mar 31).

public fun calendar_rate_limit(months: u8, limit: u256): sui::allowance::RateLimit

Function monthly_rate_limit

Function quarterly_rate_limit

Function yearly_rate_limit

Function new

Issues an allowance funded by the sender, sharing it and sending its AllowanceCap to the sender. Creation is entry so contracts cannot create allowances implicitly.

Function propose_for_app

Returns an AllowanceProposal for an allowance bound to the controlling app A, funded by the sender.

Unlike new, this creates no allowance on its own. A's module must accept the proposal via issue, giving the app a say in every allowance that names it.

Function issue

Issues the proposed allowance on A's behalf, creating and sharing it.

Function balance_spend

Signer path: the tx sender must be the spender.

Function app_balance_spend

App path: requires a SpendPermit<A> matching the allowance's app. The tx must still come from the spender.

Function revoke

Revokes an allowance, removing the ability to spend.

Function rotate_spender

Rotates the spender key without the funder reissuing the allowance.

App-only: for an app-bound allowance the app dictates who the spender is. Non-app allowances rotate through address aliases instead.

public fun rotate_spender<T, A>(self: &mut sui::allowance::Allowance<T>, _: sui::allowance::SettingsPermit<A>, new_spender: address)

Function allowance_settings

Function allowance_current_spend

public fun allowance_current_spend<T>(self: &sui::allowance::Allowance<T>): u256

Function allowance_cap_allowance

Function allowance_proposal_settings

Function funder

public fun funder(self: &sui::allowance::Settings): address

Function spender

Function app

Function lifetime_cap

Function start_timestamp_ms

Function expiration_timestamp_ms

Function rate_limit

Function name

Function rate_limit_limit

Function rate_limit_spent

Function rate_limit_window

Function civil_from_ms

Civil (year, month, day) in UTC, month and day 1-based. This is Howard Hinnant's civil_from_days, where the derivation of every constant here is documented: https://howardhinnant.github.io/date_algorithms.html#civil_from_days

The unsigned-only form of the algorithm. Chain timestamps are never pre-epoch.

public(package) fun civil_from_ms(timestamp_ms: u64): (u64, u64, u64)

Function days_in_month

Companion to civil_from_ms, following the same reference: https://howardhinnant.github.io/date_algorithms.html#last_day_of_month

public(package) fun days_in_month(year: u64, month: u64): u64

Function assert_app

Function consume

Central logic for policy checks and accounting, including the check of the spender. NB: Any app checks must be done beforehand by the caller.

Function new_rate_limit

Function charge

Records amount against the limit, aborting if it does not fit.

fun charge(self: &mut sui::allowance::RateLimit, amount: u256, now_ms: u64)

Function index_at

Which window now_ms falls in, numbered from anchor_ms (0 = first).

fun index_at(self: &sui::allowance::Window, anchor_ms: u64, now_ms: u64): u64

Function elapsed_windows

Which months-month window now_ms falls in, counting from anchor_ms (0 = the window the anchor itself is in).

fun elapsed_windows(anchor_ms: u64, now_ms: u64, months: u8): u64

Function is_leap_year

fun is_leap_year(year: u64): bool

Function new_settings

Builds and validates settings. Rejects allowances that are unbounded in amount or in time.

Function share_new