Security Best Practices
This page describes a few recommended security best practices when developing applications on Sui. These recommendations are not comprehensive for all application types, use cases, or architectures. Do your own research and have production applications audited thoroughly.
Smart contract security
Audit your contracts
Ensure transactions operate on smart contracts you trust and have audited. Sui packages are immutable once published. If a package is upgradeable, the UpgradeCap controls publication of newer versions, so audit both the current package and its upgrade policy.
Govern package upgrades
An upgradeable package can publish a new onchain version at any time, and the new version controls all future behavior. Treat the UpgradeCap as a security-critical capability:
-
Protect the
UpgradeCapwith the same care as an admin capability. Whoever holds it can change your package, as far as its upgrade policy allows. -
Apply a custom upgrade policy, a multisig, or a timelock so that no single key can ship an upgrade silently.
-
Call
make_immutableon theUpgradeCapwhen you no longer need upgrades. This is irreversible: it permanently freezes the package, and you can never upgrade it again, so be certain before you do it.
Verify packages and dependencies
Before you trust a package, verify its exact onchain package ID, the IDs of its dependencies, and its upgrade policy. Do not rely on package names or frontend constants alone, because those can point anywhere. A dependency that is still upgradeable can change after your audit.
Rely on Move's safety as a foundation, not a guarantee
Move's type system, resource model, bytecode verifier, and checked arithmetic reduce entire classes of bugs. They do not replace business-logic review, invariant testing, access control checks, or economic analysis. Treat the language guarantees as a foundation, and still review the logic that sits on top of them.
Validate invariants and object relationships
Many Sui bugs use a valid object in the wrong relationship. Inside every privileged function, validate amounts, object IDs, dynamic field keys, ownership expectations, and the relationships between objects, such as a capability matching the object it governs. Check zero and overflow edge cases even though Move arithmetic is checked, because an unexpected abort can still be a denial of service.
Emit events for privileged actions
Emit events for admin changes, allowlist updates, mint and burn operations, denylist actions, configuration changes, oracle updates, and emergency pauses. Events make privileged activity auditable and let offchain monitoring detect unexpected behavior quickly.
Onchain randomness
Limit what follows a random call in a PTB
Sui restricts which commands can follow a MoveCall that consumes Random in the same programmable transaction block. Do not depend on post-random commands other than the allowed object transfers and coin merges. This prevents test-and-abort attacks, where an attacker inspects the result and aborts the transaction if the outcome is unfavorable.
Mark randomness-consuming functions as private entry
Declare functions that consume randomness as private entry functions. A private entry function can be called directly from a transaction but not from another Move module, which prevents other modules from composing your randomness-consuming function into a larger call that enables a test-and-abort attack. This is not optional: the Move compiler enforces it by rejecting public functions that take Random or RandomGenerator as an argument.
Never accept RandomGenerator as a parameter
Always create the generator internally to prevent manipulation.
Balance gas across outcome paths
Design functions so that the outcome favorable to the transaction sender consumes the same amount of gas as, or more gas than, an unfavorable outcome. Otherwise an attacker can set a gas budget that has enough gas for the favorable outcome but not the unfavorable one, aborting the transaction whenever the result is not in their favor.
Consider a two-transaction commit-reveal pattern
For high-stakes applications, consider a two-transaction commit-reveal pattern to further reduce the influence a caller has over a random outcome.
Seal (Encryption and Access Control)
Choose trusted key server operators and diversify across organizations, jurisdictions, and infrastructure providers.
Use threshold encryption (for example, 2-of-3) to tolerate compromised servers while maintaining availability.
Use envelope encryption for sensitive or large data: encrypt data with your own symmetric key, then use Seal to manage access to that key only.
Ensure correct access policy
The Move code in seal_approve* functions must accurately express your intended access rules. If the package containing seal_approve* remains upgradeable, whoever controls its upgrade path can change future access policy. Govern that upgrade path like a security-critical capability.
Nautilus
Minimize enclave code
Less code means a smaller attack surface.
Audit dependencies
Review all libraries used in the enclave.
Use short timestamps
Short timestamps reduce the replay attack window.
Verify and enforce expected PCRs
Before you trust enclave output, verify that the platform configuration registers (PCRs) match the values you expect for your enclave image. Reject output from an enclave with unexpected PCRs, and alert on any drift.
Protect signed enclave payloads
Apply signed payload hygiene to every message the enclave signs. Use domain or intent separation so a signature for one purpose cannot be replayed for another. Check timestamp freshness, and include a nonce or request ID wherever replay matters.
Don't rely solely on TEE guarantees
A trusted execution environment reduces risk but does not remove it. Combine it with the other practices on this page.
Coin and capability security
Protect TreasuryCap
The TreasuryCap controls minting and burning permissions.
Protect MetadataCap
The MetadataCap controls coin branding.
Protect DenyCapV2
The DenyCapV2 can restrict coin usage, which is a powerful compliance and centralization control. Whoever holds it can block addresses from transacting with the coin. Govern it transparently, and document who can use it.
Set supply model early
Set the supply model early, and understand the implications of a fixed, burn-only, or flexible supply.
Access control
Access control determines who can call privileged functions in your package, such as minting, withdrawals, or configuration changes. Move gives you several mechanisms to enforce authorization. Choose them deliberately, because a missing or weak check can expose privileged operations to any caller.
For implementation patterns and code examples, see the Access control section of the Move best practices guide and the Capability Pattern example.
Prefer capability objects over address allowlists
A capability object is a struct with the key ability that acts as a permission token. Holding the object proves authorization, so a function can gate access by requiring a reference to the capability. This approach composes well, because another protocol can hold the capability on behalf of a user, and you avoid maintaining an onchain list of addresses. Use capability objects such as AdminCap or MintCap as your default access control mechanism.
Manage address allowlists deliberately
Some designs still need an address allowlist, for example a set of accounts that can claim an airdrop. When you use one, store it in a well-defined object, restrict updates to a privileged function, and emit an event whenever an entry changes. Consider adding a time-lock on updates so that membership changes take effect only after a delay, giving observers time to react to an unexpected change. Events give offchain services and auditors a record of every membership change. Without them, allowlist changes are difficult to monitor.
Do not rely on tx_context::sender() alone
tx_context::sender() returns the address that signed the transaction. Using it as the only authorization check ties a function to a single signer and breaks composability, because another contract cannot call the function on behalf of a user. Prefer capability objects or explicit object ownership checks when composability matters. Reserve tx_context::sender() for cases where the signer is genuinely the only valid actor.
Require explicit capabilities for privileged functions
Make every privileged function take the relevant capability as a parameter, such as AdminCap for configuration, TreasuryCap for minting, or DenyCapV2 for usage restrictions. Move's type system enforces this at transaction construction time, because a caller cannot produce a &AdminCap reference without owning the object. Place the capability as the second parameter to keep method associativity, as described in the Admin capability section of the Move best practices guide.
Protect and rotate admin capabilities
Treat admin capabilities as high-value assets:
-
Do not transfer admin capabilities casually. Each transfer grants full privileges to the recipient.
-
Hold admin capabilities in a multisig address or a dedicated custody setup rather than a single hot wallet.
-
Plan a rotation path before you publish. If a capability holder is compromised, you need a way to issue a new capability and stop honoring the old one.
Include a revocation path
Design a way to revoke a compromised or outdated capability before you publish the package. Common options include:
-
An onchain registry that records valid capability IDs, where privileged functions check the registry and reject revoked IDs.
-
A version or epoch field that privileged functions check, which you can advance to invalidate older capabilities.
-
A function that destroys a capability, which only helps when the holder still controls the capability and cooperates.
For a capability that is lost or held in someone else's custody after a compromise, you cannot rely on destroying it. A registry check or version check is the only revocation mechanism that works without the holder's cooperation. Without one, a leaked capability remains valid for the life of the package.
Validate capability and object relationships
When a capability governs a specific object, store the object's ID in the capability (or the capability's ID in the object) and check that they match inside the function. For example, a capability that manages one vault should only operate on that vault. Without this check, a caller could pass a valid capability for a different object and act outside your intended scope.
Enforce authorization on shared object functions
Anyone can submit a transaction that references a shared object. A public function that takes a shared object runs for any caller unless the function itself checks authorization. Never assume access to a shared object is restricted. Require a capability, validate the sender, or check object ownership inside every privileged function that touches shared state.
Oracle and offchain data
Validate offchain data before you use it
If your contracts consume offchain data, such as price feeds, validate it before you act on it. Check the freshness of the data, confirm the source, and verify any signatures. Apply replay protection so an old data point cannot be submitted again. Define explicit failure behavior for when the data is missing, stale, or invalid, so a feed outage does not produce an unsafe result.
Emergency controls
Consider pause and rate-limit mechanisms
For high-value applications, consider a pause, kill-switch, or rate-limit mechanism that can contain an active incident. Document who can trigger each control and how you prevent abuse, because an emergency control is itself a privileged operation. Gate it with a capability, and emit an event whenever someone uses it.
Frontend and transaction signing
Show human-readable transaction intent
Show users a human-readable description of what a transaction does before they sign it. Avoid blind signing, where the user approves opaque bytes. Dry-run or simulate the transaction when possible, so the user and your app can see the expected effects first.
Verify package IDs in the client
Verify the exact package IDs your frontend calls. Do not trust a package only because of its name or a constant compiled into an older build. Pin the IDs you audited, and check them at runtime.
Domain-separate offchain signed messages
When your app signs messages offchain, such as personal messages, include a domain separator and an intent so a signature created for one context cannot be replayed in another. See Intent Signing for how Sui separates signing domains.
User and operator key management
Keep your private signature key private
No other party can operate on your owned assets without your private signature key.
Verify recipient addresses carefully
Sending objects to an incorrect address might have irrevocable effects.
Use multisig or hardware custody for privileged keys
Hold admin keys and capabilities in a multisig address, a hardware wallet, or a dedicated custody setup. Avoid keeping privileged keys in a single hot wallet.
Separate keys by role and network
Use separate keys for the deployer, the admin, and routine operations, so a compromise of one role does not grant the others. Keep Testnet and Mainnet keys separate, so a test key can never sign a production transaction.
Document recovery and rotation procedures
Write down how to recover and rotate every privileged key before you launch. A documented procedure is what lets you respond quickly when a key is lost or compromised.
General network security
Understand the validator trust assumption
Sui maintains its security properties as long as more than 2/3 of validators by stake operate correctly. The network uses Byzantine fault tolerant broadcast and consensus, so it tolerates a minority of faulty or malicious validators.
Treat all onchain activity as public
Every transaction and object on Sui is public and auditable, so anyone can inspect the activity of an address. To protect user privacy, consider using multiple addresses to separate unrelated activity.