Claude Skill

cairo-vulnerability-scanner

Scans Cairo/StarkNet smart contracts for 6 critical vulnerabilities including felt252 arithmetic overflow, L1-L2 messaging issues, address conversion problems, and signature replay. Use when auditing StarkNet projects.

LLM Mart · 0 points · 7 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download trailofbits-skills-plugins_building-secure-contracts_skills_cairo-vulnerability-scanner-123037e.zip · 12 KB
trailofbits/skills 7234 616 forks CC-BY-SA-4.0 Updated 9h ago
Part of trailofbits/skills — 100 skills

Install

skills CLI npx skills add https://github.com/trailofbits/skills/tree/main/plugins/building-secure-contracts/skills/cairo-vulnerability-scanner
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install trailofbits-skills@llmmart
Git git clone https://github.com/trailofbits/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole trailofbits/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Cairo/StarkNet Vulnerability Scanner

1. Purpose

Systematically scan Cairo smart contracts on StarkNet for platform-specific security vulnerabilities related to arithmetic, cross-layer messaging, and cryptographic operations. This skill encodes 6 critical vulnerability patterns unique to Cairo/StarkNet ecosystem.

2. When to Use This Skill

  • Auditing StarkNet smart contracts (Cairo)
  • Reviewing L1-L2 bridge implementations
  • Pre-launch security assessment of StarkNet applications
  • Validating cross-layer message handling
  • Reviewing signature verification logic
  • Assessing L1 handler functions

3. Platform Detection

File Extensions & Indicators

  • Cairo files: .cairo

Language/Framework Markers

// Cairo contract indicators
#[contract]
mod MyContract {
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        balance: LegacyMap<ContractAddress, felt252>,
    }

    #[external(v0)]
    fn transfer(ref self: ContractState, to: ContractAddress, amount: felt252) {
        // Contract logic
    }

    #[l1_handler]
    fn handle_deposit(ref self: ContractState, from_address: felt252, amount: u256) {
        // L1 message handler
    }
}

// Common patterns
felt252, u128, u256
ContractAddress, EthAddress
#[external(v0)], #[l1_handler], #[constructor]
get_caller_address(), get_contract_address()
send_message_to_l1_syscall

Project Structure

  • src/contract.cairo - Main contract implementation
  • src/lib.cairo - Library modules
  • tests/ - Contract tests
  • Scarb.toml - Cairo project configuration

Tool Support

  • Caracal: Trail of Bits static analyzer for Cairo
  • Installation: cargo install --git https://github.com/crytic/caracal --profile release --force (a Rust tool — not on PyPI)
  • Usage: caracal detect src/
  • cairo-test: Built-in testing framework
  • Starknet Foundry: Testing and development toolkit

4. How This Skill Works

When invoked, I will:

  1. Search your codebase for Cairo files
  2. Analyze each contract for the 6 vulnerability patterns
  3. Report findings with file references and severity, above them a coverage table carrying a verdict for every pattern
  4. Provide fixes for each identified issue
  5. Check L1-L2 interactions for messaging vulnerabilities

5. Example Output

When vulnerabilities are found, you'll get a report like this:

=== CAIRO/STARKNET VULNERABILITY SCAN RESULTS ===

6. Vulnerability Patterns (6 Patterns)

I check for 6 critical vulnerability patterns unique to Cairo/Starknet. For detailed detection patterns, code examples, mitigations, and testing strategies, see VULNERABILITY_PATTERNS.md.

Pattern Summary:

  1. Felt252 Arithmetic Overflow/Underflow ⚠️ HIGH - felt252 wraps silently; use u128/u256
  2. L1 to L2 Address Conversion ⚠️ HIGH - L1 address not validated against STARKNET_FIELD_PRIME
  3. L1 to L2 Message Failure ⚠️ HIGH - No cancellation path when a message cannot be consumed
  4. Overconstrained L1 <-> L2 Interaction ⚠️ MEDIUM - Coupling that can strand funds or block progress
  5. Signature Replay Protection ⚠️ HIGH - No nonce, or a domain separator missing chain/contract
  6. Unchecked from_address in L1 Handler ⚠️ CRITICAL - Any L1 contract can drive the handler

For complete vulnerability patterns with code examples, see VULNERABILITY_PATTERNS.md.

7. Scanning Workflow

Step 1: Platform Identification

  1. Verify Cairo language and StarkNet framework
  2. Check Cairo version (Cairo 1.0+ vs legacy Cairo 0)
  3. Locate contract files (src/*.cairo)
  4. Identify L1-L2 bridge contracts (if applicable)

Step 2: Arithmetic Safety Sweep

# Find felt252 usage in arithmetic
rg "felt252" src/ | rg "[-+*/]"

# Find balance/amount storage using felt252
rg "felt252" src/ | rg "balance|amount|total|supply"

# Should prefer u128, u256 instead

Step 3: L1 Handler Analysis

For each #[l1_handler] function:

  • Validates from_address parameter
  • Checks address != zero
  • Has proper access control
  • Emits events for monitoring

Step 4: Signature Verification Review

For signature-based functions:

  • Includes nonce tracking
  • Nonce incremented after use
  • Domain separator includes chain ID and contract address
  • Cannot replay signatures

Step 5: L1-L2 Bridge Audit

If contract includes bridge functionality:

  • L1 validates address < STARKNET_FIELD_PRIME
  • L1 implements message cancellation
  • L2 validates from_address in handlers
  • Symmetric access controls L1 ↔ L2
  • Test full roundtrip flows

Step 6: Static Analysis with Caracal

# Run Caracal detectors
caracal detect src/

# Specific detectors
caracal detect src/ --detectors unchecked-felt252-arithmetic
caracal detect src/ --detectors unchecked-l1-handler-from
caracal detect src/ --detectors missing-nonce-validation

8. Reporting Format

Coverage Table

Report on every pattern in §6, whether or not it turned anything up. Emit this table above the findings, with all 6 rows present:

# Pattern Verdict Evidence
1 Felt252 Arithmetic Overflow/Underflow clear balances are u256; searched felt252 in arithmetic, none
2 L1 to L2 Address Conversion
3 L1 to L2 Message Failure
4 Overconstrained L1 <-> L2 Interaction
5 Signature Replay Protection
6 Unchecked from_address in L1 Handler

