Skip to Content
Supra Layer 1SupraMVMRecognising Account Operations

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.

OperationWhat another party can do afterwardsFor how long
Registering an automation taskThe 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 allowanceThe 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 SupraEVMNothing 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 capabilityThe 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 itselfThe 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 surfaceVersionAlso requires
fungible_asset_allowance: grants, signed grants, revocationNot 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_disabledFramework aptosvm-v1.16_supra-v1.9.0.rc14.Nothing further.
automation_registry::register_without_validation: task registration from a Move moduleNew 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 transactionBefore v1.9.0.Feature SUPRA_NATIVE_AUTOMATION (88).
evm_settlement::deposit, evm_settlement::deposit_to_evm: crossings to SupraEVMevm_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 belowBefore v1.9.0.Nothing further.
The events SetDelegatedVoter, ExtractOwnerCap, DepositOwnerCap, DestroyOwnerCap, RotationCapabilityOfferUpdate, SignerCapabilityOfferUpdate, ResourceAccountCreation, SignerCapabilityStored, SignerCapabilityRetrieved, CreateWithExistingAccount, and KeyRotation from every authentication-key writeFramework 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 responseRPC 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/simulate

The 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 }
FieldContent
payloadThe 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.eventsEvery 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, statusThe result. status is Success when execution succeeded.
state_heightThe height of the last block the node had executed when it took the snapshot the simulation ran against, or null for the genesis state.
changesEvery 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_valuesFor 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_truncatedThe 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::ObjectGroup on an object, is one state key. A written group is reported as one write_resource per member, each carrying the group’s state_key_hash, in both changes and prior_values; comparing the two shows which member changed. A deleted group is one delete_resource whose resource is the group’s tag, and its prior_values entry lists the members.
  • Size limit. changes and prior_values together 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 in omitted as {state_key_hash, address, kind, tag}, up to a number of entries the node sets, and omitted_counts counts every left-out key by kind. changes_truncated is true exactly when a key was left out. The bounds configured on Devnet’s node are listed in the description of the Simulate a transaction (v4) operation on the Devnet API page, https://rpc-devnet.supra.com/docs .
  • One snapshot. The simulation, prior_values and state_height are read from one snapshot of the node’s database. The node keeps Move state for the latest block only, so prior_values cannot 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 a TaskCancelledCapacitySurpassedV2 event;
  • if the owner’s balance is below the fee, the task is removed and its deposit is not refunded, with a TaskCancelledInsufficentBalanceV2 event;
  • otherwise the fee is withdrawn, with a TaskEpochFeeWithdraw event.

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_payload transaction, which the owner signs for that purpose. The simulate response’s payload.Move.type is automation_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), a public fun that any Move module holding the owner’s signer can call. It does not check that payload_tx decodes to a callable entry function. The runtime removes a task whose payload it cannot build a transaction from, refunds the owner as stop_tasks does, and emits TaskCancelledByRuntime with a diagnostic_message.

Recognising it in a simulation

All in 0x1::automation_registry:

EventFieldsEmitted when
AutomationTaskMetaDatatask_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_epochA task is registered. state is 0.
TaskRegistrationDepositFeeWithdrawtask_index, owner, registration_fee, locked_deposit_feeThe 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_item in the table behind the tasks map of 0x1::automation_registry::AutomationRegistryV2. Its data.key_type is u64, the key is the task_index, and its data.value_type is 0x1::enumerable_map::Tuple<0x1::automation_registry::AutomationTaskMetaData>, whose value field is the task. Its prior_values entry is null, since the index is new.
  • AutomationRegistryV2 and AutomationRefundBookkeeping are members of the 0x1::object::ObjectGroup at 0x1, which is reported as a write_resource per 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:0

GET /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-defaults

Cancelling 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 (state 2); a cancelled task keeps running until the end of the current cycle, and its deposit is refunded when the cycle ends. Emits TaskCancelledV2.
  • 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. Emits TasksStoppedV2.
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 AutomationTaskMetaData whose owner is the signing account, decode payload_tx as a BCS EntryFunction and show its module, function and arguments. Show expiry_time as a date, and max_gas_amount, gas_price_cap, automation_fee_cap_for_epoch and the TaskRegistrationDepositFeeWithdraw amounts 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_payload was made by a module the transaction called.
  • List the account’s tasks with the views above and offer cancel_task and stop_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:

