You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/contracts-sui/1.x/api/fixed-point.mdx
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -245,7 +245,7 @@ Returns the largest `UD30x9` value `r` such that `r * r <= x`. The result is rou
245
245
Returns the natural logarithm of `x`, derived from `log2` via `ln(x) = log2(x) * ln(2)`. The result is rounded down and may sit up to 2 ulps below the true value.
246
246
247
247
Aborts with `ELogUndefined` if `x` is zero.
248
-
Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would be negative and cannot be represented in `UD30x9` — use [`SD29x9::ln`](#sd29x9_base-ln) instead).
248
+
Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would be negative and cannot be represented in `UD30x9`, so use [`SD29x9::ln`](#sd29x9_base-ln) instead).
249
249
</APIItem>
250
250
251
251
<APIItem
@@ -256,7 +256,7 @@ Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would
256
256
Returns the base-2 logarithm of `x`. The result is rounded down and sits at most 2 ulps below the true `log2(x)`. For inputs in `[1, 2)` the result is exact.
257
257
258
258
Aborts with `ELogUndefined` if `x` is zero.
259
-
Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would be negative and cannot be represented in `UD30x9` — use [`SD29x9::log2`](#sd29x9_base-log2) instead).
259
+
Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would be negative and cannot be represented in `UD30x9`, so use [`SD29x9::log2`](#sd29x9_base-log2) instead).
260
260
</APIItem>
261
261
262
262
<APIItem
@@ -267,7 +267,7 @@ Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would
267
267
Returns the base-10 logarithm of `x`. Exact when `x` is an integer power of ten (`10^k`, `k >= 0`). Otherwise derived from `log2` via `log10(x) = log2(x) * log10(2)` and rounded down; the result may sit up to 2 ulps below the true value.
268
268
269
269
Aborts with `ELogUndefined` if `x` is zero.
270
-
Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would be negative and cannot be represented in `UD30x9` — use [`SD29x9::log10`](#sd29x9_base-log10) instead).
270
+
Aborts with `ELogResultUnrepresentable` if `x` is in `(0, 1)` (the result would be negative and cannot be represented in `UD30x9`, so use [`SD29x9::log10`](#sd29x9_base-log10) instead).
Copy file name to clipboardExpand all lines: content/contracts-sui/1.x/api/utils.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,7 +82,7 @@ The variant fields are not directly accessible from outside the module; read the
82
82
>
83
83
Creates a token bucket with an explicit initial balance and refill anchor. `available` accrues `refill_amount` every `refill_interval_ms`, capped at `capacity`.
84
84
85
-
`last_refill_ms` anchors the refill schedule; for a greenfield limiter pass `clock.timestamp_ms()`. Pass an earlier value to preserve the refill phase across a reconstruction, but only when the rate (`refill_amount` / `refill_interval_ms`) is unchanged — carrying an old anchor into a new rate pre-credits elapsed time at the new rate and can briefly exceed it. `initial_available` is the starting balance and must be `<= capacity`; `0` forces a wait for the first refill.
85
+
`last_refill_ms` anchors the refill schedule; for a greenfield limiter pass `clock.timestamp_ms()`. Pass an earlier value to preserve the refill phase across a reconstruction, but only when the rate (`refill_amount` / `refill_interval_ms`) is unchanged. Carrying an old anchor into a new rate pre-credits elapsed time at the new rate and can briefly exceed it. `initial_available` is the starting balance and must be `<= capacity`; `0` forces a wait for the first refill.
86
86
87
87
Aborts with `EZeroCapacity` if `capacity` is `0`.
88
88
@@ -124,7 +124,7 @@ A caller can spend the full quota at the end of one window and again at the star
124
124
>
125
125
Creates a cooldown limiter. Up to `capacity` units may be consumed (in any combination of per-call amounts) before the limiter gates. Once `available` reaches `0`, `cooldown_end_ms` is set to `now + cooldown_ms`; no further consume succeeds until `now >= cooldown_end_ms`, at which point `available` resets to `capacity`.
126
126
127
-
`cooldown_end_ms <= now` means no gate is armed. The valid seed combinations are: `initial_available > 0` with `cooldown_end_ms <= now` (granted — up to `initial_available` units before the first arm); `initial_available == 0` with `cooldown_end_ms > now` (gated — reconstructing a limiter mid-throttle, or arming a delay in front of an action); and `initial_available == 0` with `cooldown_end_ms <= now` (released — projects to fully available on the next read or consume).
127
+
`cooldown_end_ms <= now` means no gate is armed. The valid seed combinations are: `initial_available > 0` with `cooldown_end_ms <= now` (granted - up to `initial_available` units before the first arm); `initial_available == 0` with `cooldown_end_ms > now` (gated - reconstructing a limiter mid-throttle, or arming a delay in front of an action); and `initial_available == 0` with `cooldown_end_ms <= now` (released - projects to fully available on the next read or consume).
Copy file name to clipboardExpand all lines: content/contracts-sui/1.x/rate-limiter.mdx
+18-18Lines changed: 18 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,9 +6,9 @@ title: Rate Limiter
6
6
The example code snippets used in this guide are experimental and have not been audited. They simply help exemplify usage of the OpenZeppelin Sui Package.
7
7
</Callout>
8
8
9
-
The `rate_limiter` module provides an embeddable, multi-strategy rate-limiting primitive for Sui Move. `RateLimiter` is a plain `store + drop` value —**not** a Sui object. You embed it as a field inside an object you already own (a vault, a capability, a per-player record) and call one function on the hot path. There is no registry, no shared policy object, no admin cap, and no separate ID to track: the limiter's scope is whatever object it lives inside.
9
+
The `rate_limiter` module provides an embeddable, multi-strategy rate-limiting primitive for Sui Move. `RateLimiter` is a plain `store + drop` value,**not** a Sui object. You embed it as a field inside an object you already own (a vault, a capability, a per-player record) and call one function on the hot path. There is no registry, no shared policy object, no admin cap, and no separate ID to track: the limiter's scope is whatever object it lives inside.
10
10
11
-
Rate limiting is otherwise reimplemented ad hoc by every protocol that needs it — withdrawal throttles, daily caps, action cooldowns. `rate_limiter` generalizes the proven rate limiting math into a reusable primitive and exposes three strategies (bucket, fixed window and cooldown) that share a single API.
11
+
Rate limiting is otherwise reimplemented ad hoc by every protocol that needs it, whether for withdrawal throttles, daily caps, or action cooldowns. `rate_limiter` generalizes the proven rate limiting math into a reusable primitive and exposes three strategies (bucket, fixed window and cooldown) that share a single API.
12
12
13
13
## Use cases
14
14
@@ -18,7 +18,7 @@ Use `rate_limiter` when your protocol needs:
18
18
- Independent per-user or per-object budgets that refill over time.
19
19
- A hard per-window quota, such as "100 mints per hour".
20
20
- A cooldown that gates the reuse of an action after a burst.
21
-
- A delay that must elapse before an action — like a claim or an unstake — can execute.
21
+
- A delay that must elapse before an action such as a claim or an unstake can execute.
22
22
- A regenerating resource in non-financial logic, such as health, mana, stamina, or energy.
23
23
24
24
## Import
@@ -60,7 +60,7 @@ public struct Vault has key {
60
60
61
61
public fun create(clock: &Clock, ctx: &mut TxContext) {
62
62
// Bucket: cap 1_000 units, refilling 100 units every 6 s, starting full.
63
-
// (units are whatever you meter — here MIST)
63
+
// (units are whatever you meter; here, MIST)
64
64
let limiter = rate_limiter::new_bucket(1_000, 100, 6_000, clock.timestamp_ms(), 1_000, clock);
@@ -75,7 +75,7 @@ The limiter check runs *before* the side effect, so a denied withdrawal never to
75
75
76
76
## Gating reuse with a cooldown
77
77
78
-
`Cooldown` allows up to `capacity` uses, then locks for `cooldown_ms` before the batch refreshes. The consume call is identical to the bucket — only the constructor changes — and `available` surfaces the remaining uses without mutating:
78
+
`Cooldown` allows up to `capacity` uses, then locks for `cooldown_ms` before the batch refreshes. The consume call is identical to the bucket (only the constructor changes), and `available` surfaces the remaining uses without mutating:
79
79
80
80
```move
81
81
module my_game::ability;
@@ -99,7 +99,7 @@ public fun dash(self: &mut Hero, clock: &Clock) {
99
99
// ... apply the dash ...
100
100
}
101
101
102
-
/// Dashes still available right now — 0 while the cooldown is active.
102
+
/// Dashes still available right now; 0 while the cooldown is active.
103
103
public fun dashes_left(self: &Hero, clock: &Clock): u64 {
104
104
self.dash.available(clock)
105
105
}
@@ -109,7 +109,7 @@ The same shape arms a one-shot delay in front of an action: construct with `init
109
109
110
110
## Reconfiguring a limiter
111
111
112
-
There are no in-place setters. To change a limiter's configuration, snapshot its live state through the getters, build a fresh `RateLimiter`, and overwrite the field —`drop` lets the old value go. Gate the entry function with the same authority that guards your other privileged operations:
112
+
There are no in-place setters. To change a limiter's configuration, snapshot its live state through the getters, build a fresh `RateLimiter`, and overwrite the field, since`drop` lets the old value go. Gate the entry function with the same authority that guards your other privileged operations:
113
113
114
114
```move
115
115
/// Raise the vault's withdrawal ceiling, preserving the current balance and refill phase.
@@ -132,54 +132,54 @@ public fun set_capacity(self: &mut Vault, new_capacity: u64, clock: &Clock) {
132
132
}
133
133
```
134
134
135
-
Reading `last_refill_ms(clock)` and `available(clock)` together keeps the refill phase intact across the swap. Other policies — re-anchoring, a full reset, or switching variant entirely — are just different constructor calls over the same snapshot.
135
+
Reading `last_refill_ms(clock)` and `available(clock)` together keeps the refill phase intact across the swap. Other policies, such as re-anchoring, a full reset, or switching variant entirely, are just different constructor calls over the same snapshot.
136
136
137
137
<Callouttype="warn">
138
-
Preserving the anchor while *also* changing the refill rate re-prices the elapsed time under the new rate. A long gap since `last_refill_ms` can mint up to `capacity` tokens the instant the new limiter is constructed. If you change `refill_amount` or `refill_interval_ms`, re-anchor to `clock.timestamp_ms()` (and pick `initial_available` explicitly) instead of carrying the old anchor forward. Preserving the anchor is safe only when the rate is unchanged — as in the `set_capacity` example above.
138
+
Preserving the anchor while *also* changing the refill rate re-prices the elapsed time under the new rate. A long gap since `last_refill_ms` can mint up to `capacity` tokens the instant the new limiter is constructed. If you change `refill_amount` or `refill_interval_ms`, re-anchor to `clock.timestamp_ms()` (and pick `initial_available` explicitly) instead of carrying the old anchor forward. Preserving the anchor is safe only when the rate is unchanged, as in the `set_capacity` example above.
139
139
</Callout>
140
140
141
141
## Key concepts
142
142
143
-
-**Embedded value, not an object.**`RateLimiter` has `store + drop` and no `key`/`UID`. It can only exist as a field of a parent value you own, and its scope is that parent — no global state, no contention, no registry to index. Withholding `copy` is what prevents over-issuance: a duplicable limiter would multiply your configured capacity by N.
143
+
-**Embedded value, not an object.**`RateLimiter` has `store + drop` and no `key`/`UID`. It can only exist as a field of a parent value you own, and its scope is that parent, with no global state, no contention, and no registry to index. Withholding `copy` is what prevents over-issuance: a duplicable limiter would multiply your configured capacity by N.
144
144
145
145
-**Project-on-read.**`available(clock)` returns the units consumable *right now*, projecting any pending refill, window rollover, or cooldown release without mutating. `try_consume` commits that projection only on success.
146
146
147
147
-**All-or-nothing consume.**`try_consume` either consumes `amount` and commits the time projection, or returns `false` and writes nothing (the lone exception is a pathological `Cooldown` config whose next deadline would overflow `u64`, which aborts `ECooldownDeadlineOverflow`). There is no "denied but charged", so you can safely probe with `try_consume` without skewing state. `consume_or_abort` is the ergonomic wrapper that turns a refusal into an `ERateLimited` abort.
148
148
149
-
-**Reconfigure by reconstruction.** There are no in-place setters; you change a limiter by overwriting the field with a fresh one (see [Reconfiguring a limiter](#reconfiguring-a-limiter)). Every policy — preserve anchor, re-anchor, full reset, proportional carry, freeze an in-flight gate — is expressible in your own code; the library validates only structural validity on construction.
149
+
-**Reconfigure by reconstruction.** There are no in-place setters; you change a limiter by overwriting the field with a fresh one (see [Reconfiguring a limiter](#reconfiguring-a-limiter)). Every policy (preserve anchor, re-anchor, full reset, proportional carry, freeze an in-flight gate) is expressible in your own code; the library validates only structural validity on construction.
150
150
151
-
-**Authorization is yours.** Whoever holds `&mut` to the field may consume and reconfigure. Gate your entry functions with whatever model fits — a capability, [`openzeppelin_access`](/contracts-sui/1.x/access), governance, or a multisig. The module makes no access-control claim.
151
+
-**Authorization is yours.** Whoever holds `&mut` to the field may consume and reconfigure. Gate your entry functions with whatever model fits, such as a capability, [`openzeppelin_access`](/contracts-sui/1.x/access), governance, or a multisig. The module makes no access-control claim.
152
152
153
153
## Common mistakes
154
154
155
155
| Mistake | What happens | How to fix |
156
156
|---|---|---|
157
-
|`try_consume(rl.available(clock), clock)` when empty | Returns `false` (`available()` is `0`, and a zero-unit consume is rejected) — surprising if you expect "consume all available" to succeed as a no-op | Guard: `let n = rl.available(clock); if (n > 0) { rl.try_consume(n, clock); }`|
157
+
|`try_consume(rl.available(clock), clock)` when empty | Returns `false` (`available()` is `0`, and a zero-unit consume is rejected), which is surprising if you expect "consume all available" to succeed as a no-op | Guard: `let n = rl.available(clock); if (n > 0) { rl.try_consume(n, clock); }`|
158
158
| Calling `consume_or_abort` with `amount == 0`| Aborts `EInvalidAmount` (`try_consume` returns `false` instead) | Treat zero-unit work as a no-op in your own code; never pass `0`. |
159
159
| Reading `cooldown_end_ms()` while `available > 0`| Returns a stale value (the gate is only consulted when `available == 0`) | Only interpret `cooldown_end_ms()` when `available(clock) == 0`. |
160
-
| Expecting a partially drained `Cooldown` to refill on each cooldown | It does not — the gate arms and the batch refreshes to `capacity` only once the balance hits exactly `0`. Capacity 10 with 7 spent reports `3` forever until those `3` are consumed and the gate then elapses | Drain the batch fully before the cooldown applies; the refresh is per-emptied-batch, not per-elapsed-interval. |
160
+
| Expecting a partially drained `Cooldown` to refill on each cooldown | It does not. The gate arms and the batch refreshes to `capacity` only once the balance hits exactly `0`. Capacity 10 with 7 spent reports `3` forever until those `3` are consumed and the gate then elapses | Drain the batch fully before the cooldown applies; the refresh is per-emptied-batch, not per-elapsed-interval. |
161
161
| Seeding `new_cooldown` with `initial_available > 0` and `cooldown_end_ms > now`| Aborts `ECooldownArmedWithTokens`| Use one of the valid seed combinations (see the [API reference](/contracts-sui/1.x/api/utils)). |
162
162
| Passing a future `last_refill_ms` / `window_start_ms`| Aborts `EBucketAnchorInFuture` / `EWindowAnchorInFuture`| Pass `clock.timestamp_ms()` (or an earlier value). |
163
163
| Expecting `FixedWindow` to prevent boundary bursts | A caller can spend `capacity` at the end of one window and `capacity` at the start of the next (a `2 * capacity` burst) | Use `Bucket` if a smooth ceiling matters; `FixedWindow` only guarantees the per-window cap. |
164
164
| Preserving the bucket anchor while changing the refill rate | Elapsed time is re-priced under the new rate, minting up to `capacity` phantom tokens the instant the limiter is rebuilt | When changing `refill_amount` / `refill_interval_ms`, re-anchor to `clock.timestamp_ms()` and set `initial_available` explicitly; only preserve the anchor when the rate is unchanged. |
165
-
| Assuming the limiter authorizes callers | It does not — anyone with `&mut` to the field can consume and reconfigure | Gate your entry functions with a capability, `openzeppelin_access`, governance, or a multisig. |
165
+
| Assuming the limiter authorizes callers | It does not; anyone with `&mut` to the field can consume and reconfigure | Gate your entry functions with a capability, `openzeppelin_access`, governance, or a multisig. |
166
166
167
167
## FAQ
168
168
169
169
**Do I need to register or track the limiter anywhere?**
170
170
No. It is a field of your object, not a separate object. There is no registry, no ID, nothing for a front end or indexer to track per environment.
171
171
172
172
**How do I disable a limiter (pause)?**
173
-
The module has no `enabled` toggle by design. The simplest pause is in your own object — a `bool` checked before the consume. There is no literal deny-all config (every constructor requires positive parameters; `capacity == 0` aborts `EZeroCapacity`, and a `Bucket`/`FixedWindow` seeded empty just refills over time), but a `Cooldown` seeded with `initial_available = 0` and a far-future `cooldown_end_ms` stays gated and denies every consume until you reconstruct it.
173
+
The module has no `enabled` toggle by design. The simplest pause is in your own object: a `bool` checked before the consume. There is no literal deny-all config (every constructor requires positive parameters; `capacity == 0` aborts `EZeroCapacity`, and a `Bucket`/`FixedWindow` seeded empty just refills over time), but a `Cooldown` seeded with `initial_available = 0` and a far-future `cooldown_end_ms` stays gated and denies every consume until you reconstruct it.
174
174
175
175
**Does the library emit events?**
176
176
No. Observability is left to you, since you hold the context an event needs (which user, which object). Emit your own event after a successful consume if you want indexing.
177
177
178
178
**Can I probe the limiter without affecting it?**
179
-
Yes. You can use the `available(clock)` getter (projects pending refills). Also, a failed `try_consume` (returns `false`) is observably a no-op across all three variants — no anchor advance, no balance change, no gate re-arm.
179
+
Yes. You can use the `available(clock)` getter (projects pending refills). Also, a failed `try_consume` (returns `false`) is observably a no-op across all three variants, with no anchor advance, no balance change, and no gate re-arm.
180
180
181
181
**How do I inspect a limiter whose variant I don't know (for example, a `Table` mixing variants)?**
182
-
Branch on `is_bucket()` / `is_fixed_window()` / `is_cooldown()` before calling a variant-typed getter (`refill_amount`, `window_ms`, `cooldown_ms`, ...). Those getters abort `EWrongVariant` on a mismatch, and Move cannot recover from an abort. Some variant-typed getters also take a `&Clock` and return a projected anchor — `last_refill_ms(clock)`(`Bucket`) and `window_start_ms(clock)`(`FixedWindow`) — so don't assume every variant getter is clock-free. `capacity()` and `available(clock)` are variant-agnostic and need no guard.
182
+
Branch on `is_bucket()` / `is_fixed_window()` / `is_cooldown()` before calling a variant-typed getter (`refill_amount`, `window_ms`, `cooldown_ms`, ...). Those getters abort `EWrongVariant` on a mismatch, and Move cannot recover from an abort. Some variant-typed getters also take a `&Clock` and return a projected anchor (`last_refill_ms(clock)`for `Bucket` and `window_start_ms(clock)`for `FixedWindow`), so don't assume every variant getter is clock-free. `capacity()` and `available(clock)` are variant-agnostic and need no guard.
0 commit comments