Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Invalid

Incorrect Storage Slot Order in `Vault.sol::load()` Function

1. Summary

The load() function incorrectly computes storage slots for vaults using keccak256(abi.encode(VAULT_LOCATION, vaultId)), reversing the standard Solidity mapping slot order. This violates Solidity’s storage layout rules, creating risks of key collision and data corruption where multiple vaults/mappings could overwrite each other’s storage slots.


2. Vulnerability Details

Faulty Code

function load(uint128 vaultId) internal pure returns (Data storage vault) {
// Incorrect order: VAULT_LOCATION (slot) comes before vaultId (key)
bytes32 slot = keccak256(abi.encode(VAULT_LOCATION, vaultId));
assembly {
vault.slot := slot
}
}

Technical Breakdown

  1. Solidity Storage Rules:
    For mappings and dynamic storage, slots are calculated as:

    slot = keccak256(abi.encode(key, base_slot))
    • key: Unique identifier (e.g., vaultId)

    • base_slot: Precomputed namespace (e.g., VAULT_LOCATION)

  2. Incorrect Implementation:
    The code reverses the order:

    slot = keccak256(abi.encode(VAULT_LOCATION, vaultId)) // (base_slot, key)

    This causes:

    • All vaults to share a linear sequence of slots starting from keccak256(VAULT_LOCATION)

    • High probability of slot collisions between vaults with sequential IDs

  3. Collision Example:
    For VAULT_LOCATION = 0x123... and vaultId = 1:

    • Correct Slot: keccak256(abi.encode(1, 0x123...))

    • Actual Slot: keccak256(abi.encode(0x123..., 1))
      These produce entirely different (and non-isolated) storage locations.


3. Impact

Severity Consequences
Critical Cross-Vault Data Corruption: Vaults with different IDs may collide with unrelated storage areas

Mapping Overlap: Other mappings using keccak256(key, base_slot) could overwrite vault data
Protocol-Wide Instability: Financial accounting errors in debt/collateral tracking
Exploit Potential: Attackers could deliberately collide slots to manipulate balances

Example Scenario:

  1. Vault #1 uses slot keccak256(VAULT_LOCATION || 1)

  2. Another mapping uses slot keccak256(1 || OTHER_BASE_SLOT)

  3. If VAULT_LOCATION = OTHER_BASE_SLOT, these slots collide, causing silent data overwrites.


4. Recommendations

Immediate Fix

Correct the slot calculation order:

function load(uint128 vaultId) internal view returns (Data storage vault) {
bytes32 slot = keccak256(abi.encode(vaultId, VAULT_LOCATION)); // Key first!
assembly {
vault.slot := slot
}
}

Preventative Measures

  1. Storage Layout Tests:
    Add Foundry tests to verify slot isolation:

    function test_SlotIsolation() public {
    uint128 vaultA = 1;
    uint128 vaultB = 2;
    bytes32 slotA = keccak256(abi.encode(vaultA, VAULT_LOCATION));
    bytes32 slotB = keccak256(abi.encode(vaultB, VAULT_LOCATION));
    // Ensure slots are non-sequential and isolated
    assertTrue(uint256(slotB) - uint256(slotA) > 1);
    }
  2. Static Analysis Rule:
    Implement a Slither custom detector to flag reversed abi.encode orders in keccak256 calls:

    detectors:
    - name: "reversed-slot-order"
    description: "Detect keccak256(abi.encode(base_slot, key)) patterns"
    regex: "keccak256\(abi\.encode\(.*,\s*.*key.*\)\)"
  3. Documentation Update:
    Explicitly document storage slot conventions in the codebase:

    "For namespaced storage, always use keccak256(abi.encode(key, base_slot))."

Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.