agents/skills/evm/storage-layout-safety/SKILL.md
Type Thought-template (instantiate before use) - Trigger Pattern STORAGE_LAYOUT flag detected
npx skillsauth add plamentsv/plamen storage-layout-safetyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Type: Thought-template (instantiate before use) Trigger Pattern: STORAGE_LAYOUT flag detected Inject Into: depth-state-trace, depth-edge-case Finding prefix:
[SLS-N]Rules referenced: R1, R4, R8, R10, R14
Covers: memory vs storage confusion, lost writes, proxy/upgrade storage collisions, inline assembly slot safety, and storage semantic corruption.
This vulnerability class exists ONLY on EVM - type-safe VMs (Move, Solana's Borsh model) enforce layout correctness at the runtime level. EVM's untyped 256-bit slot model permits silent corruption when layouts diverge.
proxy|upgradeable|diamond|delegatecall|EIP1967|StorageSlot|
sstore|sload|assembly\s*\{|tstore|tload|reinitializer|
UUPSUpgradeable|TransparentUpgradeableProxy|BeaconProxy
Map the contract's persistent state surface before analyzing bugs:
| # | Variable | Type | Slot Assignment | Written By | Read By | Proxy-Relevant? | |---|----------|------|----------------|-----------|---------|-----------------|
For each state variable, determine:
bytes32 constant)?sstore/sload?keccak256(key . slot). For arrays: keccak256(slot) + index.Tag: [TRACE:variable={name} → slot={computation} → writers={functions}]
For each function operating on structs or complex types:
Trace every local variable of struct, array, or mapping type:
storage or memory?memory: is the function INTENDING to modify persistent state? If yes → lost write (copy modified in memory, never persisted).storage: does every code path that modifies the reference complete without early return before the write?For each function accepting struct/array parameters:
memory or calldata?function update(MyStruct memory s) modifies s.field but s is a memory copy - original unchanged.For libraries called via using ... for:
storage or memory references?Tag: [TRACE:function={name} → var={var} → location={memory/storage} → write_persisted={YES/NO}]
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)).For each upgrade path (V1 → V2):
__gap storage slots reserved? New variables consuming gap correctly?For EIP-2535 or namespaced storage:
Tag: [TRACE:proxy_slot={N} → impl_var={name} → collision={YES/NO}]
For each inline assembly block using sstore or sload:
sstore?sstore writes full 32 bytes - masking/shifting correct for packed slots?)If tstore/tload used:
sstore/sload? (transient cleared after tx, permanent is not)tstore instead of sstore?Tag: [BOUNDARY:user_input={MAX} → computed_slot={value} → target={what_gets_overwritten}]
Processing: ENUMERATE all calldataload/mload(add( sites with literal offsets + all byte-slicing with hardcoded N on dynamic-type data → PROCESS each against the criteria below → COVERAGE GATE before moving to Step 5.
Scope: Any code that reads from ABI-encoded data using hardcoded byte offsets rather than following offset pointers. This includes:
calldataload(N) in assembly (raw calldata)mload(add(data, N)) in assembly (bytes memory/calldata variable)data[N:] or data[N:N+32] byte-slicing in Solidity with hardcoded Nbytes fields after abi.decodeGrep: calldataload\( with a numeric literal, mload(add( with a literal offset on a bytes variable, fixed-offset byte-slicing on decoded bytes data.
| Read Site | Mechanism | Offset | Hardcoded? | Into Dynamic-Type Content? | Value Used For | Same Value Read via abi.decode? | |-----------|-----------|--------|-----------|---------------------------|----------------|-------------------------------|
Root cause: ABI encoding is a convention, not enforced by the EVM. Dynamic types (bytes, string, T[]) use offset pointers — the content can be placed anywhere the pointer says. Hardcoded offsets assume canonical pointer values. A caller can supply non-canonical (but ABI-valid) encoding, making the hardcoded position contain attacker-controlled data instead of the expected field. This applies at every nesting level — top-level calldata, inner bytes fields, and nested structs.
Three impact categories:
HIGH/CRITICAL — Dual-read divergence: Hardcoded-offset read + the same value also read via abi.decode or pointer-following elsewhere. The two reads see different values. Enables: authorization bypass (auth sees attacker, execution sees victim), accounting divergence (check validates amount X, transfer uses amount Y).
MEDIUM — Single-read assumption violation: Hardcoded-offset read into dynamic-type content, value is security-critical, no dual-read exists but offset pointer is not validated as canonical. The contract reads attacker-controlled data as a trusted field. Latent Critical — any future addition of abi.decode on the same data creates dual-read divergence.
MEDIUM — Revert injection / DoS: Hardcoded-offset read lands on attacker-controlled data that triggers a revert (zero address, overflow, out-of-bounds). Legitimate calldata (via canonical encoding) would succeed, but attacker-crafted non-canonical encoding causes revert. Enables: griefing specific operations, front-run DoS if attacker can submit a malformed version of a victim's pending transaction.
MEDIUM — Hash divergence: Contract hashes raw calldata or portions of it (via assembly keccak256 over calldatacopy or manual packing) for signature verification, deduplication, or identity. Non-canonical encoding produces different hashes for logically identical inputs — or identical hashes for different inputs if overlapping tail pointers are used. Breaks: signature verification, replay protection, UserOp-style identity schemes.
Do not report if: The read targets a static-type parameter in the ABI head area (position is fixed regardless of offset pointers), or the value is not security-critical, or the contract validates that the offset pointer equals the expected canonical value before reading.
Additional note — memory vs calldata decoding inconsistency: Non-canonical calldata that decodes successfully via abi.decode from calldata may fail when the same bytes are copied to memory and re-decoded — the Solidity memory decoder is stricter. Code that copies calldata to memory then decodes should be checked for this asymmetry.
Tag: [TRACE:hardcoded_read({mechanism}, offset={N}) → dynamic_type_content={YES/NO} → dual_read={YES/NO} → impact={DIVERGENCE/ASSUMPTION/REVERT_DOS/HASH_DIVERGENCE}]
When mapping entries or array elements are deleted:
delete mapping[key] clears value but leaves stale entries in enumeration arrays?For manual bit packing:
|= (1 << n) to set but = 0 instead of &= ~(1 << n) to clear → clears ALL bits)Variables read before explicit write:
require(configuredValue > 0) but never set → permanent DoS. if (admin == address(0)) { unrestricted } → open access until set.Tag: [TRACE:delete={op} → auxiliary={state} → updated={YES/NO} → consumer={func} → reads_stale={YES/NO}]
sstore target be influenced by external input?__gap consumed correctly → no layout shiftStorageSlot) → lower risk| Section | Required | Completed? | Notes | |---------|----------|------------|-------| | 1. Storage Surface Inventory | YES | | All state variables with slots | | 2. Memory vs Storage Confusion | IF structs/complex types | | Data location of all references | | 3. Proxy Storage Layout | IF proxy/upgradeable | | Slot overlap, upgrade continuity | | 4. Assembly Storage Safety | IF assembly with sstore/sload | | Slot computation, value encoding | | 4d. Hardcoded Offset into ABI Data | IF calldataload/mload at hardcoded offset OR byte-slicing with literal offset on dynamic-type data | | Dual-read divergence, assumption violation, revert injection | | 5. Storage Semantic Corruption | IF delete/restructure ops | | Auxiliary state consistency |
development
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project documentation checks. Generates a scored Audit Readiness Report and optionally runs static analysis. Trigger on: "prepare for audit", "audit readiness", "pre-audit check", "audit prep", "NatSpec check", or any request to review a Solidity codebase before a security review.
development
Launch the Plamen deterministic Web3 security audit pipeline
development
Run the Plamen smart-contract audit wizard in Codex
testing
Launch the Plamen deterministic L1 infrastructure audit pipeline