Security Best Practices
To build securely on Sui, audit and govern your packages, enforce access control with capabilities, protect coin capabilities and keys, use onchain randomness safely, validate offchain data, and apply the specific practices for Seal and Nautilus.
The following security best practices apply when you develop applications on Sui. These recommendations are not comprehensive for all application types, use cases, or architectures. Do your own research and get thorough audits for production applications.
What are the main smart contract security risks on Sui?
The following practices address risks in your packages, their upgrades, their dependencies, and their logic.
Audit your contracts
Ensure transactions operate on smart contracts you trust and have audited. Sui packages are immutable once you publish them. 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.
How do I use onchain randomness safely?
The following practices prevent callers from influencing or selectively rejecting random outcomes.
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 (PTB). 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 transaction can call a private entry function directly, but another Move module cannot, 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.
How do I use Seal securely?
Seal provides encryption and access control for your data. You express access rules in Move code in seal_approve* functions, and you rely on key servers that you choose. The following practices cover choosing key servers, structuring encryption, and keeping your access policy correct.
Choose trusted key servers
Choose trusted key server operators and diversify across organizations, jurisdictions, and infrastructure providers.
Use threshold encryption
Use threshold encryption (for example, 2-of-3) to tolerate compromised servers while maintaining availability.
Use envelope encryption
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.
How do I use Nautilus securely?
Nautilus runs code in an enclave, a trusted execution environment (TEE), and the enclave signs the output it produces. The practices below cover keeping enclave code small, verifying that an enclave runs the image you expect, and handling its signed payloads safely.
Minimize enclave code
Less code means a smaller attack surface.
Audit dependencies
Review all libraries that the enclave uses.
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 an attacker cannot replay a signature for one purpose in 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.
How do I protect capabilities and coins in Move?
The following practices cover the capabilities that control a coin and its supply model.
Protect TreasuryCap
The TreasuryCap controls minting and burning permissions. Hold it in a multisig address or a dedicated custody setup rather than a single hot wallet, as you do for admin capabilities.
Protect MetadataCap
The MetadataCap controls coin branding. Hold it in a multisig address or a dedicated custody setup rather than a single hot wallet, as you do for admin capabilities.
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.
How do I enforce access control in Move?
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 app.
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 the Admin capability section of the Move best practices guide describes.
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 an attacker compromises a capability holder, you need a way to issue a new capability and stop honoring the old one.