Fungible Asset Allowances
supra_framework::fungible_asset_allowance is available on Devnet only. It is not yet on Testnet
or Mainnet.
This article describes how an application lets one account spend a bounded amount of another
account’s fungible asset, using supra_framework::fungible_asset_allowance. The model is the ERC-20
approve / transferFrom pair: the asset owner (the grantor) records an allowance for a
spender, and the spender moves the asset without the grantor’s signer, a SignerCapability or the
issuer’s TransferRef. Every allowance has an expiry, and the grantor can change or revoke it at
any time.
The module also accepts a grant the grantor signed off chain, so a grantor can authorise a spending limit without submitting a transaction.
Separately from grants, the module settles a one-shot transfer authorization: the payer signs, off
chain, an authorization for one named facilitator to move up to a maximum amount to one named
recipient, once. This is the shape of the x402 upto payment scheme, and with the full amount
settled, of x402 exact. See One-Shot Settlement.
Requirements
- The chain runs a framework that carries the module, with
fungible_asset_allowance::initializeexecuted. The module is not inaptosvm-v1.16_supra-v1.8.17or earlier, and settlement is inaptosvm-v1.16_supra-v1.9.0.rc18and later. New chains runinitializeat genesis. On an existing chain the framework upgrade script runs it. Until then every write that creates grant state and every settlement abort withEMODULE_NOT_INITIALIZED;revokedoes nothing, anddecrease,transfer_fromandtransfer_from_storeabort withEALLOWANCE_NOT_FOUND. - The asset is a framework fungible asset with a
Metadataobject. Every function below takes the metadata object’s address. - The grantor’s balance sits in a
FungibleStorethe grantor owns directly, and a payer’s in its primary store for the asset. For SUPRA, that is the paired fungible asset’s primary store, notCoinStore<SupraCoin>. Balances still held asCoinare not reachable through an allowance or a settlement.
The examples use the supra CLI. Each entry function is also callable from any SDK that can submit
an entry-function transaction, and each view through POST /rpc/v3/view. A supra command without
--profile <name> runs against the active profile; --rpc-url <URL> sends it to that node in place
of the profile’s.
Moving SUPRA to the Primary Store
An account funded by a transfer or by the faucet holds its SUPRA as a Coin in
CoinStore<SupraCoin>. The module reads only fungible stores, so for such a grantor transfer_from
aborts with ESOURCE_STORE_MISSING, and so does settle_upto for such a payer. The account moves
its balance to its primary store once, with its own signer:
supra move tool run \
--function-id 0x1::coin::migrate_to_fungible_store \
--type-args 0x1::supra_coin::SupraCoin \
--profile grantor --accept-defaultsRun it for each grantor and payer before the examples below. It moves the whole CoinStore
balance, after which coin::balance reports the same total.
Terms
| Term | Meaning |
|---|---|
| grantor | The account whose asset is spent. |
| spender | The account permitted to move it. |
| grant | One allowance, identified by (grantor, spender, metadata). Holds a remaining amount, an expiry and a scope. |
| live grant | A grant whose expiry is in the future, whose remaining amount is above zero, and whose generation matches the grantor’s current one. Only live grants can be spent. |
| generation | A counter per grantor. revoke_all advances it, which voids every grant made under the old value. |
| scope | Which of the grantor’s stores of the asset the grant may be spent from. |
| grant nonce | A u64 a signed grant names, usable once per grantor. |
| payer | The account that signs a one-shot transfer authorization and whose primary store it debits. |
| facilitator | The only account that may settle an authorization: the one the payer named in it. |
| authorization | An UptoAuthorization: a payer’s signed permission for a facilitator to move up to max_amount to one recipient, once. |
| settlement nonce | The 32 bytes an authorization names. A payer’s record of it blocks any authorization carrying it from being settled until the record is removed. |
Granting an Allowance
The grantor calls increase. It creates the grant if there is no live one and otherwise adds to
it and sets its expiry to expiry_secs. expiry_secs is a Unix time in seconds; it must be later
than the current block time and at most 366 days ahead.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::increase \
--args address:<SPENDER> address:<METADATA> u64:<AMOUNT> u64:<EXPIRY_SECS> u8:<SCOPE> \
--profile grantor --accept-defaultsRead the current block time from the node rather than the host clock: GET /rpc/v3/block returns
the latest block, whose header.timestamp.microseconds_since_unix_epoch is in microseconds, so
divide it by 1,000,000 for expiry_secs.
Check the result:
supra move tool view \
--function-id 0x1::fungible_asset_allowance::allowance \
--args address:<GRANTOR> address:<SPENDER> address:<METADATA>The view returns [remaining, expiry_secs, scope], or ["0", "0", 0] when no grant is live. The
u64 values are JSON strings and the u8 scope is a JSON number.
Choosing a Scope
scope states which of the grantor’s stores of the asset the spender may withdraw from:
| Value | Name | Covers |
|---|---|---|
0 | SCOPE_PRIMARY_STORE | The grantor’s primary store for the asset, and nothing else. |
1 | SCOPE_ANY_OWNED_STORE | Any store of the asset the grantor owns directly, including the primary store. |
Scope 0 suits a grantor that holds the asset in its primary store, which is where
primary_fungible_store transfers and most wallet balances land. It also bounds the grant if the
grantor later creates other stores of the same asset: a spender that knows a store address cannot
reach it.
Scope 1 suits a grantor that keeps the asset in stores it created itself — a treasury splitting
one asset across purpose-built stores — and a spender that is expected to name those stores through
transfer_from_store.
increase against a live grant requires the scope the grant already has and otherwise aborts with
EALLOWANCE_SCOPE_MISMATCH. replace is how the scope of an existing grant changes.
Changing a Grant Without a Race
ERC-20’s approve has a known race: a spender who sees a reduction coming can spend the old
allowance first and then the new one. The module has no bare overwrite. Use one of:
decrease(spender, metadata, by)— subtracts, saturating at zero, keeping the expiry and scope. A result of zero removes the grant.by = 0leaves the grant unchanged, and the call aborts withEALLOWANCE_NOT_FOUNDwhen no grant is live.replace(spender, metadata, expected_remaining, amount, expiry_secs, scope)— sets the grant toamountwith scopescopeonly if its live remaining amount is exactlyexpected_remaining. Read the value from theallowanceview first. If a spend lands in between, the call aborts withEALLOWANCE_MISMATCHand nothing changes. Passamount = 0to remove the grant under the same condition.revoke(spender, metadata)— removes the grant unconditionally.revoke_all()— voids every grant the grantor has made, to every spender and for every asset, in one transaction. It also voids every signed grant and every settlement authorization the grantor produced under the old generation.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::replace \
--args address:<SPENDER> address:<METADATA> u64:<EXPECTED_REMAINING> u64:<NEW_AMOUNT> \
u64:<EXPIRY_SECS> u8:<SCOPE> \
--profile grantor --accept-defaultsSpending an Allowance
The spender calls transfer_from. It moves amount from the grantor’s primary store to the
recipient’s primary store, creating the recipient’s store if needed, and charges the grant. The
primary store is inside both scopes, so this works under either.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::transfer_from \
--args address:<GRANTOR> address:<METADATA> address:<RECIPIENT> u64:<AMOUNT> \
--profile spender --accept-defaultstransfer_from_store<T>(store, to_store, amount) does the same between two named stores. The
address of an account’s primary store is returned by
0x1::primary_fungible_store::primary_store_address, as shown in Recognising Account
Operations. The grant
consulted is the one made by the source store’s direct owner, so a store held through an
intermediate object is spendable only under a grant made by that object. Under scope 0 the source
store must be the grantor’s primary store; a spend against any other store aborts with
ESTORE_OUT_OF_SCOPE.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::transfer_from_store \
--type-args 0x1::fungible_asset::FungibleStore \
--args address:<SOURCE_STORE> address:<DESTINATION_STORE> u64:<AMOUNT> \
--profile spender --accept-defaultsPoints that affect an integration:
- Hooks run. The asset’s dispatchable withdraw and deposit functions execute as they do for an ordinary transfer. If the withdraw hook takes a fee, the grant is charged the amount the source store was actually debited, so the fee counts against the allowance. Leave room for it.
- Frozen stores are rejected, on either side, as for
fungible_asset::transfer. - Aborts. A spend above the live remaining amount aborts with
EINSUFFICIENT_ALLOWANCE; a spend with no live grant aborts withEALLOWANCE_NOT_FOUND; a spend from a store outside the grant’s scope aborts withESTORE_OUT_OF_SCOPE. Read the code from the committed transaction’svm_status, which readsMove abort in 0x1::fungible_asset_allowance: ....
Off-Chain Signed Grants
A grantor can authorise a grant by signature instead of a transaction. Anyone may submit the
signature with apply_signed_grant, and the spender then spends the grant with transfer_from as
for any other grant. The grantor pays no gas.
The grantor signs with an ed25519 or multi-ed25519 key, and the public key must derive the
grantor’s authentication key. For an account with an Account resource that is the key recorded
there. For an address with no Account resource, such as one that has only received assets, it is
the address itself, so the address must be the one the key derives. Accounts with other key schemes
use the on-chain functions above.
1. Build the payload
The grantor’s client gathers:
| Field | Value |
|---|---|
grantor, spender, metadata | Addresses. |
expected_remaining | The live remaining amount the grantor expects, from the allowance view. 0 for a fresh grant. |
amount | The remaining amount the grant is set to. |
expiry_secs | Unix seconds, in the future and within 366 days of block time. Required even when amount is 0. |
nonce | A u64 at or above the grantor’s min_nonce that the grantor has not used before. Check with the is_nonce_used view. |
generation | The grantor’s current generation, from the generation view. |
scope | 0 or 1, as for increase. |
generation and scope are part of the signed payload: a submitter cannot widen the scope the
grantor authorised, and a signature made before a revoke_all no longer applies. The chain id is
filled in on chain, so a signature made for one network is not valid on another.
2. Get the bytes to sign
A grantor whose key is in a supra CLI profile signs with:
supra move allowance sign-grant \
--spender <SPENDER> --metadata <METADATA> --expected-remaining <EXPECTED_REMAINING> \
--amount <AMOUNT> --expiry-secs <EXPIRY_SECS> --nonce <NONCE> --generation <GENERATION> \
--scope <SCOPE> --profile <GRANTOR_PROFILE> > grant.jsonThe grantor is the profile’s account address. The command reads the bytes to sign from the
grant_signing_message view, checks them against the payload it encodes for the profile’s network,
and signs them with the profile’s ed25519 key. It writes the result to standard output as one JSON
document, and its password prompt and notices to standard error, so > grant.json captures the
result alone. A grant of 1 SUPRA (--metadata 0xa --amount 100000000) signed on a localnet
produced:
{
"function_id": "0x1::fungible_asset_allowance::apply_signed_grant",
"message": {
"chain_id": 255,
"grantor": "0xded46df83b18540b0bd144cdea61f99e2d4dd28c274d12560d777bd01f6272f9",
"spender": "0x8cc2cb17b506e9c0e291f52b48210874198316f38f0a0a96eba87cad0e1a89ba",
"metadata": "0xa",
"expected_remaining": "0",
"amount": "100000000",
"expiry_secs": "1792144764",
"nonce": "1",
"generation": "0",
"scope": 0
},
"account_scheme": 0,
"public_key": "0xb533465b288449342a10eee983bad49df56baf545c5096b282cd428b45767939",
"signature": "0x3e012984c0f2134b91cfe660b286db6e73c5bfe8d62ccee7cdcc3a49959057313e6d06fff023870acd456a0dc785790e64348c1bce594557e4128cf6367b0804",
"args": [
"address:0xded46df83b18540b0bd144cdea61f99e2d4dd28c274d12560d777bd01f6272f9",
"address:0x8cc2cb17b506e9c0e291f52b48210874198316f38f0a0a96eba87cad0e1a89ba",
"address:0xa",
"u64:0",
"u64:100000000",
"u64:1792144764",
"u64:1",
"u64:0",
"u8:0",
"u8:0",
"hex:0xb533465b288449342a10eee983bad49df56baf545c5096b282cd428b45767939",
"hex:0x3e012984c0f2134b91cfe660b286db6e73c5bfe8d62ccee7cdcc3a49959057313e6d06fff023870acd456a0dc785790e64348c1bce594557e4128cf6367b0804"
]
}| Field | Use in step 3 |
|---|---|
function_id | The entry function to submit, apply_signed_grant. |
message | The signed fields, for the submitter to check. chain_id is the network’s. |
account_scheme | <SCHEME>. |
public_key | <PUBLIC_KEY>. |
signature | <SIGNATURE>. |
args | The whole --args list of apply_signed_grant, in order, with every value set. |
The result holds the signed fields, the public key and the signature, and no secret. The grantor
gives grant.json to whoever submits it in step 3, such as the spender.
A wallet or SDK that holds the grantor’s key reads the same bytes from the view:
supra move tool view \
--function-id 0x1::fungible_asset_allowance::grant_signing_message \
--args address:<GRANTOR> address:<SPENDER> address:<METADATA> u64:<EXPECTED_REMAINING> \
u64:<AMOUNT> u64:<EXPIRY_SECS> u64:<NONCE> u64:<GENERATION> u8:<SCOPE>The view returns the exact byte string to sign. It is the BCS encoding of
ed25519::SignedMessage<SignedGrant>, which embeds the struct’s type, so the signature cannot be
replayed against any other framework message. Sign those bytes with the grantor’s key as a plain
ed25519 (or multi-ed25519) signature; do not hash them first. The bytes are the view’s result
value, a 0x-prefixed hex string. PUBLIC_KEY in the next step is that key’s public key.
A grantor can also set a grant on chain with its own signer, as in Granting an Allowance.
3. Submit
supra move tool run \
--function-id 0x1::fungible_asset_allowance::apply_signed_grant \
--args address:<GRANTOR> address:<SPENDER> address:<METADATA> u64:<EXPECTED_REMAINING> \
u64:<AMOUNT> u64:<EXPIRY_SECS> u64:<NONCE> u64:<GENERATION> u8:<SCOPE> \
u8:<SCHEME> hex:<PUBLIC_KEY> hex:<SIGNATURE> \
--profile server --accept-defaultsWith the grant.json that sign-grant wrote, args is that argument list, and the submitter
passes it whole:
supra move tool run \
--function-id 0x1::fungible_asset_allowance::apply_signed_grant \
--args $(jq -r '.args[]' grant.json) \
--profile server --accept-defaultsjq -r '.args[]' prints one argument per line, and the shell splits the unquoted substitution into
one word per argument. No argument contains a space.
apply_signed_grant takes no signer: whoever submits the transaction pays for it, and the grant is
attributed to the address in the payload. SCHEME is 0 for ed25519 and 1 for multi-ed25519.
The signature is checked against the grantor’s current authentication key, so a signature made
before a key rotation is no longer valid after it. A signature that fails the check, or names a
scheme other than 0 or 1, aborts with EINVALID_SIGNATURE. On success the grant exists exactly
as if the grantor had called replace with the same values, and the nonce is recorded. Submitting
the same signature again aborts with ENONCE_ALREADY_USED while the expiry, generation and nonce
floor checks still admit it; once the expiry has passed, after revoke_all, or once the floor has
passed the nonce, an earlier check aborts first (EINVALID_EXPIRY, EGENERATION_MISMATCH or
ENONCE_BELOW_FLOOR).
The spender then spends the grant with transfer_from or transfer_from_store, as in
Spending an Allowance.
Reading EALLOWANCE_MISMATCH
apply_signed_grant writes the grant only while the live remaining amount of the grantor’s grant
for that spender and asset equals expected_remaining. It does not compare the expiry or scope of
the grant, so a grant that has returned to the same remaining amount accepts the signature. A grant
that is not live reads as 0, so a signature with expected_remaining of 0 is applicable
whenever no grant is live, including after a grant is spent, revoked or expires.
A signature that has not been submitted therefore stays applicable whenever the live remaining
amount equals its expected_remaining, until its nonce is recorded or falls below the floor, its
expiry passes, or the grantor calls revoke_all. A grantor who no longer wants an outstanding
signature to apply cancels it with cancel_nonce or invalidate_nonces_below; see
Cancelling a Signature Before It Is Submitted.
Any change to the live remaining amount between signing and submission produces
EALLOWANCE_MISMATCH, whether it comes from a spend, increase, decrease, replace, revoke
or the grant expiring. For example:
- A signature carrying
expected_remainingof0while a previous grant for the same spender and asset still has a live remainder. - A signature carrying a non-zero
expected_remainingwhose grant expires between signing and submission, since a grant that is no longer live reads as0.
The remedy is a fresh signature against the current value from the allowance view.
A server that batches submissions should read the view immediately before it asks the payer to
sign, and handle EALLOWANCE_MISMATCH by asking for a new signature.
Allocating nonces
Recorded nonces are held as a bitmap, one storage slot per block of 128 consecutive nonces. Only the first nonce recorded in a block creates a slot; the rest of that block carries no storage fee.
Allocate nonces densely from a low counter — take the next value from the min_nonce view, or from
a client-side counter seeded from it — rather than drawing them at random. Random u64 nonces put
each one in a block of its own, so every signed grant pays for a fresh storage slot. A block can be
reclaimed only after the floor passes it, which for random nonces voids every signature below that
floor.
Cancelling a Signature Before It Is Submitted
Three entry functions withdraw signatures the grantor has handed out, at three granularities:
cancel_nonce(nonce)— records one nonce without installing a grant, which voids the one signature carrying it and leaves the grantor’s other outstanding signatures usable. The nonce must be at or above the grantor’s floor and not already recorded; otherwise the call aborts withENONCE_BELOW_FLOORorENONCE_ALREADY_USED, and the signature is inadmissible either way.invalidate_nonces_below(new_floor)— raises the grantor’s nonce floor tonew_floor, which voids every signature naming a lower nonce whatever its expiry, generation or amount.new_floormust be above the current floor, otherwise the call aborts withEFLOOR_NOT_ADVANCED. A grantor that allocates nonces from a counter voids its whole outstanding batch in one call, by setting the floor to the counter’s current value. The grants themselves are untouched.revoke_all()— voids every grant and every signature at once, by advancing the generation.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::invalidate_nonces_below \
--args u64:<NEW_FLOOR> \
--profile grantor --accept-defaultsThe is_nonce_used(grantor, nonce) view reports true both for a nonce that has been recorded and
for one below the floor, so a submitter can test admissibility with one call.
One-Shot Settlement
A payer signs an UptoAuthorization off chain, and the facilitator it names settles it once with
settle_upto, moving any amount up to the authorization’s maximum from the payer’s primary store
to the recipient’s. Settling consumes the authorization whatever amount is settled, including zero.
Settlement does not read or write any grant, and the payer pays no gas.
In x402 terms, the client is the payer, the facilitator settles, pay_to is the resource server’s
payTo, and the authorization is the upto scheme’s payment payload. The facilitator’s /verify
maps to the verify_upto view and its /settle to settle_upto. Settling max_amount in full
serves the exact scheme with the same calls.
The payer signs with an ed25519 or multi-ed25519 key, and the key rules are those of
Off-Chain Signed Grants: a payer with no Account resource, such as a
new wallet that has only received the asset, signs with the key its address derives from.
1. Build the authorization
| Field | Value |
|---|---|
payer | The account whose primary store is debited. |
facilitator | The account that will submit settle_upto. No other account can settle the authorization. |
pay_to | The account whose primary store is credited. The store is created if absent. |
metadata | The asset’s metadata object address. |
max_amount | The most the payer’s store may be debited, including any fee the asset’s withdraw hook takes. |
valid_after | Unix seconds. The authorization can be settled from this second. |
valid_before | Unix seconds. The authorization can no longer be settled from this second. At most 3600 (MAX_UPTO_SPAN_SECS) after valid_after. |
nonce | 32 bytes, drawn at random per authorization. See Nonces, cancellation and removal. |
generation | The payer’s current generation, from the generation view. revoke_all voids authorizations signed under an earlier one. |
The chain id is filled in on chain, so an authorization made for one network is not valid on another. Every field is part of the signed payload, so a facilitator that changes the recipient, the maximum, the validity interval or any other field fails the signature check.
2. Get the bytes to sign
A payer whose key is in a supra CLI profile signs with:
supra move allowance sign-upto \
--facilitator <FACILITATOR> --pay-to <PAY_TO> --metadata <METADATA> \
--max-amount <MAX_AMOUNT> --valid-after <VALID_AFTER> --valid-before <VALID_BEFORE> \
--generation <GENERATION> --profile <PAYER_PROFILE> > upto.jsonThe payer is the profile’s account address. Without --nonce, the command draws 32 random bytes
and reports them as message.nonce. The payer keeps that value: cancel_upto takes it to cancel
the authorization before it is settled (see
Nonces, cancellation and removal). The command checks and signs
the view’s bytes as sign-grant does, and writes its result to standard output in the same form. An
authorization for up to 0.5 SUPRA (--metadata 0xa --max-amount 50000000), valid for 600 seconds
and signed on a localnet, produced:
{
"function_id": "0x1::fungible_asset_allowance::settle_upto",
"message": {
"chain_id": 255,
"payer": "0xd665a2956f7928bdc2753c22d9f6780de7a387cdc03cb6fa2e54bf2d4a23e64c",
"facilitator": "0xc2fe8c83494834c335ca3cb32c2a4354444dec05fa9ee764c637680499e64815",
"pay_to": "0x8cc2cb17b506e9c0e291f52b48210874198316f38f0a0a96eba87cad0e1a89ba",
"metadata": "0xa",
"max_amount": "50000000",
"valid_after": "1792058426",
"valid_before": "1792059026",
"nonce": "0xf93fd5e27dc0c320620771cb231d43df1c4f73642a7acef11a2405678404755b",
"generation": "0"
},
"account_scheme": 0,
"public_key": "0x98b8be18b0a91189b2252f3ebe251f9905d5f912f2358ee57c1f913bc959aa0f",
"signature": "0x82ff26db0dfa5d9e63717335d0f9ac5f78ba7d69cf48aa66344c0bd51a72ad5687a297d174a01d14f022fcb0005a9b07fae683f4055ad4a1d111b67b0dd0ec0b",
"args": [
"address:0xd665a2956f7928bdc2753c22d9f6780de7a387cdc03cb6fa2e54bf2d4a23e64c",
"address:0x8cc2cb17b506e9c0e291f52b48210874198316f38f0a0a96eba87cad0e1a89ba",
"address:0xa",
"u64:50000000",
"u64:1792058426",
"u64:1792059026",
"hex:0xf93fd5e27dc0c320620771cb231d43df1c4f73642a7acef11a2405678404755b",
"u64:0",
"u8:0",
"hex:0x98b8be18b0a91189b2252f3ebe251f9905d5f912f2358ee57c1f913bc959aa0f",
"hex:0x82ff26db0dfa5d9e63717335d0f9ac5f78ba7d69cf48aa66344c0bd51a72ad5687a297d174a01d14f022fcb0005a9b07fae683f4055ad4a1d111b67b0dd0ec0b",
"u64:<AMOUNT>"
]
}account_scheme, public_key and signature are <SCHEME>, <PUBLIC_KEY> and <SIGNATURE> in
steps 3 and 4. args is the whole --args list of settle_upto except the amount: its last entry,
u64:<AMOUNT>, is the placeholder the facilitator replaces with the amount it settles. The result
holds no secret. The payer gives upto.json to the facilitator, which uses it in steps 3 and 4.
A wallet or SDK that holds the payer’s key reads the same bytes from the view:
supra move tool view \
--function-id 0x1::fungible_asset_allowance::upto_signing_message \
--args address:<PAYER> address:<FACILITATOR> address:<PAY_TO> address:<METADATA> \
u64:<MAX_AMOUNT> u64:<VALID_AFTER> u64:<VALID_BEFORE> hex:<NONCE> u64:<GENERATION>The view returns the BCS encoding of ed25519::SignedMessage<UptoAuthorization>. Sign those bytes
with the payer’s key as a plain ed25519 (or multi-ed25519) signature, without hashing them first,
as in step 2 of Off-Chain Signed Grants.
3. Verify before settling
verify_upto takes the arguments settle_upto will take, with the facilitator’s address in place
of its signer, and reports what the settlement would do without aborting:
supra move tool view \
--function-id 0x1::fungible_asset_allowance::verify_upto \
--args address:<FACILITATOR> address:<PAYER> address:<PAY_TO> address:<METADATA> \
u64:<MAX_AMOUNT> u64:<VALID_AFTER> u64:<VALID_BEFORE> hex:<NONCE> u64:<GENERATION> \
u8:<SCHEME> hex:<PUBLIC_KEY> hex:<SIGNATURE> u64:<AMOUNT>With the upto.json that sign-upto wrote, those arguments are the facilitator’s own address
followed by args, with <AMOUNT> replaced by the amount to settle:
AMOUNT=25000000
supra move tool view \
--function-id 0x1::fungible_asset_allowance::verify_upto \
--args address:<FACILITATOR> \
$(jq -r --arg amount "$AMOUNT" '.args[] | sub("<AMOUNT>"; $amount)' upto.json)It returns [true, "0"] when the settlement would succeed, and otherwise false with the full
abort status settle_upto would raise, category included, for example 65565 (0x1001d,
EUPTO_NONCE_USED). It applies settle_upto’s admission rules, which are, in order:
initializehas run (EMODULE_NOT_INITIALIZED).nonceis 32 bytes (EUPTO_INVALID_NONCE).valid_afteris at or before the current block time (EUPTO_NOT_YET_VALID).- The current block time is before
valid_before(EUPTO_EXPIRED). valid_before - valid_afteris at most 3600 (EUPTO_SPAN_TOO_LONG).amountis at mostmax_amount(EUPTO_AMOUNT_EXCEEDS_MAX).generationis the payer’s current generation (EGENERATION_MISMATCH).- The payer has no record of
nonce(EUPTO_NONCE_USED). - The signature verifies over the authorization with the facilitator set to the caller
(
EINVALID_SIGNATURE). - The issuer has not blocked delegated spending of the asset (
EALLOWANCES_DISABLED). - For a non-zero
amount, the payer has a primary store of the asset (ESOURCE_STORE_MISSING).
For a non-zero amount it then reports a frozen payer store as EUPTO_STORE_FROZEN, a payer
balance below amount as EUPTO_INSUFFICIENT_BALANCE, and a frozen recipient store as
EUPTO_STORE_FROZEN. settle_upto aborts inside 0x1::fungible_asset in those three cases rather
than with these codes. The view does not run the asset’s withdraw or deposit hooks, so for an asset
with hooks a settlement can still abort after [true, "0"]: with EUPTO_AMOUNT_EXCEEDS_MAX when a
withdraw-hook fee takes the debit above max_amount, or with the hook’s own code.
4. Settle
The facilitator submits:
supra move tool run \
--function-id 0x1::fungible_asset_allowance::settle_upto \
--args address:<PAYER> address:<PAY_TO> address:<METADATA> u64:<MAX_AMOUNT> \
u64:<VALID_AFTER> u64:<VALID_BEFORE> hex:<NONCE> u64:<GENERATION> \
u8:<SCHEME> hex:<PUBLIC_KEY> hex:<SIGNATURE> u64:<AMOUNT> \
--profile facilitator --accept-defaultsWith upto.json, the arguments are args with <AMOUNT> replaced:
supra move tool run \
--function-id 0x1::fungible_asset_allowance::settle_upto \
--args $(jq -r --arg amount "$AMOUNT" '.args[] | sub("<AMOUNT>"; $amount)' upto.json) \
--profile facilitator --accept-defaultsAMOUNT is the metered amount, from 0 to MAX_AMOUNT. In order, the call records the nonce as
settled until VALID_BEFORE, withdraws AMOUNT from the payer’s primary store, requires that the
store was debited at most MAX_AMOUNT, deposits into PAY_TO’s primary store, and emits
UptoSettled. With AMOUNT of 0 it records the nonce and moves nothing. Any abort reverts the
whole call, so a settlement that fails leaves the nonce unrecorded. A Move module can call
settle_upto_internal with the same arguments, which returns the amount the payer’s store was
debited.
UptoSettled carries payer, facilitator, pay_to and metadata as account and metadata
addresses, the nonce, max_amount, amount (what the payer’s store was debited) and received
(what reached the recipient’s deposit), which is what an x402 settlement response reports.
Several authorizations from one payer can be outstanding at once and settled in any order, since each names its own nonce.
Nonces, cancellation and removal
A settlement nonce must be unique among the payer’s unexpired authorizations. Drawing 32 random
bytes per authorization meets that. Settling records the nonce against the payer until the
authorization’s valid_before; once the block time reaches it, anyone may remove the record, and
the same nonce may then be recorded again. An authorization whose own valid_before has passed is
refused whether or not its record still exists.
A payer withdraws an authorization before it is settled in one of three ways:
cancel_upto(nonce, valid_before), from the payer’s signer, recordsnonceas cancelled, so no authorization carrying it can be settled until the record is removed. Pass the latestvalid_beforeamong the authorizations being cancelled; the record lasts until the later of that value and the current block time. It aborts withEUPTO_NONCE_USEDwhen the payer already has a record ofnonce, in which case no authorization carrying it can be settled either.- A payer without gas signs a second authorization with the same nonce and
max_amountof0, and has it settled for0. This works only if the second authorization’svalid_beforeis at or after the original’s: the record the second leaves lasts until its ownvalid_before, and the original must have expired by the time the record can be removed. Two authorizations that share a nonce and both have a nonzeromax_amountare mutually exclusive only when theirvalid_beforeis equal. revoke_all()advances the payer’s generation, which voids every authorization and every signed grant made under the old one. A key rotation voids every signature made with the old key.
upto_nonce_state(payer, nonce) returns 0 when the payer has no record, 1 for a settled
record and 2 for a cancelled one. A removed record also reads 0, so to decide whether an
authorization can still be settled, compare its valid_before with the current block time as well,
or call verify_upto.
Each record is a table item whose storage deposit is paid by the gas payer of the transaction that
writes it, which is the facilitator for a settlement and the payer for cancel_upto. Removal
refunds the deposit to the gas payer of the removing transaction:
remove_expired_upto_nonce(payer, nonce)removes one record. It aborts withEUPTO_NONCE_NOT_FOUNDwhen there is none and withEUPTO_NONCE_NOT_EXPIREDbefore itsvalid_before.remove_expired_upto_nonces(payers, nonces)removes the record named by each position of the two lists that has reached itsvalid_before, and skips positions with no record or an unexpired one. It aborts withEUPTO_LENGTH_MISMATCHwhen the lists differ in length.
A facilitator recovers its deposits by keeping the payer and nonce of each settlement and, once
their valid_before has passed, removing them in batches with remove_expired_upto_nonces.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::remove_expired_upto_nonces \
--args 'address:["<PAYER_1>","<PAYER_2>"]' 'hex:["<NONCE_1>","<NONCE_2>"]' \
--profile facilitator --accept-defaultsx402 exact Without the Framework Module
x402’s Aptos scheme for exact has the client sign a transaction calling
0x1::primary_fungible_store::transfer, with the facilitator as its fee payer, and the facilitator
submits it. SupraMVM admits a sender with no Account resource only in a fee-payer transaction
with sequence number 0, signed with the key its address is derived from; executing that transaction
creates the sender’s Account resource. The features this relies on, fee-payer transactions
(FEE_PAYER_ENABLED, 22) and a sponsored sender with no Account resource
(SPONSORED_AUTOMATIC_ACCOUNT_CREATION, 34), are enabled on Mainnet and Testnet. Nodes up to and
including supra_node_v11.7.0 refuse such a transaction at submission, and later releases admit
it. Mainnet and Testnet run supra_node_v11.7.0, so on them the flow works only for a sender that
already has an Account resource. Devnet runs a later build, which admits a sender with no
Account resource.
The flow needs nothing from fungible_asset_allowance, and its replay protection is the payer’s
sequence number, so one payer’s payments are ordered, and a Move transaction must expire within a
bound the node sets after its submission. The bound configured on Devnet’s node is listed in the
description of the Submit transaction (v4) operation on the Devnet API page,
https://rpc-devnet.supra.com/docs . It cannot serve upto,
since the amount is fixed when the client signs.
Issuer Controls
The issuer of an asset can block delegated spending of it. The asset is one the issuer created; creating one is outside this page. The signer that owns the metadata object, directly or through intermediate objects, calls:
supra move tool run \
--function-id 0x1::fungible_asset_allowance::set_allowances_disabled \
--args address:<METADATA> bool:true \
--profile issuer --accept-defaultsfalse unblocks it again. The function requires the signer to own the metadata object. 0x1 owns
the metadata object of SUPRA (0xa), so delegated spending of SUPRA can be blocked only by a
governance proposal executed with the framework signer. The issuer of an asset paired with a coin
calls set_paired_allowances_disabled<CoinType> from Move instead; it is not an entry function and
needs the coin’s FreezeCapability. Grants can still be recorded while an asset is disabled; only
transfer_from, transfer_from_store and settle_upto are refused, with EALLOWANCES_DISABLED. A Move
package creating an asset can call disable_allowances(&constructor_ref) at creation. The
allowances_disabled(metadata) view reports the setting.
Reading State and Events
Views, all under 0x1::fungible_asset_allowance:
| View | Returns |
|---|---|
allowance(grantor, spender, metadata) | [remaining, expiry_secs, scope], or ["0", "0", 0] if not live. |
generation(grantor) | The grantor’s current generation. |
is_nonce_used(grantor, nonce) | Whether a signed grant naming this nonce can no longer be admitted, because the nonce is recorded or below the floor. |
min_nonce(grantor) | The lowest nonce a signed grant may still name. |
nonce_bucket_bits(grantor, bucket) | The recorded-nonce bits of one block of 128 nonces. 0 means the block holds nothing. |
reclaimable_bucket_limit(grantor) | The exclusive upper bound on reclaimable block indices. |
allowances_disabled(metadata) | Whether the issuer has blocked delegated spending. |
grant_signing_message(...) | The bytes a grantor signs for a signed grant. |
upto_signing_message(...) | The bytes a payer signs for an authorization. |
verify_upto(...) | [true, "0"] when settle_upto with these arguments would succeed, otherwise false and the abort status. |
upto_nonce_state(payer, nonce) | 0 with no record, 1 for a settled record, 2 for a cancelled one. |
allowances_object_address(grantor) | The object that holds the grantor’s grants. |
control_object_address(metadata) | The object that holds the asset’s issuer setting. |
Via the REST API. In the examples, <RPC_HOST> is the RPC host with its scheme, such as
https://rpc-devnet.supra.com:
curl --request POST --url '<RPC_HOST>/rpc/v3/view' \
--header 'Content-Type: application/json' \
--data '{"function":"0x1::fungible_asset_allowance::allowance",
"type_arguments":[],
"arguments":["<GRANTOR>","<SPENDER>","<METADATA>"]}'Events, emitted as module events with these types, let a wallet show and revoke allowances and let an indexer rebuild delegated-spending history:
| Event | Emitted when | Fields |
|---|---|---|
GrantChanged | A grant is created, changed or removed. | grantor, spender, metadata, remaining, expiry_secs, generation, scope. Removal reports remaining = 0. |
GrantConsumed | A transfer_from or transfer_from_store succeeds. | grantor, spender, metadata, store, to, transferred, charged, remaining. store and to are store addresses. |
NonceConsumed | A grant nonce is recorded, by apply_signed_grant or cancel_nonce. | grantor, nonce, installed_grant. installed_grant is false for cancel_nonce. |
UptoSettled | A settle_upto succeeds. | payer, facilitator, pay_to, metadata, nonce, max_amount, amount (debited), received (credited). payer and pay_to are account addresses. |
UptoCancelled | cancel_upto runs. | payer, nonce. |
NoncesInvalidated | invalidate_nonces_below runs. | grantor, min_nonce (the new floor). |
AllGrantsRevoked | revoke_all runs. | grantor, generation (the new value). |
AllowancesDisabledChanged | An issuer changes the setting. | metadata, disabled. |
A grant recorded in GrantChanged is live only while its generation equals the grantor’s latest
AllGrantsRevoked.generation (or 0 if none) and its expiry_secs is in the future. An indexer
that shows allowances should apply both tests rather than trust the last remaining it saw.
NonceConsumed is the only event naming a consumed grant nonce; nothing else in the stream reports
one. Settlement nonces appear in UptoSettled and UptoCancelled. Removing a settlement nonce
record emits no event.
Storage and Reclamation
Grants live on an object derived from the grantor’s address, and issuer settings on an object derived from the metadata address. Whoever writes first creates the object and pays its storage deposit; for a signed grant that is the submitter. Table entries for grants and for blocks of recorded nonces are paid for the same way.
Two entry functions let anyone delete entries that can no longer be used, and the transaction’s gas payer receives the storage refund:
remove_dead(grantor, spender, metadata)— a grant that is expired, fully spent, or from an old generation. Aborts withEGRANT_STILL_LIVEotherwise.remove_used_nonce_bucket(grantor, bucket)— one block of 128 recorded nonces that the grantor’s nonce floor has passed entirely. Aborts withEBUCKET_NOT_INVALIDATEDwhile any nonce the block covers is still at or above the floor, and withEBUCKET_NOT_FOUNDwhen the block holds no record.
A reclaimer needs no record of which nonces a grantor used. It reads
reclaimable_bucket_limit(grantor) and calls remove_used_nonce_bucket for each index below that
bound; the indices that hold nothing abort with EBUCKET_NOT_FOUND and can be skipped, and
nonce_bucket_bits(grantor, bucket) identifies them in advance, reading 0.
Blocks become reclaimable only as the floor advances, so a grantor that wants its nonce storage
released calls invalidate_nonces_below once it has moved past a range of nonces.
Settlement nonce records are items of one table held by the module’s registry at 0x1, keyed by
payer and nonce. Nonces, cancellation and removal covers who
pays for them and how they are removed.
Abort Codes
All in 0x1::fungible_asset_allowance. The full status code is the category prefix followed by the
code, for example 0x10002 for EINSUFFICIENT_ALLOWANCE.
| Code | Status | Name | When |
|---|---|---|---|
| 1 | 0x60001 | EALLOWANCE_NOT_FOUND | No live grant for this grantor, spender and asset. |
| 2 | 0x10002 | EINSUFFICIENT_ALLOWANCE | The spend, including any hook fee, exceeds the live remaining amount. |
| 3 | 0x10003 | EINVALID_EXPIRY | Expiry is not later than the current block time. |
| 4 | 0x10004 | EEXPIRY_TOO_FAR | Expiry is more than 366 days ahead. |
| 5 | 0x30005 | EALLOWANCE_MISMATCH | expected_remaining does not equal the live remaining amount. |
| 6 | 0x10006 | ENONCE_ALREADY_USED | This grantor has already recorded this grant nonce. |
| 7 | 0x50007 | EALLOWANCES_DISABLED | The issuer has blocked delegated spending of this asset. |
| 8 | 0x60008 | ESOURCE_STORE_MISSING | The grantor or payer has no primary store for this asset. |
| 9 | 0x30009 | EGRANT_STILL_LIVE | remove_dead on a live grant. |
| 10 | 0x1000a | EAMOUNT_CANNOT_BE_ZERO | increase by zero, or a zero spend. |
| 11 | 0x5000b | ENOT_METADATA_OWNER | The signer does not own the metadata object. |
| 12 | 0x6000c | EMODULE_NOT_INITIALIZED | initialize has not run on this chain. |
| 13 | 0x2000d | EALLOWANCE_OVERFLOW | increase would exceed u64. |
| 14 | 0x3000e | EGENERATION_MISMATCH | The signed grant’s or authorization’s generation is not the signer’s current one. |
| 15 | 0x1000f | ENOT_FUNGIBLE_ASSET_METADATA | disable_allowances on a constructor ref that is not a Metadata object. |
| 16 | 0x10010 | EINVALID_SCOPE | scope is neither 0 nor 1. |
| 17 | 0x30011 | EALLOWANCE_SCOPE_MISMATCH | increase on a live grant with a different scope. |
| 18 | 0x50012 | ESTORE_OUT_OF_SCOPE | The source store is outside the grant’s scope. |
| 19 | 0x10013 | ENONCE_BELOW_FLOOR | The grant nonce is below the grantor’s nonce floor. |
| 20 | 0x10014 | EFLOOR_NOT_ADVANCED | invalidate_nonces_below with a floor at or below the current one. |
| 21 | 0x30015 | EBUCKET_NOT_INVALIDATED | remove_used_nonce_bucket on a block the floor has not passed entirely. |
| 22 | 0x60016 | EBUCKET_NOT_FOUND | remove_used_nonce_bucket for a block this grantor has no record of. |
| 23 | 0x60017 | ECOIN_NOT_PAIRED | set_paired_allowances_disabled for a coin with no paired fungible asset. |
| 24 | 0x10018 | EUPTO_INVALID_NONCE | A settlement nonce is not 32 bytes. |
| 25 | 0x30019 | EUPTO_NOT_YET_VALID | The block time is before the authorization’s valid_after. |
| 26 | 0x3001a | EUPTO_EXPIRED | The block time is at or after the authorization’s valid_before. |
| 27 | 0x1001b | EUPTO_SPAN_TOO_LONG | valid_before is more than 3600 seconds after valid_after. |
| 28 | 0x1001c | EUPTO_AMOUNT_EXCEEDS_MAX | The amount, or the debit including any withdraw-hook fee, is above max_amount. |
| 29 | 0x1001d | EUPTO_NONCE_USED | The payer already has a record of this settlement nonce. |
| 30 | 0x1001e | EINVALID_SIGNATURE | The scheme is not 0 or 1, the public key does not derive the signer’s authentication key, or the signature does not verify. Applies to apply_signed_grant and settle_upto. |
| 31 | 0x3001f | EUPTO_NONCE_NOT_EXPIRED | remove_expired_upto_nonce before the record’s valid_before. |
| 32 | 0x60020 | EUPTO_NONCE_NOT_FOUND | remove_expired_upto_nonce for a record the payer does not have. |
| 33 | 0x10021 | EUPTO_INSUFFICIENT_BALANCE | Reported by verify_upto only: the payer’s balance is below the amount. |
| 34 | 0x50022 | EUPTO_STORE_FROZEN | Reported by verify_upto only: the payer’s or the recipient’s primary store is frozen. |
| 35 | 0x10023 | EUPTO_LENGTH_MISMATCH | remove_expired_upto_nonces with lists of different lengths. |