Each verdict is one of:

  • found — cite file:line and write the finding up in full below.
  • clear — the pattern applies to this contract and the contract handles it. Name the function, trait, or check you searched for, so a reader can repeat the search.
  • n/a — the pattern cannot apply here. Give the reason in one clause ("no L1 handlers in this contract"). Not having looked is not n/a.

A table with fewer than 6 rows is an incomplete scan and must be reported as one. A row whose Verdict cell is empty is incomplete in the same way: row 1 above is filled in to show the shape, and every row is filled in the same way before the report is done. Six clear verdicts is a result a reader can act on. A report that covers two patterns and says nothing about the other four reads exactly like a clean contract, and that is the failure this table exists to prevent.

Finding Template

## [CRITICAL] Unchecked from_address in L1 Handler

**Location**: `src/bridge.cairo:145-155` (handle_deposit function)

**Description**:
The `handle_deposit` L1 handler function does not validate the `from_address` parameter. Any L1 contract can send messages to this function and mint tokens for arbitrary users, bypassing the intended L1 bridge access controls.

**Vulnerable Code**:
```rust
// bridge.cairo, line 145
#[l1_handler]
fn handle_deposit(
    ref self: ContractState,
    from_address: felt252,  // Not validated!
    user: ContractAddress,
    amount: u256
) {
    let current_balance = self.balances.read(user);
    self.balances.write(user, current_balance + amount);
}
```

**Attack Scenario**:
1. Attacker deploys malicious L1 contract
2. Malicious contract calls `starknetCore.sendMessageToL2(l2Contract, selector, [attacker_address, 1000000])`
3. L2 handler processes message without checking sender
4. Attacker receives 1,000,000 tokens without depositing any funds
5. Protocol suffers infinite mint vulnerability

**Recommendation**:
Validate `from_address` against authorized L1 bridge:
```rust
#[l1_handler]
fn handle_deposit(
    ref self: ContractState,
    from_address: felt252,
    user: ContractAddress,
    amount: u256
) {
    // Validate L1 sender
    let authorized_l1_bridge = self.l1_bridge_address.read();
    assert(from_address == authorized_l1_bridge, 'Unauthorized L1 sender');

    let current_balance = self.balances.read(user);
    self.balances.write(user, current_balance + amount);
}
```

**References**:
- building-secure-contracts/not-so-smart-contracts/cairo/unchecked_l1_handler_from
- Caracal detector: `unchecked-l1-handler-from`

9. Priority Guidelines

Critical (Immediate Fix Required)

  • Unchecked from_address in L1 handlers (infinite mint)
  • L1-L2 address conversion issues (funds to zero address)

High (Fix Before Deployment)

  • Felt252 arithmetic overflow/underflow (balance manipulation)
  • Missing signature replay protection (replay attacks)
  • L1-L2 message failure without cancellation (locked funds)

Medium (Address in Audit)

  • Overconstrained L1-L2 interactions (trapped funds)

10. Testing Recommendations

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_felt252_overflow() {
        // Test arithmetic edge cases
    }

    #[test]
    #[should_panic]
    fn test_unauthorized_l1_handler() {
        // Wrong from_address should fail
    }

    #[test]
    fn test_signature_replay_protection() {
        // Same signature twice should fail
    }
}

Integration Tests (with L1)

// Test full L1-L2 flow
#[test]
fn test_deposit_withdraw_roundtrip() {
    // 1. Deposit on L1
    // 2. Wait for L2 processing
    // 3. Verify L2 balance
    // 4. Withdraw to L1
    // 5. Verify L1 balance restored
}

Caracal CI Integration

# .github/workflows/security.yml
- name: Run Caracal
  run: |
    # Rebuilds from source each run; cache ~/.cargo or pin a release binary instead.
    cargo install --git https://github.com/crytic/caracal --profile release --force
    caracal detect src/ --fail-on high,critical

11. Additional Resources


12. Quick Reference Checklist

Before completing Cairo/StarkNet audit:

Arithmetic Safety (HIGH):

  • No felt252 used for balances/amounts (use u128/u256)
  • OR felt252 arithmetic has explicit bounds checking
  • Overflow/underflow scenarios tested

L1 Handler Security (CRITICAL):

  • ALL #[l1_handler] functions validate from_address
  • from_address compared against stored L1 contract address
  • Cannot bypass by deploying alternate L1 contract

L1-L2 Messaging (HIGH):

  • L1 bridge validates addresses < STARKNET_FIELD_PRIME
  • L1 bridge implements message cancellation
  • L2 handlers check from_address
  • Symmetric validation rules L1 ↔ L2
  • Full roundtrip flows tested

Signature Security (HIGH):

  • Signatures include nonce tracking
  • Nonce incremented after each use
  • Domain separator includes chain ID and contract address
  • Signature replay tested and prevented
  • Cross-chain replay prevented

Tool Usage:

  • Caracal scan completed with no critical findings
  • Unit tests cover all vulnerability scenarios
  • Integration tests verify L1-L2 flows
  • Testnet deployment tested before mainnet
  • Coverage table emitted with all 6 rows, each carrying a verdict of found, clear or n/a with a reason

13. Rationalizations to Reject

  • "The contract is small, so most patterns obviously don't apply." Obvious to whom? An n/a costs one clause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from not having checked.
  • "Caracal reported nothing, so the contract is clean." Caracal covers a subset of these 6 patterns and does not reach the logic-level ones at all. A clean tool run is one row of evidence, not a verdict on the patterns it never examined. Say which patterns it covered.
  • "I checked the patterns that matter for this contract." Deciding which patterns matter is the scan, not a precondition for starting it. Rank by severity after the table is complete, not by leaving rows out.
  • "No findings, so there is nothing to report." A zero-finding scan still emits the full coverage table. That table is the deliverable: it is what distinguishes a contract that was examined from one that was glanced at.
  • "Cairo 1 has native overflow checks, so arithmetic is safe." Name the types. felt252 does not behave like the sized integer types, and the boundary between them is where pattern 4 lives.
  • "The L1 side validates that." Then cite the L1 contract. A check you believe exists across the bridge is an assumption until you have read it, and unverified from_address is the canonical StarkNet bridge bug.
