cadence

SKILL.md

Cadence Smart Contract Development

Use this skill whenever you are writing, reviewing, debugging, or auditing Cadence code on the Flow blockchain.

Documentation

Resource URL
Full docs https://cadence-lang.org/docs
LLM-optimized reference https://cadence-lang.org/llms.txt
Full LLM dump https://cadence-lang.org/llms-full.txt
Security best practices https://cadence-lang.org/docs/security-best-practices
Design patterns https://cadence-lang.org/docs/design-patterns
Anti-patterns https://cadence-lang.org/docs/anti-patterns
Testing framework https://cadence-lang.org/docs/testing-framework
Flow NFT Standard https://github.com/onflow/flow-nft
Flow FT Standard https://github.com/onflow/flow-ft

Tip: Append .mdx to any cadence-lang.org doc URL to get raw markdown.


AI Tools

Cadence is designed for AI-native development with three integrations:

Tool Description
Skills Install with npx skills add outblock/cadence-lang.org
MCP Server https://cadence-mcp.up.railway.app/mcp — docs search, code checking, type inspection
LLM Endpoints /llms.txt (index) and /llms-full.txt (complete docs)

MCP Quick Start

One-click install:

Claude Code:

claude mcp add cadence-mcp -- npx -y mcp-remote https://cadence-mcp.up.railway.app/mcp

Claude Desktop / Cursor / Antigravity / OpenCode:

{
  "mcpServers": {
    "cadence": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://cadence-mcp.up.railway.app/mcp"]
    }
  }
}

OpenCode (alternative):

{
  "mcp": {
    "cadence": {
      "type": "remote",
      "url": "https://cadence-mcp.up.railway.app/mcp"
    }
  }
}

Local stdio server (requires Flow CLI):

npx @outblock/cadence-mcp

MCP Tools: search_docs, get_doc, browse_docs, cadence_check, cadence_hover, cadence_definition, cadence_symbols. All LSP tools support mainnet/testnet imports.

Agent-Specific Setup

Agent Setup
Claude Code npx skills add outblock/cadence-lang.org + MCP above
Claude Desktop MCP config in claude_desktop_config.json
Cursor Skills + .cursor/rules file + MCP
Antigravity Skills (auto-reads SKILL.md) + MCP config
OpenCode Skills + MCP config
Codex Skills + AGENTS.md instructions
Gemini CLI Skills + GEMINI.md instructions

1. Language Fundamentals

Types at a Glance

Type Example Notes
Int 42 Arbitrary precision
UInt64 100 64-bit unsigned
UFix64 1.5 Fixed-point, 8 decimals. Use for balances
Bool true
String "hello" UTF-8
Address 0x1234 Account address
[T] [1, 2, 3] Array
{K: V} {"a": 1} Dictionary
T? nil Optional — nil-coalesce with ??
@T Resource — unique, non-copyable
&T Reference — borrow without moving

Variables, Optionals, Functions

let x: Int = 42                     // immutable
var name: String = "Cadence"        // mutable

// Optionals
let val: Int? = nil
let result = val ?? 0               // nil-coalescing
if let v = val { }                  // optional binding
let forced = val!                   // force-unwrap — panics, avoid

// Functions
access(all) fun greet(name: String): String {
    return "Hello, \(name)!"
}

// view = read-only (safe in scripts and pre/post)
access(all) view fun getBalance(): UFix64 { return self.balance }

// Pre/post conditions
access(all) fun withdraw(amount: UFix64): @Vault {
    pre  { amount > 0.0:              "Amount must be positive" }
    post { result.balance == amount:  "Withdrawal amount mismatch" }
}

Struct vs Resource

// Struct — copyable value type
struct Point {
    let x: Int; let y: Int
    init(x: Int, y: Int) { self.x = x; self.y = y }
}
let a = Point(x: 1, y: 2)
let b = a   // COPY — both exist independently

// Resource — unique, linear type (cannot copy)
resource Token {
    let id: UInt64
    init() { self.id = self.uuid }
}
let t1 <- create Token()
let t2 <- t1   // MOVE — t1 is no longer valid
destroy t2     // must explicitly destroy if not stored