EventFieldsEmitted when
GrantChangedgrantor, spender, metadata, remaining, expiry_secs, generation, scopeA grant is written. remaining above zero creates or changes it; remaining of zero removes it.
NonceConsumedgrantor, nonce, installed_grantA signed grant is applied (installed_grant is true), or the grantor cancels a nonce (false).
AllGrantsRevokedgrantor, generationThe grantor calls revoke_all.
GrantConsumedgrantor, spender, metadata, store, to, transferred, charged, remainingA spender spends a grant.
AllowancesDisabledChangedmetadata, disabledAn 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:

FieldType
chain_idu8
grantoraddress
spenderaddress
metadataaddress
expected_remainingu64
amountu64
expiry_secsu64
nonceu64
generationu64
scopeu8

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:

FieldType
chain_idu8
payeraddress
facilitatoraddress
pay_toaddress
metadataaddress
max_amountu64
valid_afteru64
valid_beforeu64
noncevector<u8>, 32 bytes
generationu64

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 carrying nonce, 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-defaults

For wallet designers

  • For every GrantChanged whose grantor is the signing account and whose remaining is above zero, show the spender, the asset (0xa is SUPRA), remaining in the asset’s decimals, expiry_secs as a date, and the scope: 0 for the primary store only, 1 for every store the account owns. 18446744073709551615 is the largest u64. Where the decoded Grant has a prior value, show the change from it.
  • Treat a request to sign a SignedGrant message as a grant. Show the decoded fields before asking for the signature, and state that anyone holding the signature can install the grant until its expiry_secs.
  • Treat a request to sign an UptoAuthorization message as a payment of up to max_amount to pay_to. Show the recipient, the facilitator, the asset, max_amount in the asset’s decimals and the validity interval before asking for the signature.
  • Show the account’s live grants and offer revoke and revoke_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

EventFieldsEmitted when
0x1::coin::CoinWithdrawcoin_type, account, amountSUPRA is taken from account’s CoinStore<SupraCoin>, when feature MODULE_EVENT_MIGRATION (57) is on.
0x1::coin::WithdrawEventamountThe same withdrawal, as a legacy event on the CoinStore’s handle; guid.account_address is the account.
0x1::fungible_asset::Withdrawstore, amountAn amount is taken from the fungible store at store.
0x1::coin::CoinDeposit, 0x1::coin::DepositEvent, 0x1::fungible_asset::Depositas above, for depositsThe value is deposited.
0x1::evm_settlement::SettlementRecordedid, record_address, source_domain, destination_domain, epoch, sender, recipient, amountA 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 a write_resource whose prior value holds the balance before the withdrawal.
  • The account’s SUPRA primary store: one write_resource at the store’s address for each member of its 0x1::object::ObjectGroup. The balance is in the 0x1::fungible_asset::FungibleStore member, or in 0x1::fungible_asset::ConcurrentFungibleBalance for a store that has one.
  • For a crossing, also a new 0x1::evm_settlement::SettlementRecord at record_address, with a null prior value, and the escrow’s fungible store at evm_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 CoinWithdraw and the primary store’s fungible_asset::Withdraw, and show the total with the deposits it went to. The balances in changes against prior_values give the same totals per store.
  • For each SettlementRecorded whose sender is the account, show amount (8 decimals) and the SupraEVM address in recipient.

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:

EventFieldsEmitted when
SetOperatorpool_address, old_operator, new_operatorThe operator changes, when feature MODULE_EVENT_MIGRATION (57) is on.
SetOperatorEventpool_address, old_operator, new_operatorThe same change, as a legacy event on the pool’s set_operator_events handle.
SetDelegatedVoterpool_address, old_voter, new_voterThe delegated voter is set, by set_delegated_voter, set_delegated_voter_with_cap or a wrapper that calls them.
ExtractOwnerCapowner, pool_addressThe pool’s OwnerCapability is moved out of the account at owner.
DepositOwnerCapowner, pool_addressThe pool’s OwnerCapability is stored under owner, including when a pool is created.
DestroyOwnerCappool_addressThe 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::StakePool at the pool address, as a write_resource. Its operator_address and delegated_voter fields against the prior_values entry show the change.
  • extract_owner_cap is a delete_resource of 0x1::stake::OwnerCapability at the account, whose prior_values entry names the pool_address. Where the calling module stores the capability appears as a write elsewhere in changes.

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-defaults