Files (skills)
  • agents
    • openai.yaml 259 B
      interface:
        display_name: "Cairo Vulnerability Scanner"
        short_description: "Find security vulnerabilities in Cairo and StarkNet contracts"
        icon_small: "assets/trail-of-bits-mark.svg"
        icon_large: "assets/trail-of-bits-mark.svg"
        brand_color: "#D83A34"
      
  • assets
    • trail-of-bits-mark.svg 3 KB · in bundle
  • resources
    • VULNERABILITY_PATTERNS.md 20.8 KB
      ## 6. Vulnerability Checklist (6 Patterns)
      
      ### 6.1 FELT252 ARITHMETIC OVERFLOW/UNDERFLOW ⚠️ HIGH
      
      **Description**: The `felt252` type in Cairo represents field elements in range [0, P] where P is the StarkNet prime. Unchecked arithmetic can overflow (wrapping to 0) or underflow (wrapping to P-1), similar to unsigned integers.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: Direct arithmetic on felt252 without bounds checking
      #[external(v0)]
      fn transfer(ref self: ContractState, to: ContractAddress, amount: felt252) {
          let sender = get_caller_address();
          let mut sender_balance = self.balances.read(sender);
      
          // OVERFLOW/UNDERFLOW: No bounds checking!
          sender_balance = sender_balance - amount;  // Can underflow to ~P
          sender_balance = sender_balance + amount;  // Can overflow past P
      
          self.balances.write(sender, sender_balance);
      }
      
      // VULNERABLE: felt252 arithmetic in calculations
      let reward = base_reward * multiplier;  // Can overflow
      let remaining = total - claimed;  // Can underflow
      ```
      
      **What to Check**:
      - [ ] Direct arithmetic on `felt252` type without overflow/underflow checks
      - [ ] Balance updates using felt252
      - [ ] Reward/fee calculations using felt252
      - [ ] No explicit validation that values stay within expected range
      - [ ] Consider using default integer types (u8, u16, u32, u64, u128, u256)
      
      **Mitigation**:
      ```rust
      // OPTION 1: Use default integer types (RECOMMENDED)
      #[storage]
      struct Storage {
          // Use u128 or u256 instead of felt252 for balances
          balances: LegacyMap<ContractAddress, u128>,
      }
      
      #[external(v0)]
      fn transfer(ref self: ContractState, to: ContractAddress, amount: u128) {
          let sender = get_caller_address();
          let sender_balance = self.balances.read(sender);
      
          // u128 has built-in overflow/underflow protection
          assert(sender_balance >= amount, 'Insufficient balance');
      
          self.balances.write(sender, sender_balance - amount);
          self.balances.write(to, self.balances.read(to) + amount);
      }
      
      // OPTION 2: Explicit checks with felt252
      #[external(v0)]
      fn transfer_felt(ref self: ContractState, to: ContractAddress, amount: felt252) {
          let sender = get_caller_address();
          let sender_balance = self.balances_felt.read(sender);
      
          // Explicit overflow/underflow checks
          assert(sender_balance >= amount, 'Insufficient balance');
      
          let new_sender_balance = sender_balance - amount;
          let recipient_balance = self.balances_felt.read(to);
          let new_recipient_balance = recipient_balance + amount;
      
          // Verify no overflow occurred
          assert(new_recipient_balance >= recipient_balance, 'Overflow');
      
          self.balances_felt.write(sender, new_sender_balance);
          self.balances_felt.write(to, new_recipient_balance);
      }
      
      // OPTION 3: SafeMath-style library (if available)
      use openzeppelin::security::safemath::SafeMath;
      
      let safe_result = SafeMath::add(balance, amount)?;
      ```
      
      **Tool Detection**:
      - Caracal detector: `unchecked-felt252-arithmetic`
      - Look for: felt252 used in balances, arithmetic operations without bounds checks
      
      **References**: building-secure-contracts/not-so-smart-contracts/cairo/arithmetic_overflow
      
      ---
      
      ### 4.2 L1 TO L2 ADDRESS CONVERSION ⚠️ HIGH
      
      **Description**: Ethereum L1 addresses are uint256 (256 bits), but StarkNet addresses are felt252 with range [0, P] where P < 2^256. L1 addresses >= P will map to zero address or unexpected address on L2.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: L1 contract doesn't validate address range
      // L1 Bridge contract (Solidity)
      function depositToL2(address l2Recipient, uint256 amount) external {
          // WRONG: No check that l2Recipient < STARKNET_FIELD_PRIME
          uint256[] memory payload = new uint256[](2);
          payload[0] = uint256(uint160(l2Recipient));  // Direct conversion!
          payload[1] = amount;
      
          starknetCore.sendMessageToL2(l2Contract, selector, payload);
      }
      
      // VULNERABLE: L2 handler assumes address is valid
      // L2 Contract (Cairo)
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,
          user: ContractAddress,  // Could be zero if L1 address >= P!
          amount: u256
      ) {
          // No validation that user != 0
          let current_balance = self.balances.read(user);
          self.balances.write(user, current_balance + amount);
      }
      ```
      
      **What to Check**:
      - [ ] L1 bridge contracts validate addresses before sending to L2
      - [ ] Validation: `0 < address < STARKNET_FIELD_PRIME`
      - [ ] L2 contracts check that addresses != 0
      - [ ] Documentation warns about address conversion issues
      
      **Mitigation**:
      ```solidity
      // SECURE: L1 Bridge with address validation (Solidity)
      contract L1Bridge {
          // StarkNet field prime (approximately)
          uint256 constant STARKNET_FIELD_PRIME =
              0x0800000000000011000000000000000000000000000000000000000000000001;
      
          function depositToL2(uint256 l2Recipient, uint256 amount) external {
              // Validate L2 address is within valid range
              require(l2Recipient != 0, "Zero address");
              require(l2Recipient < STARKNET_FIELD_PRIME, "Address out of range");
      
              uint256[] memory payload = new uint256[](2);
              payload[0] = l2Recipient;
              payload[1] = amount;
      
              starknetCore.sendMessageToL2(l2Contract, selector, payload);
          }
      }
      ```
      
      ```rust
      // SECURE: L2 handler with validation (Cairo)
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,
          user: ContractAddress,
          amount: u256
      ) {
          // Validate user address is not zero
          let zero_address: ContractAddress = 0.try_into().unwrap();
          assert(user != zero_address, 'Invalid user address');
      
          // Validate L1 sender is authorized bridge
          let expected_l1_bridge: felt252 = self.l1_bridge_address.read();
          assert(from_address == expected_l1_bridge, 'Unauthorized L1 sender');
      
          let current_balance = self.balances.read(user);
          self.balances.write(user, current_balance + amount);
      }
      ```
      
      **Testing**:
      ```rust
      // Test with edge case addresses
      #[test]
      fn test_large_address_conversion() {
          // Address close to STARKNET_FIELD_PRIME
          let large_address = STARKNET_FIELD_PRIME - 1;
          // Should handle correctly
      
          let invalid_address = STARKNET_FIELD_PRIME + 1;
          // Should reject on L1
      }
      ```
      
      **References**: building-secure-contracts/not-so-smart-contracts/cairo/l1_to_l2_address_conversion
      
      ---
      
      ### 4.3 L1 TO L2 MESSAGE FAILURE ⚠️ HIGH
      
      **Description**: Messages from L1 to L2 may not be processed by the sequencer (due to gas spikes, congestion, etc.). Without a cancellation mechanism, funds sent via failed messages are locked permanently.
      
      **Detection Patterns**:
      ```solidity
      // VULNERABLE: L1 bridge without cancellation mechanism
      contract L1Bridge {
          function depositToL2(uint256 l2Recipient, uint256 amount) external payable {
              // Lock funds on L1
              require(msg.value == amount, "Incorrect amount");
      
              // Send message to L2
              starknetCore.sendMessageToL2(l2Contract, selector, payload);
      
              // PROBLEM: If message fails to process on L2, funds locked forever!
              // No way to cancel and return funds to user
          }
      }
      ```
      
      **What to Check**:
      - [ ] L1 bridge implements message cancellation mechanism
      - [ ] Uses `startL1ToL2MessageCancellation` and `cancelL1ToL2Message`
      - [ ] Cancellation delay period documented
      - [ ] Users can recover funds if L2 message not processed
      
      **Mitigation**:
      ```solidity
      // SECURE: L1 Bridge with cancellation support
      contract L1Bridge {
          IStarknetCore public starknetCore;
          uint256 public constant MESSAGE_CANCEL_DELAY = 5 days;
      
          // Track message hashes for cancellation
          mapping(bytes32 => address) public messageOwner;
      
          function depositToL2(
              uint256 l2Contract,
              uint256 l2Recipient,
              uint256 amount
          ) external payable returns (bytes32 msgHash) {
              require(msg.value == amount, "Incorrect amount");
      
              uint256[] memory payload = new uint256[](2);
              payload[0] = l2Recipient;
              payload[1] = amount;
      
              // Send message and get hash
              msgHash = starknetCore.sendMessageToL2{value: msg.value}(
                  l2Contract,
                  DEPOSIT_SELECTOR,
                  payload
              );
      
              // Track message owner for cancellation
              messageOwner[msgHash] = msg.sender;
      
              emit DepositInitiated(msg.sender, l2Recipient, amount, msgHash);
              return msgHash;
          }
      
          function startCancellation(bytes32 msgHash, uint256 nonce) external {
              require(messageOwner[msgHash] == msg.sender, "Not message owner");
      
              uint256[] memory payload = new uint256[](2);
              payload[0] = uint256(uint160(msg.sender));
              payload[1] = msg.value;
      
              // Start cancellation process
              starknetCore.startL1ToL2MessageCancellation(
                  l2Contract,
                  DEPOSIT_SELECTOR,
                  payload,
                  nonce
              );
      
              emit CancellationStarted(msgHash, msg.sender);
          }
      
          function completeCancellation(
              bytes32 msgHash,
              uint256 nonce
          ) external {
              require(messageOwner[msgHash] == msg.sender, "Not message owner");
      
              uint256[] memory payload = new uint256[](2);
              payload[0] = uint256(uint160(msg.sender));
              payload[1] = msg.value;
      
              // Complete cancellation (after delay)
              starknetCore.cancelL1ToL2Message(
                  l2Contract,
                  DEPOSIT_SELECTOR,
                  payload,
                  nonce
              );
      
              // Return funds to user
              (bool success, ) = msg.sender.call{value: msg.value}("");
              require(success, "Transfer failed");
      
              delete messageOwner[msgHash];
      
              emit DepositCancelled(msgHash, msg.sender);
          }
      }
      ```
      
      **L2 Considerations**:
      ```rust
      // L2 contract should handle idempotent deposits
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,
          user: ContractAddress,
          amount: u256
      ) {
          // If message is replayed after cancellation attempt,
          // should handle gracefully (idempotency)
      }
      ```
      
      **References**: building-secure-contracts/not-so-smart-contracts/cairo/l1_to_l2_message_failure
      
      ---
      
      ### 4.4 OVERCONSTRAINED L1 <-> L2 INTERACTION ⚠️ MEDIUM
      
      **Description**: Asymmetrical validation on L1 vs L2 sides creates situations where funds can be deposited but not withdrawn, or vice versa. Different access control rules trap user funds.
      
      **Detection Patterns**:
      ```solidity
      // VULNERABLE: L1 has whitelist, L2 doesn't
      contract L1Bridge {
          mapping(address => bool) public whitelist;
      
          function depositToL2(uint256 l2Recipient, uint256 amount) external {
              require(whitelist[msg.sender], "Not whitelisted");  // Whitelist on L1
              // Send to L2
          }
      
          function withdrawFromL2(uint256 amount) external {
              // No whitelist check - anyone can withdraw!
          }
      }
      ```
      
      ```rust
      // VULNERABLE: L2 allows deposits but L1 blocks withdrawals
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,
          user: ContractAddress,
          amount: u256
      ) {
          // No restrictions - accepts from any L1 address
          self.balances.write(user, self.balances.read(user) + amount);
      }
      
      #[external(v0)]
      fn initiate_withdrawal(ref self: ContractState, amount: u256) {
          // User can initiate withdrawal on L2
          send_message_to_l1_syscall(to_address, payload);
      }
      
      // But L1 contract has restrictions preventing withdrawal completion!
      ```
      
      **What to Check**:
      - [ ] Same validation rules on both L1 and L2
      - [ ] If L1 has whitelist, L2 should too (or vice versa)
      - [ ] If L1 has blacklist, L2 should too
      - [ ] Access controls symmetric across layers
      - [ ] Test full roundtrip: deposit on L1 → withdraw on L1
      
      **Mitigation**:
      ```solidity
      // SECURE: Symmetric validation on L1
      contract L1Bridge {
          mapping(address => bool) public blockedUsers;
      
          function depositToL2(uint256 l2Recipient, uint256 amount) external {
              require(!blockedUsers[msg.sender], "User blocked");
              // Process deposit
          }
      
          function finalizeWithdrawal(uint256 recipient, uint256 amount) external {
              // Validate L2 message
              require(!blockedUsers[msg.sender], "User blocked");
              // Process withdrawal
          }
      }
      ```
      
      ```rust
      // SECURE: Symmetric validation on L2
      #[storage]
      struct Storage {
          blocked_users: LegacyMap<ContractAddress, bool>,
      }
      
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,
          user: ContractAddress,
          amount: u256
      ) {
          // Same validation as L1
          assert(!self.blocked_users.read(user), 'User blocked');
          self.balances.write(user, self.balances.read(user) + amount);
      }
      
      #[external(v0)]
      fn initiate_withdrawal(ref self: ContractState, amount: u256) {
          let user = get_caller_address();
      
          // Same validation as L1
          assert(!self.blocked_users.read(user), 'User blocked');
      
          // Process withdrawal
      }
      ```
      
      **Testing Strategy**:
      ```rust
      // Test full roundtrip scenarios
      #[test]
      fn test_deposit_and_withdraw_roundtrip() {
          // 1. Deposit on L1
          // 2. Verify balance on L2
          // 3. Initiate withdrawal on L2
          // 4. Finalize withdrawal on L1
          // 5. Verify funds returned
      
          // Should succeed for all valid users
      }
      
      #[test]
      fn test_blocked_user_cannot_roundtrip() {
          // Blocked user should fail at EVERY step
          // Not just one side
      }
      ```
      
      **References**: building-secure-contracts/not-so-smart-contracts/cairo/l1_l2_overconstrained
      
      ---
      
      ### 4.5 SIGNATURE REPLAY PROTECTION ⚠️ HIGH
      
      **Description**: Signatures without nonce tracking and domain separation can be replayed: same signature used multiple times on the same chain, or across different chains (mainnet/testnet).
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: No nonce tracking
      #[external(v0)]
      fn execute_with_signature(
          ref self: ContractState,
          to: ContractAddress,
          amount: u128,
          signature: Array<felt252>
      ) {
          // Verify signature
          let message_hash = hash_message(to, amount);
          let signer = recover_signer(message_hash, signature);
      
          // WRONG: No nonce! Same signature can be replayed infinitely
          self.transfer_internal(signer, to, amount);
      }
      
      // VULNERABLE: No chain ID in signature
      fn hash_message(to: ContractAddress, amount: u128) -> felt252 {
          // Missing chain ID - signature valid on mainnet AND testnet!
          pedersen_hash(to.into(), amount.into())
      }
      ```
      
      **What to Check**:
      - [ ] Signatures include nonce that is incremented after each use
      - [ ] Nonce stored per signer address
      - [ ] Signatures include domain separator (chain ID, contract address)
      - [ ] Signature hash includes all relevant parameters
      - [ ] Cannot replay same signature twice
      - [ ] Cannot replay signature from testnet on mainnet
      
      **Mitigation**:
      ```rust
      // SECURE: Proper signature with nonce and domain separator
      use openzeppelin::account::interface::ISRC6;
      
      #[storage]
      struct Storage {
          nonces: LegacyMap<ContractAddress, felt252>,
          // Other storage
      }
      
      #[external(v0)]
      fn execute_with_signature(
          ref self: ContractState,
          to: ContractAddress,
          amount: u128,
          nonce: felt252,
          signature: Array<felt252>
      ) {
          // Get signer's current nonce
          let signer = get_caller_address();
          let current_nonce = self.nonces.read(signer);
      
          // Verify nonce matches
          assert(nonce == current_nonce, 'Invalid nonce');
      
          // Build message hash with domain separator
          let message_hash = self.hash_message_with_domain(
              signer,
              to,
              amount,
              nonce
          );
      
          // Verify signature
          let is_valid = self.verify_signature(signer, message_hash, signature);
          assert(is_valid, 'Invalid signature');
      
          // Increment nonce BEFORE execution (reentrancy protection)
          self.nonces.write(signer, current_nonce + 1);
      
          // Execute operation
          self.transfer_internal(signer, to, amount);
      }
      
      fn hash_message_with_domain(
          self: @ContractState,
          signer: ContractAddress,
          to: ContractAddress,
          amount: u128,
          nonce: felt252
      ) -> felt252 {
          // Include domain separator for replay protection
          let domain_separator = self.get_domain_separator();
      
          // Hash all parameters including domain and nonce
          let mut message = array![
              domain_separator,
              signer.into(),
              to.into(),
              amount.into(),
              nonce
          ];
      
          poseidon_hash_span(message.span())
      }
      
      fn get_domain_separator(self: @ContractState) -> felt252 {
          // EIP-712 style domain separator
          let chain_id = get_tx_info().unbox().chain_id;
          let contract_address = get_contract_address();
      
          poseidon_hash_span(
              array!['StarkNet Domain', chain_id, contract_address.into()].span()
          )
      }
      ```
      
      **Using OpenZeppelin**:
      ```rust
      // BETTER: Use OpenZeppelin Account implementation
      use openzeppelin::account::Account;
      use openzeppelin::account::interface::AccountABIDispatcher;
      
      // OpenZeppelin Account automatically handles:
      // - Nonce management
      // - Domain separation
      // - Signature verification
      // - Replay protection
      ```
      
      **Testing**:
      ```rust
      #[test]
      #[should_panic(expected: ('Invalid nonce',))]
      fn test_cannot_replay_signature() {
          // Execute with signature once
          contract.execute_with_signature(to, amount, nonce, signature);
      
          // Try to replay same signature - should fail
          contract.execute_with_signature(to, amount, nonce, signature);
      }
      
      #[test]
      #[should_panic]
      fn test_cannot_use_testnet_signature_on_mainnet() {
          // Generate signature on testnet
          let testnet_signature = sign_on_testnet(message);
      
          // Try to use on mainnet - should fail due to domain separator
          mainnet_contract.execute_with_signature(..., testnet_signature);
      }
      ```
      
      **Tool Detection**:
      - Caracal detector: `missing-nonce-validation`
      - Look for: Signature verification without nonce checks
      
      **References**: building-secure-contracts/not-so-smart-contracts/cairo/signature_replay
      
      ---
      
      ### 4.6 UNCHECKED from_address IN L1 HANDLER ⚠️ CRITICAL
      
      **Description**: L1 handler functions (`#[l1_handler]`) can be invoked by any L1 contract unless `from_address` is validated. Missing validation allows unauthorized L1 contracts to send messages.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: No from_address validation
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,  // Not validated!
          user: ContractAddress,
          amount: u256
      ) {
          // WRONG: Any L1 contract can call this and mint tokens!
          let current_balance = self.balances.read(user);
          self.balances.write(user, current_balance + amount);
      }
      
      // VULNERABLE: Only validates on L1 side
      // L1 Contract has access control
      function depositToL2() external onlyAuthorized {
          starknetCore.sendMessageToL2(...);
      }
      
      // But L2 doesn't check - attacker deploys their own L1 contract!
      #[l1_handler]
      fn handle_deposit(ref self: ContractState, from_address: felt252, ...) {
          // No check that from_address == AUTHORIZED_L1_CONTRACT
      }
      ```
      
      **What to Check**:
      - [ ] ALL `#[l1_handler]` functions validate `from_address`
      - [ ] Validation: `from_address == expected_l1_contract_address`
      - [ ] Expected L1 addresses stored in contract storage
      - [ ] Cannot skip validation even if L1 contract has access control
      
      **Mitigation**:
      ```rust
      // SECURE: Validate from_address in L1 handler
      #[storage]
      struct Storage {
          l1_bridge_address: felt252,
          balances: LegacyMap<ContractAddress, u256>,
      }
      
      #[constructor]
      fn constructor(ref self: ContractState, l1_bridge: felt252) {
          self.l1_bridge_address.write(l1_bridge);
      }
      
      #[l1_handler]
      fn handle_deposit(
          ref self: ContractState,
          from_address: felt252,
          user: ContractAddress,
          amount: u256
      ) {
          // CRITICAL: Validate from_address is authorized L1 bridge
          let authorized_l1_bridge = self.l1_bridge_address.read();
          assert(from_address == authorized_l1_bridge, 'Unauthorized L1 sender');
      
          // Validate user address
          let zero_address: ContractAddress = 0.try_into().unwrap();
          assert(user != zero_address, 'Invalid user address');
      
          // Now safe to process deposit
          let current_balance = self.balances.read(user);
          self.balances.write(user, current_balance + amount);
      
          // Emit event
          self.emit(DepositProcessed { user, amount, from_l1: from_address });
      }
      
      // Admin function to update L1 bridge (if needed)
      #[external(v0)]
      fn update_l1_bridge(ref self: ContractState, new_l1_bridge: felt252) {
          // Access control
          self.ownable.assert_only_owner();
      
          self.l1_bridge_address.write(new_l1_bridge);
          self.emit(L1BridgeUpdated { new_address: new_l1_bridge });
      }
      ```
      
      **Testing**:
      ```rust
      #[test]
      #[should_panic(expected: ('Unauthorized L1 sender',))]
      fn test_unauthorized_l1_sender_rejected() {
          let unauthorized_address = 0x123; // Wrong L1 address
      
          // Should reject
          contract.handle_deposit(
              from_address: unauthorized_address,
              user: user_address,
              amount: 1000
          );
      }
      
      #[test]
      fn test_authorized_l1_sender_accepted() {
          let authorized_address = l1_bridge_address;
      
          // Should succeed
          contract.handle_deposit(
              from_address: authorized_address,
              user: user_address,
              amount: 1000
          );
      
          assert(contract.balances(user_address) == 1000);
      }
      ```
      
      **Tool Detection**:
      - Caracal detector: `unchecked-l1-handler-from`
      - Look for: `#[l1_handler]` without `from_address` validation
      
      **References**: building-secure-contracts/not-so-smart-contracts/cairo/unchecked_l1_handler_from
      
      ---
      
  • SKILL.md 12.8 KB
    ---
    name: cairo-vulnerability-scanner
    description: Scans Cairo/StarkNet smart contracts for 6 critical vulnerabilities including felt252 arithmetic overflow, L1-L2 messaging issues, address conversion problems, and signature replay. Use when auditing StarkNet projects.
    ---
    
    # Cairo/StarkNet Vulnerability Scanner
    
    ## 1. Purpose
    
    Systematically scan Cairo smart contracts on StarkNet for platform-specific security vulnerabilities related to arithmetic, cross-layer messaging, and cryptographic operations. This skill encodes 6 critical vulnerability patterns unique to Cairo/StarkNet ecosystem.
    
    ## 2. When to Use This Skill
    
    - Auditing StarkNet smart contracts (Cairo)
    - Reviewing L1-L2 bridge implementations
    - Pre-launch security assessment of StarkNet applications
    - Validating cross-layer message handling
    - Reviewing signature verification logic
    - Assessing L1 handler functions
    
    ## 3. Platform Detection
    
    ### File Extensions & Indicators
    - **Cairo files**: `.cairo`
    
    ### Language/Framework Markers
    ```rust
    // Cairo contract indicators
    #[contract]
    mod MyContract {
        use starknet::ContractAddress;
    
        #[storage]
        struct Storage {
            balance: LegacyMap<ContractAddress, felt252>,
        }
    
        #[external(v0)]
        fn transfer(ref self: ContractState, to: ContractAddress, amount: felt252) {
            // Contract logic
        }
    
        #[l1_handler]
        fn handle_deposit(ref self: ContractState, from_address: felt252, amount: u256) {
            // L1 message handler
        }
    }
    
    // Common patterns
    felt252, u128, u256
    ContractAddress, EthAddress
    #[external(v0)], #[l1_handler], #[constructor]
    get_caller_address(), get_contract_address()
    send_message_to_l1_syscall
    ```
    
    ### Project Structure
    - `src/contract.cairo` - Main contract implementation
    - `src/lib.cairo` - Library modules
    - `tests/` - Contract tests
    - `Scarb.toml` - Cairo project configuration
    
    ### Tool Support
    - **Caracal**: Trail of Bits static analyzer for Cairo
    - Installation: `cargo install --git https://github.com/crytic/caracal --profile release --force` (a Rust tool — not on PyPI)
    - Usage: `caracal detect src/`
    - **cairo-test**: Built-in testing framework
    - **Starknet Foundry**: Testing and development toolkit
    
    ---
    
    ## 4. How This Skill Works
    
    When invoked, I will:
    
    1. **Search your codebase** for Cairo files
    2. **Analyze each contract** for the 6 vulnerability patterns
    3. **Report findings** with file references and severity, above them a coverage table carrying a verdict for every pattern
    4. **Provide fixes** for each identified issue
    5. **Check L1-L2 interactions** for messaging vulnerabilities
    
    ---
    
    ## 5. Example Output
    
    When vulnerabilities are found, you'll get a report like this:
    
    ```
    === CAIRO/STARKNET VULNERABILITY SCAN RESULTS ===
    ```
    
    ---
    
    ## 6. Vulnerability Patterns (6 Patterns)
    
    I check for 6 critical vulnerability patterns unique to Cairo/Starknet. For detailed detection patterns, code examples, mitigations, and testing strategies, see [VULNERABILITY_PATTERNS.md](resources/VULNERABILITY_PATTERNS.md).
    
    ### Pattern Summary:
    
    1. **Felt252 Arithmetic Overflow/Underflow** ⚠️ HIGH - `felt252` wraps silently; use `u128`/`u256`
    2. **L1 to L2 Address Conversion** ⚠️ HIGH - L1 address not validated against STARKNET_FIELD_PRIME
    3. **L1 to L2 Message Failure** ⚠️ HIGH - No cancellation path when a message cannot be consumed
    4. **Overconstrained L1 <-> L2 Interaction** ⚠️ MEDIUM - Coupling that can strand funds or block progress
    5. **Signature Replay Protection** ⚠️ HIGH - No nonce, or a domain separator missing chain/contract
    6. **Unchecked from_address in L1 Handler** ⚠️ CRITICAL - Any L1 contract can drive the handler
    
    For complete vulnerability patterns with code examples, see [VULNERABILITY_PATTERNS.md](resources/VULNERABILITY_PATTERNS.md).
    
    ## 7. Scanning Workflow
    
    ### Step 1: Platform Identification
    1. Verify Cairo language and StarkNet framework
    2. Check Cairo version (Cairo 1.0+ vs legacy Cairo 0)
    3. Locate contract files (`src/*.cairo`)
    4. Identify L1-L2 bridge contracts (if applicable)
    
    ### Step 2: Arithmetic Safety Sweep
    ```bash
    # Find felt252 usage in arithmetic
    rg "felt252" src/ | rg "[-+*/]"
    
    # Find balance/amount storage using felt252
    rg "felt252" src/ | rg "balance|amount|total|supply"
    
    # Should prefer u128, u256 instead
    ```
    
    ### Step 3: L1 Handler Analysis
    For each `#[l1_handler]` function:
    - [ ] Validates `from_address` parameter
    - [ ] Checks address != zero
    - [ ] Has proper access control
    - [ ] Emits events for monitoring
    
    ### Step 4: Signature Verification Review
    For signature-based functions:
    - [ ] Includes nonce tracking
    - [ ] Nonce incremented after use
    - [ ] Domain separator includes chain ID and contract address
    - [ ] Cannot replay signatures
    
    ### Step 5: L1-L2 Bridge Audit
    If contract includes bridge functionality:
    - [ ] L1 validates address < STARKNET_FIELD_PRIME
    - [ ] L1 implements message cancellation
    - [ ] L2 validates from_address in handlers
    - [ ] Symmetric access controls L1 ↔ L2
    - [ ] Test full roundtrip flows
    
    ### Step 6: Static Analysis with Caracal
    ```bash
    # Run Caracal detectors
    caracal detect src/
    
    # Specific detectors
    caracal detect src/ --detectors unchecked-felt252-arithmetic
    caracal detect src/ --detectors unchecked-l1-handler-from
    caracal detect src/ --detectors missing-nonce-validation
    ```
    
    ---
    
    ## 8. Reporting Format
    
    ### Coverage Table
    
    Report on every pattern in §6, whether or not it turned anything up. Emit this table above the findings, with
    all 6 rows present:
    
    | # | Pattern | Verdict | Evidence |
    |---|---------|---------|----------|
    | 1 | Felt252 Arithmetic Overflow/Underflow | `clear` | balances are `u256`; searched `felt252` in arithmetic, none |
    | 2 | L1 to L2 Address Conversion | | |
    | 3 | L1 to L2 Message Failure | | |
    | 4 | Overconstrained L1 <-> L2 Interaction | | |
    | 5 | Signature Replay Protection | | |
    | 6 | Unchecked from_address in L1 Handler | | |
    
    Each verdict is one of:
    
    - **`found`** — cite `file:line` and write the finding up in full below.
    - **`clear`** — the pattern applies to this contract and the contract handles it. Name the function, trait, or
      check you searched for, so a reader can repeat the search.
    - **`n/a`** — the pattern cannot apply here. Give the reason in one clause ("no L1 handlers in this contract").
      Not having looked is not `n/a`.
    
    A table with fewer than 6 rows is an incomplete scan and must be reported as one. A row whose Verdict cell is empty is incomplete in the same way: row 1 above is filled in to show the shape, and every row is filled in the same way before the report is done. Six `clear` verdicts is a
    result a reader can act on. A report that covers two patterns and says nothing about the other four reads
    exactly like a clean contract, and that is the failure this table exists to prevent.
    
    ### Finding Template
    ````markdown
    ## [CRITICAL] Unchecked from_address in L1 Handler
    
    **Location**: `src/bridge.cairo:145-155` (handle_deposit function)
    
    **Description**:
    The `handle_deposit` L1 handler function does not validate the `from_address` parameter. Any L1 contract can send messages to this function and mint tokens for arbitrary users, bypassing the intended L1 bridge access controls.
    
    **Vulnerable Code**:
    ```rust
    // bridge.cairo, line 145
    #[l1_handler]
    fn handle_deposit(
        ref self: ContractState,
        from_address: felt252,  // Not validated!
        user: ContractAddress,
        amount: u256
    ) {
        let current_balance = self.balances.read(user);
        self.balances.write(user, current_balance + amount);
    }
    ```
    
    **Attack Scenario**:
    1. Attacker deploys malicious L1 contract
    2. Malicious contract calls `starknetCore.sendMessageToL2(l2Contract, selector, [attacker_address, 1000000])`
    3. L2 handler processes message without checking sender
    4. Attacker receives 1,000,000 tokens without depositing any funds
    5. Protocol suffers infinite mint vulnerability
    
    **Recommendation**:
    Validate `from_address` against authorized L1 bridge:
    ```rust
    #[l1_handler]
    fn handle_deposit(
        ref self: ContractState,
        from_address: felt252,
        user: ContractAddress,
        amount: u256
    ) {
        // Validate L1 sender
        let authorized_l1_bridge = self.l1_bridge_address.read();
        assert(from_address == authorized_l1_bridge, 'Unauthorized L1 sender');
    
        let current_balance = self.balances.read(user);
        self.balances.write(user, current_balance + amount);
    }
    ```
    
    **References**:
    - building-secure-contracts/not-so-smart-contracts/cairo/unchecked_l1_handler_from
    - Caracal detector: `unchecked-l1-handler-from`
    ````
    
    ---
    
    ## 9. Priority Guidelines
    
    ### Critical (Immediate Fix Required)
    - Unchecked from_address in L1 handlers (infinite mint)
    - L1-L2 address conversion issues (funds to zero address)
    
    ### High (Fix Before Deployment)
    - Felt252 arithmetic overflow/underflow (balance manipulation)
    - Missing signature replay protection (replay attacks)
    - L1-L2 message failure without cancellation (locked funds)
    
    ### Medium (Address in Audit)
    - Overconstrained L1-L2 interactions (trapped funds)
    
    ---
    
    ## 10. Testing Recommendations
    
    ### Unit Tests
    ```rust
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_felt252_overflow() {
            // Test arithmetic edge cases
        }
    
        #[test]
        #[should_panic]
        fn test_unauthorized_l1_handler() {
            // Wrong from_address should fail
        }
    
        #[test]
        fn test_signature_replay_protection() {
            // Same signature twice should fail
        }
    }
    ```
    
    ### Integration Tests (with L1)
    ```rust
    // Test full L1-L2 flow
    #[test]
    fn test_deposit_withdraw_roundtrip() {
        // 1. Deposit on L1
        // 2. Wait for L2 processing
        // 3. Verify L2 balance
        // 4. Withdraw to L1
        // 5. Verify L1 balance restored
    }
    ```
    
    ### Caracal CI Integration
    ```yaml
    # .github/workflows/security.yml
    - name: Run Caracal
      run: |
        # Rebuilds from source each run; cache ~/.cargo or pin a release binary instead.
        cargo install --git https://github.com/crytic/caracal --profile release --force
        caracal detect src/ --fail-on high,critical
    ```
    
    ---
    
    ## 11. Additional Resources
    
    - **Building Secure Contracts**: `building-secure-contracts/not-so-smart-contracts/cairo/`
    - **Caracal**: https://github.com/crytic/caracal
    - **Cairo Documentation**: https://book.cairo-lang.org/
    - **StarkNet Documentation**: https://docs.starknet.io/
    - **OpenZeppelin Cairo Contracts**: https://github.com/OpenZeppelin/cairo-contracts
    
    ---
    
    ## 12. Quick Reference Checklist
    
    Before completing Cairo/StarkNet audit:
    
    **Arithmetic Safety (HIGH)**:
    - [ ] No felt252 used for balances/amounts (use u128/u256)
    - [ ] OR felt252 arithmetic has explicit bounds checking
    - [ ] Overflow/underflow scenarios tested
    
    **L1 Handler Security (CRITICAL)**:
    - [ ] ALL `#[l1_handler]` functions validate `from_address`
    - [ ] from_address compared against stored L1 contract address
    - [ ] Cannot bypass by deploying alternate L1 contract
    
    **L1-L2 Messaging (HIGH)**:
    - [ ] L1 bridge validates addresses < STARKNET_FIELD_PRIME
    - [ ] L1 bridge implements message cancellation
    - [ ] L2 handlers check from_address
    - [ ] Symmetric validation rules L1 ↔ L2
    - [ ] Full roundtrip flows tested
    
    **Signature Security (HIGH)**:
    - [ ] Signatures include nonce tracking
    - [ ] Nonce incremented after each use
    - [ ] Domain separator includes chain ID and contract address
    - [ ] Signature replay tested and prevented
    - [ ] Cross-chain replay prevented
    
    **Tool Usage**:
    - [ ] Caracal scan completed with no critical findings
    - [ ] Unit tests cover all vulnerability scenarios
    - [ ] Integration tests verify L1-L2 flows
    - [ ] Testnet deployment tested before mainnet
    - [ ] Coverage table emitted with all 6 rows, each carrying a verdict of `found`, `clear` or `n/a` with a reason
    
    ---
    
    ## 13. Rationalizations to Reject
    
    - **"The contract is small, so most patterns obviously don't apply."** Obvious to whom? An `n/a` costs one
      clause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from
      not having checked.
    - **"Caracal reported nothing, so the contract is clean."** Caracal covers a subset of these 6 patterns and
      does not reach the logic-level ones at all. A clean tool run is one row of evidence, not a verdict on the
      patterns it never examined. Say which patterns it covered.
    - **"I checked the patterns that matter for this contract."** Deciding which patterns matter *is* the scan,
      not a precondition for starting it. Rank by severity after the table is complete, not by leaving rows out.
    - **"No findings, so there is nothing to report."** A zero-finding scan still emits the full coverage table.
      That table is the deliverable: it is what distinguishes a contract that was examined from one that was
      glanced at.
    - **"Cairo 1 has native overflow checks, so arithmetic is safe."** Name the types. `felt252` does not behave
      like the sized integer types, and the boundary between them is where pattern 4 lives.
    - **"The L1 side validates that."** Then cite the L1 contract. A check you believe exists across the bridge is
      an assumption until you have read it, and unverified `from_address` is the canonical StarkNet bridge bug.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related