Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,63 @@
- Verify complete transfers (assert residual balance == 0.0)
- Size withdrawals by sink capacity when immediately depositing

## Cross-VM and DeFi Advanced Checks

### Stuck State / Missing Emergency Exit
- Does any `burnCallback` or `closeCallback` assert against an external system (EVM vault, bridge, oracle)?
- If that external system fails permanently, can the vault ever be closed? Is there an admin emergency exit with a grace period (e.g., >7 days stuck → force close)?
- Pattern: closing a vault asserts a pending operation is nil, but cancelling that operation also calls EVM → if EVM is paused, both close and cancel fail, funds are permanently trapped.

### Cross-VM Timing Races
- In deferred operations (request phase → claim phase): is the timelock or deadline captured once at request time and never re-queried?
- If the external system changes its timelock after the request but before the claim, the stored timestamp is stale — claim executes too early (EVM reverts) or too late (user waits unnecessarily).
- Fix: re-query the current external timelock at claim time and use `max(storedTimestamp, currentTime + currentTimelock)`.

### Lingering EVM Approvals
- After completing an EVM-side redemption or transfer, is the ERC20 approval (`approve(spender, amount)`) explicitly revoked?
- ERC4626 `redeem()` does not consume ERC20 allowance — an unrewoked approval persists. If the user later deposits more shares and the spender COA is compromised, unauthorized redemption becomes possible.
- Fix: send a zero-approval call immediately after a successful EVM redemption completes.

### EVM Call Result Not Validated
- Every `coa.call()` and `EVM.dryCall()` returns `EVM.Result`. Is `result.status == EVM.Status.successful` checked before trusting `result.data`?
- Unchecked EVM failure = silent state divergence between Cadence and EVM.

### Bridge Precision Loss (UFix64 ↔ uint256)
- UFix64 has 8 decimal places. EVM ERC20 tokens typically use 18. Conversion without explicit ×10^10 scaling truncates or inflates amounts.
- Dust accumulates across calls into exploitable value at volume.
- Fix: explicit scaling on every Cadence↔EVM conversion. Round against the user (floor on deposit, ceiling on withdrawal).
- HackenProof Flow bug bounty classifies this as Critical.

### Fixed-Point Overflow in AMM Liquidity (Cetus-class)
- Cetus DEX (Sui, May 2025, $223M): unchecked bit-shift in liquidity delta math. Same pattern applies to any Cadence AMM with custom fixed-point scaling.
- Does any bit-shift or scaling operation have a correct bit-width guard (not just value bounds)?
- Can a flash-add max liquidity + immediate remove expose a divergence between deposit requirement and withdrawal entitlement?

### access(account) Co-Deploy Escalation
- `access(account)` exposes fields to ALL contracts on the same Flow account.
- A future contract upgrade on that account (or compromised key) gains full read/write access to all `access(account)` state across every co-deployed contract.
- Dec 27, 2025 exploit deployed ~40 malicious contracts to amplify this vector.
- Audit: enumerate all `access(account)` fields. Verify all contracts on that account are equally trusted. Verify upgrade requires multi-sig.

### Burnable.burnCallback() Griefing via External Token
- Any protocol that accepts arbitrary `FungibleToken` vaults and destroys them can be griefed by a token whose `burnCallback()` panics or executes unexpected state changes inside the protocol's own transaction.
- Is `Burner.burn(<-vault)` called on vaults received from external sources without type verification?
- Fix: only destroy vaults of explicitly allowlisted token types. Never call `Burner.burn()` on caller-supplied vaults without type verification.

### Cross-VM Bridge as Exploit Exit — No Rate Limit
- Dec 27, 2025 ($3.9M): attacker minted counterfeit FLOW → bridged to Flow EVM → bridged to Ethereum before validators halted. All in one block sequence.
- Does the protocol interact with the EVM bridge? Is there a per-block or per-transaction volume cap? Is there an admin pause callable within seconds?
- Any mint/inflate exploit can exit via the bridge faster than governance can react.

### Contract Initializer Argument Type Smuggling
- Dec 27, 2025: `account.contracts.add()` accepted arguments where calling context treated them as value types but initializer treated them as resources → resources copied instead of moved → infinite mint. Patched in Cadence v1.8.9.
- Audit: any deployment/upgrade transaction that passes attachments, `PublicKey` wrappers, or nested composites as initializer arguments. Verify static type == dynamic type for all complex arguments.