Resource rules:

  1. Resources exist in exactly one place at a time.
  2. Every resource must be saved, moved, or destroyed before the current scope ends.
  3. Use @ in type annotations: @NFT, @{UInt64: NFT}.
  4. Use <- to move, <-! for force-assign into optional (panics on conflict).

2. Access Control & Entitlements

Access Modifiers

Modifier Who can access
access(all) Everyone (public read, callable by anyone)
access(self) Only within the type itself
access(contract) Same contract
access(account) Same account
access(E) Callers holding entitlement E

Entitlements Pattern

access(all) entitlement Withdraw
access(all) entitlement Owner

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

    // Anyone can read
    access(all) view fun getBalance(): UFix64 { return self.balance }

    // Anyone can deposit into you
    access(all) fun deposit(from: @Vault) {
        self.balance = self.balance + from.balance
        destroy from
    }

    // Only authorized callers can withdraw
    access(Withdraw) fun withdraw(amount: UFix64): @Vault {
        pre { self.balance >= amount: "Insufficient balance" }
        self.balance = self.balance - amount
        return <- create Vault(balance: amount)
    }
}

// Getting an entitled reference
let vaultRef = signer.storage
    .borrow<auth(Withdraw) &Vault>(from: /storage/vault)
    ?? panic("Could not borrow Vault")

References

let ref: &NFT = &myNFT                          // read-only reference
let authRef: auth(Withdraw) &Vault = &myVault   // entitled reference

// From storage
let r = signer.storage.borrow<auth(Withdraw) &Vault>(from: /storage/vault)
    ?? panic("No vault found")

Matching Access Modifiers Required for Interface Implementations

Implementation members must use exactly the same access modifier as the interface:

access(all) resource interface I {
    access(account) fun foo()
}

// BAD — access(all) is more permissive than access(account)
access(all) resource R: I {
    access(all) fun foo() {}
}

// GOOD
access(all) resource R: I {
    access(account) fun foo() {}
}

3. Capabilities

Capabilities are the mechanism for sharing access to stored objects. Nothing is public by default.

// 1. Save object to storage
signer.storage.save(<- create Vault(balance: 0.0), to: /storage/vault)

// 2. Issue a capability (creates a tracked controller)
let receiverCap = signer.capabilities.storage
    .issue<&{FungibleToken.Receiver}>(/storage/vault)

// 3. Publish so others can borrow it
signer.capabilities.publish(receiverCap, at: /public/receiver)

// 4. Borrow from another account
let receiver = getAccount(address).capabilities
    .borrow<&{FungibleToken.Receiver}>(/public/receiver)
    ?? panic("Account has no receiver capability")

// 5. Revoke when needed
let controller = signer.capabilities.storage
    .getController(byCapabilityID: capID) ?? panic("Controller not found")
controller.delete()

// Issue an entitled capability (grants Withdraw to holder)
let ownerCap = signer.capabilities.storage
    .issue<auth(Withdraw) &Vault>(/storage/vault)

Public Capability Acquisition Returns Non-Optional

capabilities.get<T> returns an invalid capability (not nil) when no capability exists or when T mismatches. Check validity with .check():

let capability = account.capabilities.get<&MyNFT.Collection>(/public/NFTCollection)
if !capability.check() {
    // Handle invalid capability (ID == 0, borrow returns nil)
}

4. Transactions & Scripts

Transaction Structure

transaction(amount: UFix64, recipient: Address) {

    // Declare fields shared across phases
    let vaultRef: auth(FungibleToken.Withdraw) &FungibleToken.Vault

    // Access accounts — the ONLY phase with account access
    prepare(signer: auth(BorrowValue) &Account) {
        self.vaultRef = signer.storage
            .borrow<auth(FungibleToken.Withdraw) &FungibleToken.Vault>(
                from: /storage/vault
            ) ?? panic("No vault found")
    }

    pre  { amount > 0.0: "Amount must be positive" }

    execute {
        let tokens <- self.vaultRef.withdraw(amount: amount)
        let receiver = getAccount(recipient).capabilities
            .borrow<&{FungibleToken.Receiver}>(/public/receiver)
            ?? panic("Recipient has no receiver")
        receiver.deposit(from: <- tokens)
    }

    post { /* verify post-conditions */ }
}

