Exchange Resolutions Complete, Ecosystem Growing, and a Commitment To Korea. Read more.

Developers
April 6, 2026

Cadence: Language-Level Security

By
Flow
Team
and
Cadence: Language-Level Security

Most smart contract vulnerabilities exist because the language allows them. Reentrancy, unchecked external calls, MEV attacks: these are not edge cases. They are predictable consequences of languages that treat digital assets as numbers in a ledger. Cadence smart contract security takes a fundamentally different approach. Rather than asking developers to write defensive code around a permissive language, Cadence eliminates entire vulnerability classes at the compiler level. The result is a programming model where the most common exploits in blockchain history simply cannot compile.

This post breaks down the specific language-level mechanisms that make Cadence secure by default, with real code examples you can deploy on Flow today.

The Compiler Rejects Resource Duplication. Assets Cannot Exist in Two Places

Flow utilizes a resource-oriented programming model via its native language, Cadence. Unlike many blockchains where tokens are simple entries in a ledger (a map of balances), tokens on Flow are programmable objects that exist directly in a user's account storage. Resources effectively mimic physical assets in that they cannot be copied or implicitly discarded, only moved or explicitly destroyed. Resource-oriented programming is also the basis of the Move language. (Cadence and Move are both derived from the same academic research: https://dl.acm.org/doi/10.1145/3417516)

  • Resources cannot be copied. The compiler rejects any attempt to duplicate a resource.
  • Resources cannot be implicitly discarded. Every resource must be explicitly moved or destroyed.
  • Resources cannot be accessed after being moved. Once transferred, the original reference is invalidated.

Here is what a resource looks like in Cadence 1.0+:

access(all) contract GameItems {


    access(all) resource Sword {
        access(all) let damage: Int
        access(all) let id: UInt64


        init(damage: Int) {
            self.id = self.uuid
            self.damage = damage
        }
    }


    access(all) fun createSword(damage: Int): @Sword {
        return <- create Sword(damage: damage)
    }
  }

Notice the move operator (<-). In Cadence, resources are never assigned with =. They are explicitly moved. This is not a convention or a best practice. It is enforced by the compiler. If you try to copy a resource, your contract will not deploy.

Compare this to Solidity, where a token balance is an integer in a mapping. Nothing in the language prevents a developer from accidentally incrementing that integer without a corresponding decrement elsewhere. In Cadence, the asset is the object. Transferring it to another account means the sender no longer has it. There is no second copy to reconcile.

This single design choice prevents an entire category of bugs that have cost the industry billions: accidental minting, double-spending, and orphaned assets that exist in a contract but belong to nobody.

No Reentrancy: Eliminated by Design, Not by Convention

Reentrancy was the root cause of the DAO hack on Ethereum, where $60 million was drained through a recursive external call. Since then, Solidity developers have relied on patterns like checks-effects-interactions and reentrancy guards (mutex locks) to prevent the same class of attack. These patterns work when applied correctly. The problem is that they must be applied manually to every vulnerable function, and a single omission is enough.

access(Withdraw) fun withdraw(amount: UFix64):

@Vault {
    self.balance = self.balance - amount
    // Balance is reduced before the returned @Vault
    // is handed to any downstream code.
    // No re-entrant call can observe the pre-reduction state.
    return <- create Vault(balance: amount)
}

Note: The withdraw function inside ExampleVault illustrates why: the balance reduction is the first operation, completed before the returned resource exists in any caller's scope.

In the Cadence VM environment, the attack pattern cannot execute as it does in Solidity. When a resource is moved in a transaction, the caller's reference is immediately invalidated — the compiler enforces this. There is no balance integer the re-entrant call can read again, because the asset is no longer in the caller's scope. The reduction happens before the returned resource is handed to any downstream code. There is no window in which a re-entrant call could observe the original balance.

Capability-Based Security and Entitlements

Access control in Solidity is typically address-based: require(msg.sender == owner). This works, but it is coarse. You either have access or you do not. Cadence replaces this with a fine-grained system of entitlements and capabilities that control exactly what operations a caller can perform on a resource.

Consider a token vault. Anyone should be able to deposit tokens into it. But only the owner should be able to withdraw. In Cadence 1.0+, this is expressed directly in the type system:

access(all) contract ExampleVault {


    access(all) entitlement Withdraw


    access(all) resource Vault {
        access(all) var balance: UFix64


        init(balance: UFix64) {
            self.balance = balance
        }


        access(Withdraw) fun withdraw(amount: UFix64): @Vault {
            self.balance = self.balance - amount
            return <- create Vault(balance: amount)
        }


        access(all) fun deposit(from: @Vault) {
            self.balance = self.balance + from.balance
            Burner.burn(<-from)
        }
    }
}

