Recognising Operations a Contract Performs on an Account
A Move entry function receives the sender’s signer (&signer) and can pass it to any module it
calls. Code holding the signer can perform operations on the sender’s account that the transaction’s
payload does not name. This article covers the operations whose effect outlasts the transaction,
moves the account’s SUPRA, or passes control of the account or its assets to another party. For each
one it states what the operation lets another party do, how to recognise it in a transaction
simulation before signing, how to see afterwards what the account has granted or registered, and how
to undo it.
It has two readers. The subsections headed For wallet designers say what a wallet can detect and what to show. The subsections headed For account holders say what a wallet’s warning means.
| Operation | What another party can do afterwards | For how long |
|---|---|---|
| Registering an automation task | The chain sends transactions from the account that call the task’s entry function, and the registry charges the account an automation fee each cycle. | Until the task’s expiry_time, or until it is cancelled, stopped or removed. |
| Granting a fungible asset allowance | The spender moves up to the granted amount of the asset out of the account, without the account’s signature. | Until the grant’s expiry_secs, at most 366 days after it was written, or until it is spent, revoked or voided. |
| Withdrawing or transferring SUPRA, including a crossing to SupraEVM | Nothing further: the value has left the account. | Final when the transaction commits. |
| Changing a stake pool’s operator or delegated voter, or moving its owner capability | The operator runs the pool’s validator. The holder of the owner capability controls the pool and its stake. | Until the owner sets another address, or for as long as the other party holds the capability. |
| Passing control of the account itself | The recipient of a signer-capability offer acts as the account; the recipient of a rotation-capability offer replaces its key; the owners of a multisig account act for it. | Until the offer is revoked or the key is changed. A multisig conversion cannot be undone. |
Each of these operations emits an event, and each appears in the write set that
POST /rpc/v4/transactions/simulate returns. Availability lists the framework and
node versions each depends on.
Availability
| Operation or surface | Version | Also requires |
|---|---|---|
fungible_asset_allowance: grants, signed grants, revocation | Not in v1.8.17 or earlier. See Fungible Asset Allowances for the framework release that carries it. | fungible_asset_allowance::initialize must have run. A new chain runs it at genesis. On an existing chain the framework upgrade must call it with the framework signer; until then every write that creates grant state aborts with EMODULE_NOT_INITIALIZED (0x6000c); revoke does nothing, decrease and transfer_from abort with EALLOWANCE_NOT_FOUND, and the allowance view reports no grant. |
fungible_asset_allowance::set_paired_allowances_disabled | Framework aptosvm-v1.16_supra-v1.9.0.rc14. | Nothing further. |
automation_registry::register_without_validation: task registration from a Move module | New in the v1.9.0 framework. | Features SUPRA_NATIVE_AUTOMATION (88) and SUPRA_AUTOMATION_V2_1 (101). With 101 off the call aborts with EDISABLED_AUTOMATION_V2_1_FEATURE (49). |
Task registration through an automation_registration_payload transaction | Before v1.9.0. | Feature SUPRA_NATIVE_AUTOMATION (88). |
evm_settlement::deposit, evm_settlement::deposit_to_evm: crossings to SupraEVM | evm_settlement is new in the v1.9.0 framework. | Feature SUPRA_EVM_SETTLEMENT (103). See Performing Cross-VM Transfers. |
stake::set_operator, stake::set_delegated_voter, coin and primary_fungible_store withdrawals, and the account, resource_account and multisig_account functions below | Before v1.9.0. | Nothing further. |
The events SetDelegatedVoter, ExtractOwnerCap, DepositOwnerCap, DestroyOwnerCap, RotationCapabilityOfferUpdate, SignerCapabilityOfferUpdate, ResourceAccountCreation, SignerCapabilityStored, SignerCapabilityRetrieved, CreateWithExistingAccount, and KeyRotation from every authentication-key write | Framework aptosvm-v1.16_supra-v1.9.0.rc14. rc13 does not emit them, except that rotate_authentication_key emits KeyRotation before rc14; rc14 extends it to every authentication-key write. | A framework without them performs the same operations without these events. The v4 simulate write set shows the operations either way. |
changes, prior_values and the other state-change fields of the simulate response | RPC node releases after v11.7.0, on /rpc/v4/transactions/simulate only. | Decoded table items need the node’s table-info index; see Table items. |
Simulating a Transaction Before Signing
The RPC node executes a transaction without committing it at:
POST /rpc/v4/transactions/simulateThe body is a SupraTransaction: either JSON of the form {"Move": <SignedTransaction>} with
Content-Type: application/json, or its BCS encoding with Content-Type: application/x.supra.signed_transaction+bcs. The node’s OpenAPI description, the JSON document at
/docs/api.json that its /docs page renders, carries a complete JSON example. /rpc/v1,
/rpc/v2 and /rpc/v3/transactions/simulate accept the same body and return the transaction
without the state-change fields described below.
The transaction’s signature must not verify. The node refuses a transaction whose signature is
valid, with status 400 and the message Transaction to be simulated should not have valid signature. A wallet simulates the transaction with a zeroed signature, as the OpenAPI example does,
and asks for the real signature only after the user has seen the result.
A supra CLI command that sends a transaction simulates it on the RPC node instead when given
--simulate, for example supra move tool run --function-id <FUNCTION> --args <ARGS> --simulate.
The response
The response is a TransactionSimulationV4: the fields of a TransactionInfoV2, as
/rpc/v3/transactions/simulate returns them, and six more.
{
"authenticator": { "Move": { "...": "..." } },
"block_header": null,
"hash": "0x...",
"header": { "...": "..." },
"payload": {
"Move": {
"type": "entry_function_payload",
"function": "0x<MODULE_ADDRESS>::<MODULE>::<FUNCTION>",
"type_arguments": [],
"arguments": []
}
},
"output": {
"Move": {
"gas_used": 0,
"events": [
{
"guid": { "creation_number": "0", "account_address": "0x0" },
"sequence_number": "0",
"type": "0x1::stake::SetDelegatedVoter",
"data": { "pool_address": "0x...", "old_voter": "0x...", "new_voter": "0x..." }
}
],
"vm_status": "..."
}
},
"status": "Success",
"state_height": 1234,
"changes": [
{
"type": "write_resource",
"address": "0x<POOL>",
"state_key_hash": "0x<HASH>",
"data": {
"type": "0x1::stake::StakePool",
"data": { "delegated_voter": "0x<NEW_VOTER>", "...": "..." }
}
}
],
"prior_values": {
"0x<HASH>": [
{
"type": "write_resource",
"address": "0x<POOL>",
"state_key_hash": "0x<HASH>",
"data": {
"type": "0x1::stake::StakePool",
"data": { "delegated_voter": "0x<OLD_VOTER>", "...": "..." }
}
}
]
},
"omitted": [],
"omitted_counts": { "resource": 0, "resource_group": 0, "module": 0, "table_item": 0 },
"changes_truncated": false
}| Field | Content |
|---|---|
payload | The entry function the transaction calls, or its script. A module the transaction calls can perform any of the operations below without the payload naming them. |
output.Move.events | Every event the transaction emitted. type is the fully qualified name of the event struct, with framework addresses written short (0x1). A module event has a guid of account 0x0 and creation number 0; a legacy handle event has a guid naming the address that created the handle. |
output.Move.vm_status, status | The result. status is Success when execution succeeded. |
state_height | The height of the last block the node had executed when it took the snapshot the simulation ran against, or null for the genesis state. |
changes | Every change the transaction would make to Move state, in the Aptos API’s representation: write_resource, delete_resource, write_table_item, delete_table_item, write_module and delete_module, each with the state_key_hash of the state key it changes. |
prior_values | For each state_key_hash in changes, the value the state key held before the transaction, as the write_* changes that would write it, or null when it held no value. |
omitted, omitted_counts, changes_truncated | The state keys left out of changes and prior_values to keep the two within the node’s size limit. |
How the state changes are reported:
- Order. State keys are listed in priority order: resources and resource groups at the transaction’s sender, secondary signers and fee payer; then other resources and resource groups; then table items; then modules. Within each class they are in state key order.
- Resource groups. A resource group, such as
0x1::object::ObjectGroupon an object, is one state key. A written group is reported as onewrite_resourceper member, each carrying the group’sstate_key_hash, in bothchangesandprior_values; comparing the two shows which member changed. A deleted group is onedelete_resourcewhoseresourceis the group’s tag, and itsprior_valuesentry lists the members. - Size limit.
changesandprior_valuestogether are bounded by a size the node sets, spent in priority order. A state key that does not fit is left out of both and the walk continues with the next one. Each left-out key is listed inomittedas{state_key_hash, address, kind, tag}, up to a number of entries the node sets, andomitted_countscounts every left-out key by kind.changes_truncatedistrueexactly when a key was left out. The bounds configured on Devnet’s node are listed in the description of theSimulate a transaction (v4)operation on the Devnet API page, https://rpc-devnet.supra.com/docs . - One snapshot. The simulation,
prior_valuesandstate_heightare read from one snapshot of the node’s database. The node keeps Move state for the latest block only, soprior_valuescannot be read back from the node once a later block commits. - Execution can differ. The committed transaction executes against the state at its position in its block, so a module whose behaviour depends on state can make different changes when the transaction commits.
Table items
Several operations below write only table items: an allowance grant, an automation task. A
write_table_item carries the table’s handle and the item’s key and value as hex-encoded BCS
bytes, and data with the key and value decoded and their types named:
{
"type": "write_table_item",
"state_key_hash": "0x<HASH>",
"handle": "0x<TABLE_HANDLE>",
"key": "0x...",
"value": "0x...",
"data": {
"key": { "spender": "0x<SPENDER>", "metadata": "0xa" },
"key_type": "0x1::fungible_asset_allowance::GrantKey",
"value": { "remaining": "1000", "expiry_secs": "1790000000", "generation": "0", "scope": 0 },
"value_type": "0x1::fungible_asset_allowance::Grant"
}
}A delete_table_item carries data with key and key_type. data is present in changes and
prior_values alike, both for tables that existed before the transaction and for tables it
creates. The types come from an index each RPC node keeps from table handle to key and value types,
controlled by the node’s move_table_info_indexing setting, which defaults to on. An item of a table
the index has no entry for is reported without data: that happens while a node rebuilds the index
after it starts behind its Move state, including the first start after an upgrade, and for any table
on a node that has run with the setting off.
Automation Task Registration
What it permits
An automation task holds payload_tx, a BCS-encoded EntryFunction. While the task is active, the
chain executes that entry function in automated transactions whose sender is the task’s owner, each
with at most max_gas_amount gas at a gas price no higher than gas_price_cap.
Registration transfers the registry’s flat registration fee plus a deposit equal to
automation_fee_cap_for_epoch from the owner’s SUPRA. At each automation cycle transition the
registry charges the owner that cycle’s automation fee from the owner’s SUPRA balance:
- if the fee exceeds
automation_fee_cap_for_epoch, the task is removed and its deposit refunded, with aTaskCancelledCapacitySurpassedV2event; - if the owner’s balance is below the fee, the task is removed and its deposit is not refunded,
with a
TaskCancelledInsufficentBalanceV2event; - otherwise the fee is withdrawn, with a
TaskEpochFeeWithdrawevent.
A task registered during a cycle is PENDING (state 0) and becomes ACTIVE (state 1) at the
next cycle transition. It stays registered until its expiry_time, which registration requires to
be later than the end of the current cycle and at most task_duration_cap_in_secs after
registration. automation_registry::get_automation_registry_config() returns the current
task_duration_cap_in_secs and flat_registration_fee_in_quants.
A task is registered in one of three ways:
- An
automation_registration_payloadtransaction, which the owner signs for that purpose. The simulate response’spayload.Move.typeisautomation_registration_payload. - A multisig transaction that carries an automation registration payload. The task is owned by the multisig account.
automation_registry::register_without_validation(owner_signer, payload_tx, expiry_time, max_gas_amount, gas_price_cap, automation_fee_cap_for_epoch, aux_data), apublic funthat any Move module holding the owner’s signer can call. It does not check thatpayload_txdecodes to a callable entry function. The runtime removes a task whose payload it cannot build a transaction from, refunds the owner asstop_tasksdoes, and emitsTaskCancelledByRuntimewith adiagnostic_message.
Recognising it in a simulation
All in 0x1::automation_registry:
| Event | Fields | Emitted when |
|---|---|---|
AutomationTaskMetaData | task_index, owner, payload_tx, expiry_time, tx_hash, max_gas_amount, gas_price_cap, automation_fee_cap_for_epoch, aux_data, registration_time, state, locked_fee_for_next_epoch | A task is registered. state is 0. |
TaskRegistrationDepositFeeWithdraw | task_index, owner, registration_fee, locked_deposit_fee | The registration fee and deposit are taken from owner. |
The fee transfer also emits the SUPRA withdrawal events listed under Recognising a withdrawal in a simulation.
A transaction registers a task for the signing account when it emits AutomationTaskMetaData with
owner equal to the sender.
In changes:
- The task is a
write_table_itemin the table behind thetasksmap of0x1::automation_registry::AutomationRegistryV2. Itsdata.key_typeisu64, the key is thetask_index, and itsdata.value_typeis0x1::enumerable_map::Tuple<0x1::automation_registry::AutomationTaskMetaData>, whosevaluefield is the task. Itsprior_valuesentry isnull, since the index is new. AutomationRegistryV2andAutomationRefundBookkeepingare members of the0x1::object::ObjectGroupat0x1, which is reported as awrite_resourceper member.- The owner’s SUPRA balance changes as for a withdrawal.
Checking what an account has registered
No view lists tasks by owner. get_task_ids() returns every registered task index, and
get_task_details_bulk(task_indexes) returns the AutomationTaskMetaData of each; the account’s
tasks are those whose owner is the account. state is 0 for pending, 1 for active and 2 for
cancelled. has_sender_active_task_with_id(sender, task_index) reports whether one task is owned by
sender and not pending.
supra move tool view --function-id 0x1::automation_registry::get_task_ids
supra move tool view --function-id 0x1::automation_registry::get_task_details_bulk --args "u64:[0]"
supra move tool view --function-id 0x1::automation_registry::has_sender_active_task_with_id \
--args address:<OWNER> u64:0GET /rpc/v3/accounts/{address}/automated_transactions lists the automated transactions executed
for the tasks the account registered.
A task is registered with supra move automation register, which takes the entry function the task
calls and the task’s limits:
supra move automation register \
--function-id 0x1::supra_account::transfer --args address:<RECIPIENT> u64:1 \
--task-max-gas-amount 5000 --task-gas-price-cap 1000000 \
--task-expiry-time-secs <EXPIRY_UNIX_SECS> --task-automation-fee-cap 1000000000 \
--profile owner --accept-defaultsCancelling a task
Both functions take the owner’s signer and abort with ECYCLE_TRANSITION_IN_PROGRESS (32) while a
cycle transition is in progress; the same call succeeds once the transition finishes.
automation_registry::cancel_task(task_index)removes a pending task and refunds half its deposit. It marks an active task cancelled (state2); a cancelled task keeps running until the end of the current cycle, and its deposit is refunded when the cycle ends. EmitsTaskCancelledV2.automation_registry::stop_tasks(task_indexes)removes the tasks at once. For an active or cancelled task it refunds the full deposit and half the fee for the rest of the current cycle; for a pending task it refunds half the deposit. EmitsTasksStoppedV2.
supra move automation cancel --task-index 0 --profile owner --accept-defaults
supra move automation stop-tasks --task-indexes 0 1 2 --profile owner --accept-defaults--task-index takes one index and --task-indexes a space-separated list of indexes.
For wallet designers
- For every
AutomationTaskMetaDatawhoseowneris the signing account, decodepayload_txas a BCSEntryFunctionand show its module, function and arguments. Showexpiry_timeas a date, andmax_gas_amount,gas_price_cap,automation_fee_cap_for_epochand theTaskRegistrationDepositFeeWithdrawamounts in SUPRA. - State that the task sends transactions from the account calling that function until
expiry_time, and that the registry takes a fee from the account’s SUPRA every cycle. - A registration in a transaction whose payload type is not
automation_registration_payloadwas made by a module the transaction called. - List the account’s tasks with the views above and offer
cancel_taskandstop_tasks.
For account holders
A warning about an automation task means this transaction sets up a job that keeps sending
transactions from your account after this one has finished. Each of those transactions calls the
function the wallet shows. The job takes a fee from your SUPRA every cycle until the date shown.
Cancelling it (supra move automation cancel) stops it at the end of the current cycle; stopping it
(supra move automation stop-tasks) removes it at once.
Fungible Asset Allowances
Fungible Asset Allowances describes the module in full. The module is available on Devnet only. This section covers recognising and undoing a grant.
What it permits
A grant lets spender move up to remaining of the asset whose metadata object is metadata out of
the grantor’s stores, with fungible_asset_allowance::transfer_from or transfer_from_store,
without the grantor’s signature, until expiry_secs. scope 0 limits it to the grantor’s primary
store for the asset; scope 1 extends it to every store of the asset the grantor owns directly.
The spender can draw on balances that arrive after the grant.
For SUPRA, metadata is 0xa, and a grant reaches only balances held in the grantor’s SUPRA
primary fungible store, not CoinStore<SupraCoin>.
expiry_secs is at most 366 days after the block time at which the grant is written. Each
increase or replace writes a new expiry.
A grant is written from the grantor’s signer by increase, replace or decrease. They are
public entry functions, so a Move module holding the grantor’s signer can call them.
A grant can also be written from a signature. The grantor signs a message off chain; anyone can
then submit it with apply_signed_grant, and the spender spends the grant with transfer_from or
transfer_from_store. The grant is written in a transaction the grantor did not send. A signature
can be submitted while all of these hold: its expiry_secs is after the current block time and no
more than 366 days after it; its nonce is unused and at or above the grantor’s min_nonce; its
generation equals the grantor’s current generation; the key that signed it derives the
grantor’s authentication key, which for an address with no Account resource is the address
itself; and its expected_remaining equals the grant’s live remaining amount, otherwise the call
aborts with EALLOWANCE_MISMATCH.
An asset’s issuer can block delegated spending of it, so that transfer_from and
transfer_from_store abort with EALLOWANCES_DISABLED while grants stay recorded. The owner of the
asset’s metadata object does it with set_allowances_disabled. For the fungible asset paired with a
coin, whose metadata object the framework owns, the holder of the coin’s FreezeCapability does it
with set_paired_allowances_disabled<CoinType>(freeze_cap, disabled). A Move package creating an
asset can also call disable_allowances(&constructor_ref) at creation. SUPRA has no
FreezeCapability holder: supra_coin destroys it when it initialises the coin.
set_allowances_disabled requires the signer to own the metadata object, and 0x1 owns that of
SUPRA (0xa), so delegated spending of SUPRA can be blocked only by a governance proposal executed
with the framework signer. The setter functions emit
AllowancesDisabledChanged { metadata, disabled }, except that unblocking an asset that was never
blocked emits nothing. The allowances_disabled(metadata) view reports the setting.
Recognising a grant in a simulation
All in 0x1::fungible_asset_allowance:
| Event | Fields | Emitted when |
|---|---|---|
GrantChanged | grantor, spender, metadata, remaining, expiry_secs, generation, scope | A grant is written. remaining above zero creates or changes it; remaining of zero removes it. |
NonceConsumed | grantor, nonce, installed_grant | A signed grant is applied (installed_grant is true), or the grantor cancels a nonce (false). |
AllGrantsRevoked | grantor, generation | The grantor calls revoke_all. |
GrantConsumed | grantor, spender, metadata, store, to, transferred, charged, remaining | A spender spends a grant. |
AllowancesDisabledChanged | metadata, disabled | An issuer blocks or unblocks delegated spending of an asset. |
A transaction grants an allowance from the signing account when it emits GrantChanged with
grantor equal to the sender and remaining above zero. decrease and replace to a lower
amount also emit GrantChanged with remaining above zero; compare remaining with the
prior_values entry to tell an increase from a reduction.
In changes, the grant is a write_table_item whose data.key_type is
0x1::fungible_asset_allowance::GrantKey (spender, metadata) and whose data.value_type is
0x1::fungible_asset_allowance::Grant (remaining, expiry_secs, generation, scope), as in
the example under Table items. Its prior_values entry is the grant it replaces, or
null for a new one; a removed grant is a delete_table_item. The item is in the grants table of
0x1::fungible_asset_allowance::Allowances, on the object at
fungible_asset_allowance::allowances_object_address(grantor). A grantor’s first write creates that
object, so its Allowances appears in the same changes as a write_resource with the table’s
handle. Signed-grant nonces are items of the used_nonces table of the same resource.
Recognising a request to sign a grant
A signed grant is the BCS encoding of aptos_std::ed25519::SignedMessage<SignedGrant>. It begins
with the TypeInfo of the payload type, which is account address 0x1, module name
fungible_asset_allowance and struct name SignedGrant, each name as a length-prefixed byte
string. The SignedGrant fields follow in this order:
| Field | Type |
|---|---|
chain_id | u8 |
grantor | address |
spender | address |
metadata | address |
expected_remaining | u64 |
amount | u64 |
expiry_secs | u64 |
nonce | u64 |
generation | u64 |
scope | u8 |
The view 0x1::fungible_asset_allowance::grant_signing_message(grantor, spender, metadata, expected_remaining, amount, expiry_secs, nonce, generation, scope) returns the bytes to sign for
those values on the chain it is called on. A wallet can decode a signing request, call the view with
the decoded values, and compare the result with the bytes it was asked to sign.
Signed Messages That Pass Control lists the other messages of
this form.
Recognising a request to sign a settlement authorization
A settlement authorization is the BCS encoding of
aptos_std::ed25519::SignedMessage<UptoAuthorization>, with the same TypeInfo prefix as a signed
grant and struct name UptoAuthorization. It installs no grant. It lets facilitator, once, move
up to max_amount of the asset from payer’s primary store to pay_to’s primary store with
settle_upto, from valid_after until before valid_before, which is at most 3600 seconds after
valid_after. The fields follow in this order:
| Field | Type |
|---|---|
chain_id | u8 |
payer | address |
facilitator | address |
pay_to | address |
metadata | address |
max_amount | u64 |
valid_after | u64 |
valid_before | u64 |
nonce | vector<u8>, 32 bytes |
generation | u64 |
The view 0x1::fungible_asset_allowance::upto_signing_message(payer, facilitator, pay_to, metadata, max_amount, valid_after, valid_before, nonce, generation) returns the bytes to sign, for the same
comparison as grant_signing_message. A settlement emits UptoSettled { payer, facilitator, pay_to, metadata, nonce, max_amount, amount, received }.
Checking what an account has granted
0x1::fungible_asset_allowance::allowance(grantor, spender, metadata) returns [remaining, expiry_secs, scope] for a live grant and ["0", "0", 0] otherwise. The u64 values are JSON
strings and the u8 scope is a JSON number. 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>","0xa"]}'No view lists a grantor’s grants. The spender and asset pairs come from GrantChanged events whose
grantor is the account, which GET /rpc/v3/events/0x1::fungible_asset_allowance::GrantChanged
returns page by page: the x-supra-cursor response header of a page is passed back verbatim as
the start query parameter to read the next one, and a response without that header is the last.
limit is at most 100. Confirm each pair with the allowance view. The endpoint answers 410 for
a range the node has pruned. A grant recorded in an event is live only while its generation equals
the generation(grantor) view and its expiry_secs is in the future.
Revoking a grant
All take the grantor’s signer:
revoke(spender, metadata)removes one grant.revoke_all()voids every grant the grantor has made, for every spender and asset, and every signed grant the grantor has produced and not yet seen submitted.cancel_nonce(nonce)voids the one signed grant carryingnonce, before it is submitted.invalidate_nonces_below(new_floor)voids every signed grant carrying a lower nonce.
supra move tool run \
--function-id 0x1::fungible_asset_allowance::revoke_all \
--profile grantor --accept-defaultsFor wallet designers
- For every
GrantChangedwhosegrantoris the signing account and whoseremainingis above zero, show the spender, the asset (0xais SUPRA),remainingin the asset’s decimals,expiry_secsas a date, and the scope:0for the primary store only,1for every store the account owns.18446744073709551615is the largestu64. Where the decodedGranthas a prior value, show the change from it. - Treat a request to sign a
SignedGrantmessage as a grant. Show the decoded fields before asking for the signature, and state that anyone holding the signature can install the grant until itsexpiry_secs. - Treat a request to sign an
UptoAuthorizationmessage as a payment of up tomax_amounttopay_to. Show the recipient, the facilitator, the asset,max_amountin the asset’s decimals and the validity interval before asking for the signature. - Show the account’s live grants and offer
revokeandrevoke_all.
For account holders
A warning about an allowance means that, after this transaction, the spender the wallet names can
take up to the amount shown of that asset from your account, without asking you again, until the
date shown. That includes funds you receive later. Signing an allowance message has the same effect
as soon as someone submits it, and it does not need a transaction from you. You can remove one
allowance with revoke, or all of them, including allowance messages you have signed and nobody has
submitted yet, with revoke_all. An asset’s issuer can block all spending through allowances of
that asset; that does not remove your allowances. For SUPRA, only a governance proposal executed
with the framework signer can do it.
SUPRA Withdrawals, Transfers and Crossings
What it permits
A Move module holding the signer can take SUPRA from the account with coin::withdraw<SupraCoin> or
primary_fungible_store::withdraw with metadata 0xa, and deposit it anywhere. It can also call
evm_settlement::deposit, which moves the SUPRA into the settlement escrow and records a crossing
to a SupraEVM address; deposit_to_evm is the entry-function form. coin::withdraw<SupraCoin>
takes from the account’s CoinStore<SupraCoin> first and the remainder from its SUPRA primary
fungible store.
The value leaves the account in this transaction. Nothing continues afterwards. A crossing is credited on SupraEVM in a later block. A crossing from SupraMVM to SupraEVM has no refund path, and value credited to an address that no one can spend from is not recovered; see Cross-VM Transfers and Tracing Cross-VM Transfers. Refunds exist only for crossings from SupraEVM to SupraMVM.
Recognising a withdrawal in a simulation
| Event | Fields | Emitted when |
|---|---|---|
0x1::coin::CoinWithdraw | coin_type, account, amount | SUPRA is taken from account’s CoinStore<SupraCoin>, when feature MODULE_EVENT_MIGRATION (57) is on. |
0x1::coin::WithdrawEvent | amount | The same withdrawal, as a legacy event on the CoinStore’s handle; guid.account_address is the account. |
0x1::fungible_asset::Withdraw | store, amount | An amount is taken from the fungible store at store. |
0x1::coin::CoinDeposit, 0x1::coin::DepositEvent, 0x1::fungible_asset::Deposit | as above, for deposits | The value is deposited. |
0x1::evm_settlement::SettlementRecorded | id, record_address, source_domain, destination_domain, epoch, sender, recipient, amount | A crossing is recorded from sender to the SupraEVM address in recipient. source_domain is 1 (Move) and destination_domain is 2 (SupraEVM). amount is in quants. |
0x1::fungible_asset::Withdraw names the store, not its owner. The account’s SUPRA primary store is
at the address returned by 0x1::primary_fungible_store::primary_store_address:
curl --request POST --url '<RPC_HOST>/rpc/v3/view' \
--header 'Content-Type: application/json' \
--data '{"function":"0x1::primary_fungible_store::primary_store_address",
"type_arguments":["0x1::fungible_asset::Metadata"],
"arguments":["<ACCOUNT>","0xa"]}'A transaction takes SUPRA from the signing account when it emits CoinWithdraw with account
equal to the sender and coin_type 0x1::supra_coin::SupraCoin, or fungible_asset::Withdraw
whose store is the sender’s SUPRA primary store.
Gas is reported by 0x1::transaction_fee::FeeStatement. The gas is taken from the CoinStore first
with no event, and only the remainder, if any, is a fungible_asset::Withdraw from the sender’s
primary store immediately before FeeStatement; its amount is computed from the statement as
Gas describes. Exclude that Withdraw from the amounts the
transaction takes, so that the gas is not counted twice. A sender whose gas is covered by its
CoinStore shows no gas event.
These two rules do not match SUPRA taken from a non-primary fungible store that the sender owns:
the Withdraw names that store’s address. Match Withdraw.store against every store the sender
owns, or read changes.
In changes:
0x1::coin::CoinStore<0x1::supra_coin::SupraCoin>at the account, as awrite_resourcewhose prior value holds the balance before the withdrawal.- The account’s SUPRA primary store: one
write_resourceat the store’s address for each member of its0x1::object::ObjectGroup. The balance is in the0x1::fungible_asset::FungibleStoremember, or in0x1::fungible_asset::ConcurrentFungibleBalancefor a store that has one. - For a crossing, also a new
0x1::evm_settlement::SettlementRecordatrecord_address, with anullprior value, and the escrow’s fungible store atevm_settlement::escrow_store_address().
SupraEVM credits the crossing in a later block, with a NativeCrossingFromMove log whose
settlementId is the record_address above. The
0x53555002 precompile section
describes the log.
Checking afterwards
GET /rpc/v3/accounts/{address}/coin_transactions lists the account’s coin withdrawals and
deposits. GET /rpc/v3/accounts/{address}/fa_transactions lists those of a fungible store, taking
the store’s address. GET /rpc/v3/accounts/{address}/history lists every finalized transaction
that concerns the account once, including the withdrawals from and deposits to the stores it owns,
with the categories each matched, except automated transactions and automation registry records.
Nodes at v11.7.0 and earlier do not serve it. A crossing is followed as
Route 1 of the tracing
article describes. A committed transfer cannot be reversed.
For wallet designers
- Add up the SUPRA leaving the signing account across
CoinWithdrawand the primary store’sfungible_asset::Withdraw, and show the total with the deposits it went to. The balances inchangesagainstprior_valuesgive the same totals per store. - For each
SettlementRecordedwhosesenderis the account, showamount(8 decimals) and the SupraEVM address inrecipient.
For account holders
A warning about a SUPRA withdrawal means this transaction moves the amount shown out of your account. Unlike an allowance or an automation task, nothing continues after the transaction, and a committed transfer cannot be undone. A crossing sends the amount to the SupraEVM address shown.
Stake Pool Control
What it permits
The account holding a pool’s 0x1::stake::OwnerCapability changes the pool’s operator with
stake::set_operator(owner, new_operator) and its delegated voter with
stake::set_delegated_voter(owner, new_voter). Both are public entry functions, so a Move module
holding the owner’s signer can call them.
The operator is the only account that can call stake::rotate_consensus_key,
stake::update_network_and_fullnode_addresses, stake::join_validator_set and
stake::leave_validator_set for the pool. Adding, unlocking and withdrawing stake take the
OwnerCapability, not the operator’s signer.
The delegated voter is stored in the pool’s delegated_voter field. supra_governance in this
framework decides who votes from its own configured voter list (the get_voters_list view), not
from this field.
Both last until the owner sets another address.
stake::extract_owner_cap(owner) is a public fun that moves the OwnerCapability out of the
account and returns it to the calling module. Whoever then holds it can call set_operator_with_cap,
set_delegated_voter_with_cap, unlock_with_cap and withdraw_with_cap, which returns the
withdrawn stake to the caller. stake::deposit_owner_cap(owner, owner_cap) stores a capability
under an account, and stake::destroy_owner_cap(owner_cap) destroys it, which leaves the pool with
no owner capability.
Recognising it in a simulation
All in 0x1::stake:
| Event | Fields | Emitted when |
|---|---|---|
SetOperator | pool_address, old_operator, new_operator | The operator changes, when feature MODULE_EVENT_MIGRATION (57) is on. |
SetOperatorEvent | pool_address, old_operator, new_operator | The same change, as a legacy event on the pool’s set_operator_events handle. |
SetDelegatedVoter | pool_address, old_voter, new_voter | The delegated voter is set, by set_delegated_voter, set_delegated_voter_with_cap or a wrapper that calls them. |
ExtractOwnerCap | owner, pool_address | The pool’s OwnerCapability is moved out of the account at owner. |
DepositOwnerCap | owner, pool_address | The pool’s OwnerCapability is stored under owner, including when a pool is created. |
DestroyOwnerCap | pool_address | The pool’s OwnerCapability is destroyed. |
A transaction passes control of the signing account’s pool when it emits SetOperator or
SetDelegatedVoter for the pool, or ExtractOwnerCap with owner equal to the sender. Control
also passes through staking_contract::create_staking_contract, which takes SUPRA and stores the
new pool’s OwnerCapability in the contract, through staking_contract::switch_operator and
update_voter, and through vesting::update_operator and update_voter. The operator and voter
events of these calls carry the contract’s pool address, which is not tied to the sender’s address.
In changes:
0x1::stake::StakePoolat the pool address, as awrite_resource. Itsoperator_addressanddelegated_voterfields against theprior_valuesentry show the change.extract_owner_capis adelete_resourceof0x1::stake::OwnerCapabilityat the account, whoseprior_valuesentry names thepool_address. Where the calling module stores the capability appears as a write elsewhere inchanges.
Checking and resetting
The views 0x1::stake::get_operator(pool_address) and
0x1::stake::get_delegated_voter(pool_address) return the current values for a pool that exists,
which 0x1::stake::initialize_stake_owner creates at the calling account’s address. GET /rpc/v3/accounts/{address}/resources/0x1::stake::OwnerCapability returns the capability, with its
pool_address, while the account holds it.
The owner resets either value by calling the same function with the intended address.
SetOperator.old_operator and SetDelegatedVoter.old_voter record the values before a change. A
capability that has left the account comes back only if its holder deposits it again.
supra move tool run \
--function-id 0x1::stake::set_operator \
--args address:<OPERATOR> \
--profile owner --accept-defaultsFor wallet designers
- For an account that holds an
OwnerCapability, show everySetOperatorandSetDelegatedVoterwhosepool_addressis that pool, with the old and new address. - Show an
ExtractOwnerCapwhoseowneris the signing account, and state that the calling module receives control of the pool and its stake.
For account holders
A warning about a stake operator means this transaction changes which account runs your validator.
The new operator can rotate its consensus key, change its network addresses, and add it to or
remove it from the validator set. The operator cannot withdraw your stake. You can set the operator
back with stake::set_operator, and the voter with stake::set_delegated_voter.
A warning about the owner capability means this transaction hands the right to control your stake pool to a contract, including withdrawing its stake. You cannot take it back yourself.
Control of the Account
What it permits
These operations let another party act as the account, replace its key, or share control of it.
| Operation | Function | What it needs | What the other party can do |
|---|---|---|---|
| Signer-capability offer | account::offer_signer_capability(account, signer_capability_sig_bytes, account_scheme, account_public_key_bytes, recipient_address) | The account’s signer and a signature by the account’s key over SignerCapabilityOfferProofChallengeV2. | The recipient calls account::create_authorized_signer(recipient, offerer_address) in any later transaction and receives the account’s signer. |
| Rotation-capability offer | account::offer_rotation_capability(account, rotation_capability_sig_bytes, account_scheme, account_public_key_bytes, recipient_address) | The account’s signer and a signature by the account’s key over RotationCapabilityOfferProofChallengeV2. | The recipient calls account::rotate_authentication_key_with_rotation_capability and sets the account’s authentication key. |
| Authentication-key rotation | account::rotate_authentication_key, which needs signatures by the current and the new key; rotate_authentication_key_call, a private entry function that only a transaction’s own payload can call | As listed. | Whoever holds the new key controls the account. |
| Conversion to a multisig account | multisig_account::create_with_existing_account and create_with_existing_account_and_revoke_auth_key | No signer. Anyone can submit a signature by the account’s key over MultisigAccountCreationMessage or MultisigAccountCreationWithAuthKeyRevocationMessage. | The owners execute transactions as the account through the multisig account. The second function also sets the authentication key to zero and revokes any signer- or rotation-capability offer, so only the owners keep control. |
| Resource-account creation | account::create_resource_account(source, seed), a public fun, and resource_account::create_resource_account, create_resource_account_and_fund and create_resource_account_and_publish_package | The source account’s signer. | account::create_resource_account creates an account at an address derived from the source’s address and the seed, or takes over an unused account at that address, and returns its SignerCapability to the calling module. The resource_account functions store it under the source account instead, and set the new account’s key. |
A signer-capability offer and a rotation-capability offer each have one recipient at a time; a new
offer replaces the old one. They last until revoked or replaced. A key rotation lasts until the next
one. The framework has no function that removes a MultisigAccount from an account.
Recognising it in a simulation
| Event | Fields | Emitted when |
|---|---|---|
0x1::account::SignerCapabilityOfferUpdate | account, old_recipient, new_recipient | A signer-capability offer is set or revoked, or create_resource_account sets a resource account’s offer to itself. Recipients are Option<address>; none means no offer. |
0x1::account::RotationCapabilityOfferUpdate | account, old_recipient, new_recipient | A rotation-capability offer is set or revoked. |
0x1::account::KeyRotation | account, old_authentication_key, new_authentication_key | An authentication key is written, when feature MODULE_EVENT_MIGRATION (57) is on. The proof-based rotation functions emit it on every call; the other writes emit it only when the key changes. |
0x1::account::KeyRotationEvent | old_authentication_key, new_authentication_key | The same write, as a legacy event on the account’s key_rotation_events handle. |
0x1::multisig_account::CreateWithExistingAccount | multisig_account, owners, num_signatures_required, auth_key_revoked | An existing account becomes a multisig account. |
0x1::account::ResourceAccountCreation | source, resource_account | create_resource_account creates or takes over the account at resource_account and returns its SignerCapability to the caller. |
0x1::resource_account::SignerCapabilityStored | origin, resource_account | A resource account’s SignerCapability is stored in the Container under origin. |
0x1::resource_account::SignerCapabilityRetrieved | origin, resource_account | The capability is removed from that Container and returned to the resource account’s signer. |
A transaction passes control of the signing account when it emits SignerCapabilityOfferUpdate or
RotationCapabilityOfferUpdate with account equal to the sender and a new_recipient,
KeyRotation with account equal to the sender, or CreateWithExistingAccount with
multisig_account equal to the sender.
In changes:
0x1::account::Accountat the account, as awrite_resource. Against itsprior_valuesentry,authentication_key,signer_capability_offer.forandrotation_capability_offer.forshow the change.- A conversion writes
0x1::multisig_account::MultisigAccountat the account, with anullprior value; itsownersandnum_signatures_requiredfields name who controls it. - A resource account is a new
0x1::account::Accountat the derived address; theresource_accountfunctions also write0x1::resource_account::Containerat the source account.
Signed messages that pass control
The key rotation, the signer-capability offer, the rotation-capability offer and the multisig
conversion each verify a signature over the BCS encoding of aptos_std::ed25519::SignedMessage<T>,
as a signed allowance grant does. The encoding begins with the TypeInfo of T, account address
0x1, then the module and struct names, then the fields of T in order:
T | Fields |
|---|---|
0x1::account::RotationProofChallenge | sequence_number: u64, originator: address, current_auth_key: address, new_public_key: vector<u8>. rotate_authentication_key needs it signed by both the current and the new key. |
0x1::account::SignerCapabilityOfferProofChallengeV2 | sequence_number: u64, source_address: address, recipient_address: address |
0x1::account::RotationCapabilityOfferProofChallengeV2 | chain_id: u8, sequence_number: u64, source_address: address, recipient_address: address |
0x1::multisig_account::MultisigAccountCreationMessage | chain_id: u8, account_address: address, sequence_number: u64, owners: vector<address>, num_signatures_required: u64 |
0x1::multisig_account::MultisigAccountCreationWithAuthKeyRevocationMessage | chain_id: u8, account_address: address, sequence_number: u64, owners: vector<address>, num_signatures_required: u64 |
0x1::fungible_asset_allowance::SignedGrant | See Recognising a request to sign a grant. |
The five account and multisig_account messages each name the account’s current sequence number,
so such a signature can be applied only until the account’s sequence number next changes, except
that rotate_authentication_key_with_rotation_capability signs the delegate’s sequence number.
Checking and undoing
0x1::account::get_authentication_key(addr)returns the key.0x1::account::is_signer_capability_offered(addr)andget_signer_capability_offer_for(addr)report the signer-capability offer;is_rotation_capability_offered(addr)andget_rotation_capability_offer_for(addr)the rotation-capability offer.0x1::multisig_account::owners(addr)andnum_signatures_required(addr)report a multisig account.
The account removes an offer with account::revoke_signer_capability(recipient) or
revoke_any_signer_capability(), and revoke_rotation_capability(recipient) or
revoke_any_rotation_capability(). Each emits the matching …OfferUpdate with new_recipient
none. revoke_any_signer_capability() aborts with EOPTION_NOT_SET when no signer-capability
offer exists, so check is_signer_capability_offered first. Revoking an offer does not undo a key
rotation its recipient has already made. Owners and the
threshold of a multisig account change only through the multisig account’s own transactions.
supra move tool run \
--function-id 0x1::account::revoke_any_signer_capability \
--profile owner --accept-defaultsFor wallet designers
- Show every
SignerCapabilityOfferUpdateandRotationCapabilityOfferUpdatewhoseaccountis the signing account and whosenew_recipientis set, with the recipient, and everyKeyRotationof the signing account. - Treat a request to sign any message in the table above as the operation it authorises. Decode it, show the recipient or the owners and threshold, and state that anyone holding the signature can submit it.
- Show the account’s current offers from the views above and offer the revoke functions.
For account holders
A warning about a signer capability means that, once this transaction commits, the account the wallet names can act as your account in its own transactions, including moving everything in it, until you revoke the offer. A warning about a rotation capability means that account can replace your key and lock you out. A warning about a key rotation means your account will answer to a different key. Converting to a multisig account gives its owners control of your account; if the conversion also revokes your key, you keep control only if you are one of its owners.
Other Changes of Control That Emit Events
These functions change who controls an asset or a role, and emit an event from framework
aptosvm-v1.16_supra-v1.9.0.rc14. This list is not exhaustive: object::transfer and
object::transfer_to_object move any object the signer owns and emit 0x1::object::Transfer;
code::publish_package_txn and object_code_deployment::publish publish code under the signer or
an object it owns; vesting::set_beneficiary and staking_contract::set_beneficiary_for_operator
change a beneficiary.
| Event | Fields | Emitted by |
|---|---|---|
0x1::coin::CoinStoreFrozenUpdate | coin_type, account, old_frozen, new_frozen | coin::freeze_coin_store and unfreeze_coin_store, called by the holder of the coin’s FreezeCapability. A frozen CoinStore cannot send or receive the coin. |
0x1::coin::PairedBurnRefRemoval | coin_type, fungible_asset_metadata_address | coin::convert_and_take_paired_burn_ref, which moves the paired fungible asset’s BurnRef out of the pairing permanently. |
0x1::vesting::ResetBeneficiary, 0x1::vesting_without_staking::ResetBeneficiary | admin, vesting_contract_address, shareholder, old_beneficiary, reset_by | reset_beneficiary, when a beneficiary was set. |
0x1::vesting::SetManagementRole, 0x1::vesting_without_staking::SetManagementRole | admin, vesting_contract_address, role, old_role_holder, new_role_holder | set_management_role, and set_beneficiary_resetter through it. |
0x1::object::UngatedTransferUpdate | object, old_allow_ungated_transfer, new_allow_ungated_transfer | object::disable_ungated_transfer and enable_ungated_transfer, called with a TransferRef after the object’s creating transaction. |
0x1::object::ObjectDeletion | object, owner | object::delete. |
0x4::royalty::RoyaltyUpdate | object, old_numerator, old_denominator, old_payee_address, new_numerator, new_denominator, new_payee_address | royalty::update, and aptos_token::set_collection_royalties through it. The old_* fields are Options. |
SupraEVM Equivalents
The first three apply to contracts in SupraEVM’s genesis set, which Predeployed Contracts lists with their addresses. The last applies to every EVM account.
Wrapped SUPRA approvals
Wrapped SUPRA (WSUPRA) is built on
OpenZeppelin’s ERC20 and ERC20Permit. An approval is recorded by:
approve(spender, value), which records an allowance formsg.sender, so a contract calling it approves its own balance, not its caller’s;permit(owner, spender, value, deadline, v, r, s), which applies an EIP-712 signature overPermit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)and can be submitted by any account.
Both emit Approval(address indexed owner, address indexed spender, uint256 value), and
allowance(owner, spender) returns the current value. An approval has no expiry: deadline bounds
only when a permit signature can be submitted. An allowance of type(uint256).max is not reduced
by spending. approve(spender, 0) removes an approval.
The EVM automation registry
The registry is one of the system contracts.
register(...) records msg.sender as the task’s owner, and the node executes the task’s automated
transactions with the owner as sender. The registration fee and deposit are taken with
WSUPRA.transferFrom(msg.sender, registry, fee), so the owner must first approve the registry for
WSUPRA. Each cycle the registry charges the cycle fee the same way, against that approval; a task
whose owner’s WSUPRA balance or approval is below the fee is removed without refunding its deposit,
with TaskCancelledInsufficientBalanceAllowance.
Registration emits TaskRegistered(taskIndex, owner, registrationFee, lockedDepositFee, taskMetadata); a cycle charge emits TaskCycleFeeWithdraw. getTasksByAddress(owner) lists an
owner’s task indexes and getTaskDetails(taskIndex) returns a task. cancelTasks(taskIndexes) and
stopTasks(taskIndexes) correspond to the Move registry’s cancel_task and stop_tasks.
Crossings from SupraEVM
The crossing precompile at 0x0000000000000000000000000000000053555002 debits the value sent with
the call from the calling address and logs NativeCrossingToMove(bytes32 indexed settlementId, address indexed sender, bytes32 indexed recipient, uint256 amountQuants) at its own address. A
contract crosses value it holds, including value sent to it in the same transaction. eth_call,
eth_estimateGas and debug_traceCall run the call as transaction index 0, so the settlementId
in a simulated crossing log is not the id the crossing gets when included. The
0x53555002 precompile section
describes the call and the log. SupraMVM names the crossing by the same id in the id field of
0x1::evm_settlement::InboundSettlementApplied or InboundSettlementSenderRejected.
EIP-7702 delegations
SupraEVM accepts EIP-7702 set-code transactions (type 0x04; see
Transaction types). A set-code transaction carries a list
of authorizations. Each is a tuple (chain_id, address, nonce) signed by the key of an ordinary
account, over keccak256(0x05 || rlp([chain_id, address, nonce])). When the transaction executes,
each valid authorization sets the signing account’s code to the 23-byte delegation designator
0xef0100 || address.
From then on, a call to the account runs the code at address in the account’s own context: with
its SUPRA balance and storage, and as msg.sender of every call it makes, so it can transfer the
account’s SUPRA and tokens. What it does is whatever its code does, for any caller its code lets
through. The account’s key still signs ordinary transactions.
- Who submits it. Any account can submit the set-code transaction; the authorization’s signer need not be its sender. Signing an authorization is enough to delegate.
- Which chains. An authorization whose
chain_idis535600is valid only on Devnet. One whosechain_idis0is valid on every chain, so a signature given for another network also delegates the same address here. - Which nonce. It applies only while
nonceequals the account’s nonce at that point of the transaction, and applying it increments the nonce. When the signing account also sends the set-code transaction, its nonce has already been incremented for the transaction, so the authorization must carry the current nonce plus one. - How long. The delegation stays until another authorization replaces it. An authorization for the zero address clears it and leaves the account without code.
To check an account, eth_getCode(address) returns 0x for no delegation and
0xef0100 followed by the delegate’s 20-byte address for one. A wallet asked to sign an
authorization should show the delegate’s address and the chain_id, and treat 0 as a delegation
on every chain. An account holder who signed one they do not recognise can revoke it by signing an
authorization for the zero address and having it included.