Phases: preparepreexecutepost

Common Signer Authorizations

prepare(signer: auth(SaveValue) &Account)                          // save to storage
prepare(signer: auth(BorrowValue) &Account)                        // borrow from storage
prepare(signer: auth(LoadValue) &Account)                          // load (remove) from storage
prepare(signer: auth(IssueStorageCapabilityController) &Account)   // issue capabilities
prepare(signer: auth(PublishCapability) &Account)                  // publish capabilities
prepare(signer: auth(Contracts) &Account)                          // deploy/update contracts
prepare(signer: auth(Keys) &Account)                               // manage account keys

Scripts (read-only, free, no transaction needed)

access(all) fun main(address: Address): UFix64 {
    return getAccount(address).capabilities
        .borrow<&{FungibleToken.Balance}>(/public/balance)
        ?.balance ?? 0.0
}

5. Account Storage

// Save — moves resource into storage
signer.storage.save(<- create NFT(), to: /storage/myNFT)

// Borrow — get a reference without removing
let ref = signer.storage.borrow<&NFT>(from: /storage/myNFT)

// Load — removes and returns (must handle the resource)
let nft <- signer.storage.load<@NFT>(from: /storage/myNFT)!

// Check existence and type
let t: Type? = signer.storage.type(at: /storage/myNFT)
if t != nil { /* already exists */ }

// Iterate all stored values
signer.storage.forEachStored(fun (path: StoragePath, type: Type): Bool {
    return true  // true = continue, false = stop
})

6. NFT Contract (Production Pattern)

access(all) contract BasicNFT {

    access(all) var totalSupply: UInt64
    access(all) event Minted(id: UInt64)
    access(all) event Withdraw(id: UInt64, from: Address?)
    access(all) event Deposit(id: UInt64, to: Address?)
    access(all) entitlement Withdraw

    access(all) let CollectionStoragePath: StoragePath
    access(all) let CollectionPublicPath: PublicPath

    access(all) resource NFT {
        access(all) let id: UInt64
        access(all) let metadata: {String: String}
        init(metadata: {String: String}) {
            self.id = self.uuid
            BasicNFT.totalSupply = BasicNFT.totalSupply + 1
            emit Minted(id: self.id)
        }
    }

    access(all) resource Collection {
        access(self) var ownedNFTs: @{UInt64: NFT}

        access(all) fun deposit(token: @NFT) {
            emit Deposit(id: token.id, to: self.owner?.address)
            self.ownedNFTs[token.id] <-! token
        }

        access(Withdraw) fun withdraw(id: UInt64): @NFT {
            let token <- self.ownedNFTs.remove(key: id)
                ?? panic("NFT \(id) not found in collection")
            emit Withdraw(id: token.id, from: self.owner?.address)
            return <- token
        }

        access(all) fun getIDs(): [UInt64] { return self.ownedNFTs.keys }
        access(all) view fun borrowNFT(_ id: UInt64): &NFT? {
            return &self.ownedNFTs[id]
        }

        init() { self.ownedNFTs <- {} }
    }

    access(all) fun createEmptyCollection(): @Collection {
        return <- create Collection()
    }

    init() {
        self.totalSupply = 0
        self.CollectionStoragePath = /storage/basicNFTCollection
        self.CollectionPublicPath = /public/basicNFTCollection
    }
}

NFT Setup Transaction

transaction {
    prepare(signer: auth(SaveValue, IssueStorageCapabilityController, PublishCapability) &Account) {
        // Idempotent — skip if already set up
        if signer.storage.type(at: BasicNFT.CollectionStoragePath) != nil { return }

        signer.storage.save(<- BasicNFT.createEmptyCollection(), to: BasicNFT.CollectionStoragePath)
        let cap = signer.capabilities.storage
            .issue<&BasicNFT.Collection>(BasicNFT.CollectionStoragePath)
        signer.capabilities.publish(cap, at: BasicNFT.CollectionPublicPath)
    }
}