The deposit function is access(all), meaning anyone with a reference to the vault can call it. The withdraw function is gated by the Withdraw entitlement, meaning callers need an authorized reference (auth(Withdraw) &Vault) to invoke it. This is not enforced at runtime by an if statement. It is enforced at compile time by the type checker.

Capabilities extend this model to cross-account interactions. When an account wants to expose its vault for deposits, it publishes a capability that only includes the deposit interface:

let cap = signer.capabilities.storage.issue<&{FungibleToken.Receiver}>(
    /storage/flowTokenVault
)
signer.capabilities.publish(cap, at: /public/flowTokenReceiver)

External accounts can borrow this capability and deposit tokens, but they never gain access to withdrawal functions. The security boundary is defined in the type signature, not in runtime checks that a developer might forget.

For a deeper comparison between the two access control models, see Cadence vs Solidity on the Flow engineering blog.

Strong Static Typing: Catching Errors Before Deployment

Cadence uses a strong static type system that catches invalid programs at compile time rather than at runtime. Type inference reduces boilerplate without sacrificing safety. Every variable, function parameter, and return value has a known type, and the compiler verifies consistency across the entire contract.

This matters for security because many smart contract exploits depend on type confusion or unexpected runtime behavior. When the compiler guarantees that a function accepting @{FungibleToken.Vault} will never receive a different resource type, an entire surface area of attacks disappears.

Since the Forte upgrade in 2025, the Cadence compiler also produces AI-friendly error messages with explanations, suggested fixes, and links to documentation. This lowers the barrier for developers writing secure code and makes it harder to ship a contract with subtle type errors that could become vulnerabilities in production.

Upgradeable Contracts with Safety Guarantees

On Ethereum, upgradeability is an opt-in pattern that introduces its own attack surface: proxy contracts with implementation slot collisions, uninitialized logic contracts, and delegatecall pitfalls. Developers who want upgradeability must add complexity. Developers who want immutability get it by default.

Cadence reverses this. Contracts deployed on Flow are upgradeable by default. The runtime enforces backward compatibility — you can add new fields and functions, but you cannot remove or change existing ones in ways that break deployed interfaces. This means bug fixes and feature additions are possible without proxy wrappers or data migration contracts.

Immutability, if you want it, is explicit: remove all keys from the account that holds the contract. Once the account has no keys, no upgrade can be submitted. This is the right model for contracts that have reached production maturity and want to make a credible commitment to users.

The upgrade system does not protect against logic errors in the original deployment. A contract with a flawed access control design will remain flawed unless the account still holds keys to submit an upgrade. Ship carefully; upgradeability is a safety net, not a substitute for correctness.

Transactions Separate Authorization from Execution with Verifiable Pre/Post Conditions

Cadence transactions support a design-by-contract model with explicit phases:

transaction(amount: UFix64, recipient: Address) {


    let sentVault: @{FungibleToken.Vault}


    prepare(signer: auth(BorrowValue) &Account) {
        let vault = signer.storage.borrow<
            auth(FungibleToken.Withdraw) &FlowToken.Vault
        >(
            from: /storage/flowTokenVault
        ) ?? panic("Could not borrow vault")


        self.sentVault <- vault.withdraw(amount: amount)
    }


    execute {
        let receiverRef = getAccount(recipient)
            .capabilities.borrow<&{FungibleToken.Receiver}>(
                /public/flowTokenReceiver
            )
            ?? panic("Could not borrow receiver")


        receiverRef.deposit(from: <-self.sentVault)
    }
}

The prepare phase accesses account storage with explicit authorization entitlements. The execute phase performs logic without direct storage access. Optional pre and post conditions let developers assert invariants before and after execution. If a postcondition fails, the entire transaction reverts.

This structured approach means that authorization logic is separated from business logic, and security invariants are declarative rather than scattered across function bodies.

What This Means for Developers Building on Flow

Cadence smart contract security is not a feature you opt into. It is the default behavior of the language. Every contract deployed on Flow inherits these protections:

  • No reentrancy by construction
  • No accidental asset duplication or loss via resource linearity
  • Granular access control through entitlements and capabilities
  • Compile-time safety through strong static typing
  • Safe upgradability without proxy pattern risks
  • Declarative security invariants through transaction pre/post conditions

For developers coming from Solidity, Cadence and Solidity are languages that complement each other on Flow. The network supports both through full EVM equivalence, so teams can choose the right tool for each use case or create cross-vm experiences. For contracts handling high-value assets where security is the primary concern, Cadence offers guarantees that no amount of careful Solidity coding can replicate, because the guarantees come from the language itself.

Get Started

Start building with Cadence on the Flow developer portal

Explore the React SDK for AI-assisted development

Read the full Cadence language reference.