### Use-After-Free via Retained Reference After Resource Destroy
- Halborn 2021: Cadence allowed method calls on a resource reference after the resource was destroyed. Runtime-patched.
- In multi-contract systems: if contract A holds a borrowed reference from contract B's storage, and B destroys the resource via a separate path, the reference in A is stale.
- Verify no live cross-contract reference exists to a resource before it is destroyed.

## Optimization

### Storage Operations
Expand Down
1 change: 1 addition & 0 deletions plugins/flow-dev/skills/cadence-lang/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Read the relevant reference file based on your task:
| Security best practices | [security-best-practices.md](references/security-best-practices.md) |
| Anti-patterns to avoid | [anti-patterns.md](references/anti-patterns.md) |
| Design patterns | [design-patterns.md](references/design-patterns.md) |
| CU cost by operation, optimization patterns, MAX_SAFE_N methodology | [cu-optimization.md](references/cu-optimization.md) |

For security-sensitive tasks, also read `security-best-practices.md` and `anti-patterns.md`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,56 @@ access(all) resource Minter {

Similarly, avoid emitting events from struct initializers.

## Anti-Pattern 6: access(account) in Multi-Contract Accounts

**Problem**: `access(account)` grants access to ALL contracts on the same Flow account — not just the declaring contract. A future contract upgrade or compromised key exposes all `access(account)` state.

```cadence
// ❌ Any contract on this account can read/write this field
access(account) var totalSupply: UFix64
access(account) var adminNonce: UInt64
```

**Real-world risk**: Dec 27, 2025 exploit deployed malicious contracts to an account to amplify access. If sensitive state is `access(account)`, any co-deployed contract is a lateral movement path.

**Fix**: Prefer `access(self)` for fields that only the declaring contract needs. Use `access(account)` only when cross-contract access within the account is genuinely required AND all co-deployed contracts are equally trusted.
```cadence
// ✅ Only this contract can access
access(self) var totalSupply: UFix64

// ✅ If cross-contract access is needed, document why and what contracts share the account
access(account) var sharedNonce: UInt64 // Shared with ContractB on same account (multi-sig upgrades only)
```

**Audit**: For every `access(account)` field, list which contracts share the account and whether any of them can be upgraded independently.

## Anti-Pattern 7: Burner.burn() on External Token Vaults

**Problem**: Accepting arbitrary `FungibleToken` vaults and destroying them via `Burner.burn()` lets an attacker grief the protocol with a malicious `burnCallback()`.

```cadence
// ❌ burnCallback() of unknown token type runs in THIS transaction
access(all) fun rejectDeposit(vault: @{FungibleToken.Vault}) {
Burner.burn(<-vault) // Attacker controls burnCallback — can panic or modify state
}
```

**Why dangerous**: Cadence 1.0 `Burner.Burnable` interface lets any token define custom logic on destruction. A maliciously crafted token's `burnCallback()` runs inside the protocol's transaction context, potentially causing panic (DoS) or unexpected state changes.

**Fix**: Validate token type against an allowlist before destroying. Return rejected vaults to the sender rather than destroying them.
```cadence
// ✅ Only destroy known, trusted token types
access(all) fun rejectDeposit(vault: @{FungibleToken.Vault}) {
assert(vault.isInstance(Type<@FlowToken.Vault>()), message: "Unsupported token type")
Burner.burn(<-vault)
}

// ✅ Even better: return instead of destroy
access(all) fun rejectDeposit(vault: @{FungibleToken.Vault}): @{FungibleToken.Vault} {
return <-vault // Caller handles disposal
}
```

## Detection Checklist

- [ ] Functions accepting `auth(...) &Account` parameters
Expand All @@ -134,3 +184,5 @@ Similarly, avoid emitting events from struct initializers.
- [ ] State modifications in struct initializers
- [ ] Event emissions in struct initializers
- [ ] Over-use of `access(all)` modifier
- [ ] `access(account)` fields without documenting which co-deployed contracts share the account
- [ ] `Burner.burn()` called on externally-supplied vaults without token type validation
126 changes: 126 additions & 0 deletions plugins/flow-dev/skills/cadence-lang/references/cu-optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Cadence CU Optimization

Flow hard-caps every transaction at **9,999 CU**. Fees have two components:
- Inclusion fee: 0.0001 FLOW (fixed)
- Execution fee: CU × price_per_CU (≈ 4.0e-5 FLOW/CU)

Extract real CU from the `FlowFees.FeesDeducted` event `amount` field after any sealed transaction.

## CU Cost by Operation Type

| Operation | CU cost | Notes |
|-----------|---------|-------|
| FT transfer (reference) | ~19 CU | Baseline for fee math |
| `{UInt64: T}` dict write | ~7–8 CU | 2 hash passes + B+ tree traversal |
| `[T]` array append | ~0.3–0.5 CU | Bounds check only |
| `account.storage.borrow<&T>` | ~5 CU | Charged even if resource unchanged |
| `StoragePath(identifier: s)` construction | ~1 CU | String allocation |
| EVM `dryCall` (simple view) | ~50–100 CU | Varies with return data size |
| EVM `coa.call` (state mutating) | ~200–500 CU | Plus EVM gas converted to CU |

## High-CU Patterns to Avoid

**Dict writes inside loops**
```cadence
// ❌ 7–8 CU per iteration
for id in ids {
self.registry[id] = value
}

// ✅ ~0.4 CU per iteration — use array if keyed access not needed
self.items.append(value)
```

**Borrow inside loops**
```cadence
// ❌ ~5 CU wasted per iteration even if resource unchanged
for i in items {
let r = self.account.storage.borrow<&Counter>(from: /storage/counter)!
r.increment()
}

// ✅ borrow once, use reference for all iterations
let counter = self.account.storage.borrow<&Counter>(from: /storage/counter)!
for i in items {
counter.increment()
}
```

**Path construction inside loops**
```cadence
// ❌ ~1 CU per iteration
for id in ids {
let path = StoragePath(identifier: "vault_".concat(id.toString()))!
}

// ✅ construct once outside loop when path is constant
let path = StoragePath(identifier: "vault_main")!
```

**Individual counter increments**
```cadence
// ❌ N transactions of 5 CU each
for _ in items {
self.head = self.head + 1
}

// ✅ single bulk update
self.head = self.head + UInt64(items.length)
```

## Resource Inlining

Resources with total encoded size ≤ 512 bytes are inlined in their parent — zero extra storage read. If a resource exceeds 512 bytes it spills to a separate slot, adding CU on every access.

```
Rough field sizes (encoded):
UInt64 8 bytes
UFix64 8 bytes
Address 20 bytes
String variable (length prefix + content)
Bool 1 byte

Check: sum(field_sizes) ≤ 512 bytes → inlined → no extra CU
```

## Composite Key Packing

```cadence
// ❌ String key: 85 bytes, hashed twice per write
let key = fromAddr.toString().concat("->").concat(toAddr.toString())
self.edges[key] = weight

// ✅ packed UInt64: 8 bytes, same hash cost
let key: UInt64 = (UInt64(fromId) << 32) | UInt64(toId)
self.edges[key] = weight
```

Only works when both components fit in UInt32 (max value ~4.3 billion each).

## Sweep Methodology for Measuring MAX_SAFE_N

Run the transaction at increasing N values and record the fee from `FlowFees.FeesDeducted`:

```
N = [64, 128, 256, 512, 768, 1024, 1280, 1536]
```

1. Find the cliff where the tx fails — binary-search the exact limit at ±4 entries.
2. Plot fee vs N: y-intercept = fixed overhead, slope = CU/entry.
3. Set MAX_SAFE_N = highest passing N with ≥10% headroom below 9,999 CU.

```
RECOMMENDED_FEE_PER_ENTRY = (FEE_PER_TX / MAX_SAFE_N) × 1.10
```

## EVM CU Interaction

Cross-VM calls consume CU on the Cadence side in addition to EVM gas:

| Call type | Cadence CU overhead | EVM gas |
|-----------|---------------------|---------|
| `EVM.dryCall` simple view | ~50–100 CU | consumed but not charged |
| `EVM.dryCall` large array return | ~200–800 CU | scales with return data size |
| `coa.call` state mutating | ~200–500 CU | charged separately in FLOW |

Decode `EVM.Result.data` inline — passing a large `[UInt8]` as a function argument costs extra CU at the function boundary.
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,78 @@ access(all) fun getRandomSeed(blockHeight: UInt64): [UInt8] {
**DeFi applications:** Fair lottery/raffle contracts, randomized NFT drops, prediction market resolution.

> **See also:** `defi-primitives.md` for building blocks (lending models, AMM selection).

---

## Cross-VM Failure Modes

Understanding what happens when the EVM component of an atomic transaction fails.

### Atomicity Guarantee

A Cadence transaction containing EVM calls is atomic at the Cadence level:
- If the Cadence transaction panics, all state changes (Cadence + EVM) are reverted.
- If an EVM call fails, the EVM state change is reverted, but **Cadence execution continues** unless you explicitly check the result and panic.

```cadence
let result = coa.call(to: evmAddr, data: calldata, gasLimit: 100000, value: EVM.Balance(attoflow: 0))

// result.status is NOT automatically checked — you must handle failure explicitly
if result.status != EVM.Status.successful {
panic("EVM call failed: ".concat(result.errorCode.toString()))
}
```

Failing to check `result.status` means the Cadence transaction succeeds and its state changes persist even when the EVM call silently failed.

### EVM.Status Values

| Status | Meaning |
|--------|---------|
| `EVM.Status.successful` | EVM call executed and committed |
| `EVM.Status.failed` | EVM call reverted — EVM state unchanged |
| `EVM.Status.invalid` | Malformed call (bad calldata, insufficient gas) |

### Gas Exhaustion

If `gasLimit` is too low for the EVM call, the call fails with `EVM.Status.failed` and the gas is consumed. The Cadence transaction continues unless you panic on failure.

Conservative gas defaults:
- Simple view call: `50_000`
- Array read (N ≤ 1024): `3_000_000`
- State-mutating call: `100_000`

### Handling Partial Batch Failures

When batching multiple EVM calls in one Cadence transaction, decide upfront whether partial success is acceptable:

```cadence
// All-or-nothing: panic on any failure
for call in calls {
let result = coa.call(to: call.to, data: call.data, gasLimit: call.gas, value: EVM.Balance(attoflow: 0))
if result.status != EVM.Status.successful {
panic("batch aborted at call ".concat(call.label).concat(": ").concat(result.errorCode.toString()))
}
}

// Best-effort: log failures, continue
var failedCalls: [String] = []
for call in calls {
let result = coa.call(to: call.to, data: call.data, gasLimit: call.gas, value: EVM.Balance(attoflow: 0))
if result.status != EVM.Status.successful {
failedCalls.append(call.label)
}
}
```

### CU Interaction with EVM Calls

EVM calls consume Cadence CU in addition to EVM gas:

| Call type | Cadence CU overhead |
|-----------|---------------------|
| `EVM.dryCall` simple view | ~50–100 CU |
| `EVM.dryCall` large array return | ~200–800 CU |
| `coa.call` state mutating | ~200–500 CU |

Both EVM gas and Cadence CU must stay within budget. A transaction that stays under 9,999 CU but exceeds the EVM `gasLimit` will have the EVM call fail silently (unless you panic on `result.status`).
1 change: 1 addition & 0 deletions plugins/flow-dev/skills/flow-dev-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Install and configure the tools needed for Flow blockchain development. Each ref
| Start and configure the Flow Emulator (ports, persistence, fork mode) | [emulator.md](references/emulator.md) |
| Install Cadence VS Code extension for editor support | [vscode-extension.md](references/vscode-extension.md) |
| Set up testing framework, run tests, coverage, fork testing | [testing.md](references/testing.md) |
| CDC vs Go/overflow decision tree, adversarial test categories, coverage interpretation | [testing-patterns.md](references/testing-patterns.md) |
| Set up dev wallet for local frontend auth testing | [dev-wallet.md](references/dev-wallet.md) |
| Install FCL or React SDK for frontend integration | [frontend-sdk.md](references/frontend-sdk.md) |
| Set up Cadence MCP server for AI-assisted development | [cadence-mcp.md](references/cadence-mcp.md) |
Expand Down
Loading