Mint & Transfer

// Mint (admin transaction)
transaction(recipient: Address, name: String) {
    execute {
        let nft <- create BasicNFT.NFT(metadata: {"name": name})
        getAccount(recipient).capabilities
            .borrow<&BasicNFT.Collection>(BasicNFT.CollectionPublicPath)
            ?.deposit(token: <- nft)
            ?? panic("Recipient has no collection")
    }
}

// Transfer (user transaction)
transaction(recipient: Address, nftID: UInt64) {
    let withdrawRef: auth(BasicNFT.Withdraw) &BasicNFT.Collection
    prepare(signer: auth(BorrowValue) &Account) {
        self.withdrawRef = signer.storage
            .borrow<auth(BasicNFT.Withdraw) &BasicNFT.Collection>(
                from: BasicNFT.CollectionStoragePath
            ) ?? panic("No collection")
    }
    execute {
        let nft <- self.withdrawRef.withdraw(id: nftID)
        getAccount(recipient).capabilities
            .borrow<&BasicNFT.Collection>(BasicNFT.CollectionPublicPath)
            ?.deposit(token: <- nft)
            ?? panic("Recipient has no collection")
    }
}

7. Fungible Token (Production Pattern)

access(all) contract BasicToken {
    access(all) var totalSupply: UFix64
    access(all) entitlement Withdraw

    access(all) resource Vault {
        access(all) var balance: UFix64
        init(balance: UFix64) { self.balance = balance }

        access(all) fun deposit(from: @Vault) {
            self.balance = self.balance + from.balance
            destroy from
        }

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

    access(all) fun createEmptyVault(): @Vault {
        return <- create Vault(balance: 0.0)
    }

    init() { self.totalSupply = 0.0 }
}

Cadence 1.0 Token Standard Changes

FungibleToken.Vault and NonFungibleToken.NFT / NonFungibleToken.Collection are now interfaces, not concrete types. Update all references:

// Before (Cadence 0.x)
fun deposit(from: @FungibleToken.Vault)

// After (Cadence 1.0)
fun deposit(from: @{FungibleToken.Vault})

Why vaults beat ledgers:

Ledger (Solidity) Vault (Cadence)
Balances in a contract mapping Each user holds their own Vault
Reentrancy attacks possible Resources move atomically
Admin can modify any balance Only the holder can access their Vault

8. Security Rules (Non-Negotiable)

S1 — Least Privilege

Start access(self), widen only when justified. Never access(all) on state-modifying functions.

// BAD — anyone drains your vault
access(all) fun withdraw(amount: UFix64): @Vault { ... }

// GOOD — requires entitlement
access(Withdraw) fun withdraw(amount: UFix64): @Vault { ... }

S2 — Never Pass Fully Authorized Account Refs as Parameters

// BAD — gives full storage access to callee
access(all) fun setup(admin: auth(Storage) &Account) { ... }

// GOOD — use a narrowly-scoped capability instead
access(all) fun setup(adminCap: Capability<&Admin>) { ... }

S3 — Always Validate with Pre/Post Conditions

access(Withdraw) fun withdraw(amount: UFix64): @Vault {
    pre  { amount > 0.0:                             "Amount must be positive" }
    pre  { self.balance >= amount:                   "Insufficient balance" }
    post { result.balance == amount:                 "Withdrawal amount mismatch" }
    post { self.balance == before(self.balance) - amount: "Balance accounting error" }
}

S4 — Meaningful Panic Messages (Never Force-Unwrap Silently)

// BAD — silent crash
let ref = cap.borrow()!

// GOOD — actionable message
let ref = cap.borrow()
    ?? panic("Could not borrow Vault — capability may be revoked or incorrect type")

S5 — Private Data Is Not Secret

access(self) controls programmatic access, not blockchain visibility. All storage is publicly readable off-chain. Never store secrets, private keys, or PII on-chain.

S6 — Never Hard-Code Addresses

// BAD
import FungibleToken from 0xf233dcee88fe0abe

// GOOD (uses flow.json aliases, resolved at deployment)
import "FungibleToken"

S7 — Check Before Setup (Idempotent Transactions)

transaction {
    prepare(signer: auth(SaveValue) &Account) {
        if signer.storage.type(at: /storage/vault) != nil { return }
        signer.storage.save(<- MyToken.createEmptyVault(), to: /storage/vault)
    }
}

9. Testing

Tests are Cadence files using the Test standard library, executed via Flow CLI.

import Test

let blockchain = Test.newEmulatorBlockchain()
let admin = blockchain.createAccount()

access(all) fun setup() {
    let err = blockchain.deployContract(
        name: "MyNFT",
        code: Test.readFile("../contracts/MyNFT.cdc"),
        account: admin,
        arguments: []
    )
    Test.expect(err, Test.beNil())
}

access(all) fun testMint() {
    let tx = Test.Transaction(
        code: Test.readFile("../transactions/mint.cdc"),
        authorizers: [admin.address],
        signers: [admin],
        arguments: ["My NFT"]
    )
    Test.expect(blockchain.executeTransaction(tx), Test.beSucceeded())

    let events = Test.eventsOfType(Type<MyNFT.Minted>())
    Test.assertEqual(1, events.length)
}

access(all) fun testQuery() {
    let result = blockchain.executeScript(
        Test.readFile("../scripts/get_ids.cdc"),
        [admin.address]
    )
    let ids = result.returnValue! as! [UInt64]
    Test.assertEqual(1, ids.length)
}

Common assertions:

Test.assertEqual(expected, actual)
Test.assert(condition, message: "reason")
Test.expect(result, Test.beSucceeded())
Test.expect(result, Test.beFailed())
Test.expect(err, Test.beNil())

Run tests:

flow test tests/my_test.cdc
flow test --cover tests/my_test.cdc

10. Naming Conventions

Construct Convention Example
Contract, Resource, Struct, Interface, Event PascalCase FlowToken, TokenVault
Function, variable, parameter camelCase getBalance(), totalSupply
Storage/public paths let fields on the contract let VaultStoragePath: StoragePath
Entitlements PascalCase entitlement Withdraw

Never hardcode path strings in transactions — always reference the contract's published path constants.


11. Anti-Patterns

A1 — Never Pass Fully Authorized Account Refs as Parameters

A function accepting auth(Storage) &Account can access all storage — drain vaults, steal NFTs.

// BAD — callee can withdraw FLOW or modify anything
access(all) fun transferNFT(id: UInt64, owner: auth(Storage) &Account) { ... }

// GOOD — authenticate via resources/capabilities
access(all) fun transferNFT(id: UInt64, collectionCap: Capability<auth(Withdraw) &Collection>) { ... }

A2 — Public Functions Should Be Read-Only or Explicitly Entitled

Only view functions and functions everyone should call should be access(all). State-modifying functions require entitlements.

A3 — Capability-Typed Public Fields Are Security Holes

Capabilities are value types — a public field can be copied by anyone:

// BAD — anyone copies the capability and calls its functions
access(all) var adminCap: Capability<&Admin>

// GOOD
access(self) var adminCap: Capability<&Admin>
access(Owner) fun getAdminCap(): Capability<&Admin> { return self.adminCap }

A4 — Never Make Admin Creation Functions Public

// BAD — anyone mints admin access
access(all) fun createAdmin(): @Admin { return <- create Admin() }

// GOOD — create once in init, existing admins create new ones
init() {
    self.account.storage.save(<- create Admin(), to: /storage/currencyAdmin)
}

A5 — Never Emit Events or Modify Contract State in Struct Initializers

Structs are public and can be created by anyone. Side effects in init() can be exploited:

// BAD — anyone spams events and overflows nextPlayID
access(all) struct Play {
    init() {
        TopShot.nextPlayID = TopShot.nextPlayID + 1  // BAD
        emit PlayCreated(id: self.playID)             // BAD
    }
}

// GOOD — state changes happen only inside admin resource
access(all) resource Admin {
    access(all) fun createPlay() {
        var newPlay = Play()
        TopShot.nextPlayID = TopShot.nextPlayID + UInt32(1)
        emit PlayCreated(id: newPlay.playID)
    }
}

A6 — Complex/Capability Fields Must Be access(self)

access(all) on arrays, dictionaries, structs, resources, or capabilities allows direct mutation:

// BAD
access(all) var adminCap: Capability<&Admin>

// GOOD
access(self) var adminCap: Capability<&Admin>

12. Capability Bootstrapping (Inbox API)

When account A needs to send a capability to account B, a single transaction cannot be signed by both accounts simultaneously. Use the Inbox API:

Step 1 — Provider publishes the capability to recipient's inbox:

import "BasicNFT"

transaction(receiver: Address, name: String) {
    prepare(signer: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) {
        let capability = signer.capabilities.storage
            .issue<&BasicNFT.Minter>(BasicNFT.minterPath)

        let controller = signer.capabilities.storage
            .getController(byCapabilityID: capability.id)
            ?? panic("Controller not found")
        controller.setTag(name)

        signer.inbox.publish(capability, name: name, recipient: receiver)
    }
}

Step 2 — Recipient claims the capability:

import "BasicNFT"

transaction(provider: Address, name: String) {
    prepare(signer: auth(ClaimInboxCapability, SaveValue) &Account) {
        let capability = signer.inbox.claim<&BasicNFT.Minter>(name, provider: provider)
            ?? panic("No capability named '\(name)' from \(provider)")
        signer.storage.save(capability, to: BasicNFT.minterPath)
    }
}

The provider can call signer.inbox.unpublish(name) to retract before the recipient claims.


13. Performance: Prefer borrow Over load/save

load() moves the resource out of storage (expensive), save() moves it back (expensive again). borrow() returns an in-place reference — always prefer it.

// BAD — unnecessary round-trip moves the entire resource
transaction {
    prepare(acct: auth(LoadValue, SaveValue) &Account) {
        let vault <- acct.storage.load<@ExampleToken.Vault>(from: /storage/exampleToken)!
        let burned <- vault.withdraw(amount: 10)
        destroy burned
        acct.storage.save(<- vault, to: /storage/exampleToken)
    }
}

// GOOD — borrow a reference, mutate in-place, no save needed
transaction {
    prepare(acct: auth(BorrowValue) &Account) {
        let vault = acct.storage.borrow<&ExampleToken.Vault>(from: /storage/exampleToken)!
        let burned <- vault.withdraw(amount: 10)
        destroy burned
    }
}

14. Measuring Time

Flow produces blocks approximately every 0.8 seconds. Both block timestamp and block height are available on-chain.

let block = getCurrentBlock()
block.timestamp   // Unix timestamp (UFix64, seconds)
block.height      // Block number (UInt64)

let pastBlock = getBlock(at: 70001)
pastBlock?.timestamp
pastBlock?.height
Method Use when
getCurrentBlock().timestamp Acceptable for events lasting hours/days. Accurate to ~10 seconds.
getCurrentBlock().height More manipulation-resistant. Requires off-chain rate estimation.

Rules:

  • Timestamps cannot go backwards and cannot be more than 10 seconds ahead of the previous block.
  • Never hardcode an assumed block rate — it changes over time.
  • Auctions and time-locks should have an extension mechanism.

15. Contract Upgrades

Compatible Changes (Preferred)

Cadence supports in-place upgrades for additive changes: adding new fields (with defaults), new functions, new events. Use flow contracts update.

Incompatible Changes

Option A — New address (safest):

  1. Deploy new contract to a new account.
  2. Increment path suffixes (/public/MyVault002).
  3. Write upgrade transactions to migrate users' resources.

Option B — Same address (risky, last resort):

  1. Delete all resources in the contract account (e.g., Admin resource).
  2. Delete the contract from the account.
  3. Deploy the new contract.

⚠️ If any user account holds structs or resources from the old contract version, those will **fail to

Weekly Installs
16
First Seen
Feb 21, 2026
Installed on
opencode16
github-copilot16
codex16
kimi-cli16
gemini-cli16
cursor16