For wallet designers

  • For an account that holds an OwnerCapability, show every SetOperator and SetDelegatedVoter whose pool_address is that pool, with the old and new address.
  • Show an ExtractOwnerCap whose owner is 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.

OperationFunctionWhat it needsWhat the other party can do
Signer-capability offeraccount::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 offeraccount::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 rotationaccount::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 callAs listed.Whoever holds the new key controls the account.
Conversion to a multisig accountmultisig_account::create_with_existing_account and create_with_existing_account_and_revoke_auth_keyNo 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 creationaccount::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_packageThe 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

EventFieldsEmitted when
0x1::account::SignerCapabilityOfferUpdateaccount, old_recipient, new_recipientA 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::RotationCapabilityOfferUpdateaccount, old_recipient, new_recipientA rotation-capability offer is set or revoked.
0x1::account::KeyRotationaccount, old_authentication_key, new_authentication_keyAn 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::KeyRotationEventold_authentication_key, new_authentication_keyThe same write, as a legacy event on the account’s key_rotation_events handle.
0x1::multisig_account::CreateWithExistingAccountmultisig_account, owners, num_signatures_required, auth_key_revokedAn existing account becomes a multisig account.
0x1::account::ResourceAccountCreationsource, resource_accountcreate_resource_account creates or takes over the account at resource_account and returns its SignerCapability to the caller.
0x1::resource_account::SignerCapabilityStoredorigin, resource_accountA resource account’s SignerCapability is stored in the Container under origin.
0x1::resource_account::SignerCapabilityRetrievedorigin, resource_accountThe 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::Account at the account, as a write_resource. Against its prior_values entry, authentication_key, signer_capability_offer.for and rotation_capability_offer.for show the change.
  • A conversion writes 0x1::multisig_account::MultisigAccount at the account, with a null prior value; its owners and num_signatures_required fields name who controls it.
  • A resource account is a new 0x1::account::Account at the derived address; the resource_account functions also write 0x1::resource_account::Container at 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:

TFields
0x1::account::RotationProofChallengesequence_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::SignerCapabilityOfferProofChallengeV2sequence_number: u64, source_address: address, recipient_address: address
0x1::account::RotationCapabilityOfferProofChallengeV2chain_id: u8, sequence_number: u64, source_address: address, recipient_address: address
0x1::multisig_account::MultisigAccountCreationMessagechain_id: u8, account_address: address, sequence_number: u64, owners: vector<address>, num_signatures_required: u64
0x1::multisig_account::MultisigAccountCreationWithAuthKeyRevocationMessagechain_id: u8, account_address: address, sequence_number: u64, owners: vector<address>, num_signatures_required: u64
0x1::fungible_asset_allowance::SignedGrantSee 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) and get_signer_capability_offer_for(addr) report the signer-capability offer; is_rotation_capability_offered(addr) and get_rotation_capability_offer_for(addr) the rotation-capability offer.
  • 0x1::multisig_account::owners(addr) and num_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-defaults

For wallet designers

  • Show every SignerCapabilityOfferUpdate and RotationCapabilityOfferUpdate whose account is the signing account and whose new_recipient is set, with the recipient, and every KeyRotation of 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.

EventFieldsEmitted by
0x1::coin::CoinStoreFrozenUpdatecoin_type, account, old_frozen, new_frozencoin::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::PairedBurnRefRemovalcoin_type, fungible_asset_metadata_addresscoin::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::ResetBeneficiaryadmin, vesting_contract_address, shareholder, old_beneficiary, reset_byreset_beneficiary, when a beneficiary was set.
0x1::vesting::SetManagementRole, 0x1::vesting_without_staking::SetManagementRoleadmin, vesting_contract_address, role, old_role_holder, new_role_holderset_management_role, and set_beneficiary_resetter through it.
0x1::object::UngatedTransferUpdateobject, old_allow_ungated_transfer, new_allow_ungated_transferobject::disable_ungated_transfer and enable_ungated_transfer, called with a TransferRef after the object’s creating transaction.
0x1::object::ObjectDeletionobject, ownerobject::delete.
0x4::royalty::RoyaltyUpdateobject, old_numerator, old_denominator, old_payee_address, new_numerator, new_denominator, new_payee_addressroyalty::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 for msg.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 over Permit(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_id is 535600 is valid only on Devnet. One whose chain_id is 0 is valid on every chain, so a signature given for another network also delegates the same address here.
  • Which nonce. It applies only while nonce equals 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.

Last updated on