## 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

---
