Arbitrary Storage Write Remediation
How to eliminate arbitrary storage write vulnerabilities by removing user-controlled assembly SSTORE operations, using Solidity mappings, and enforcing slot allowlists for unavoidable inline assembly.
Arbitrary Storage Write Remediation
Overview
Arbitrary storage write vulnerabilities arise when a contract exposes an assembly sstore(slot, value) operation where the target slot argument is controlled by user input without bounds checking or allowlisting. Since storage slot 0 is conventionally the owner address in simple contracts, an attacker calls the function with slot = 0 and value = uint256(attackerAddress) to overwrite the owner and gain full privileged access. The attack also works indirectly through array index overflow: a dynamic array stored at slot N has its elements at keccak256(N) + index. By computing an index such that keccak256(N) + index ≡ 0 (mod 2^256), an attacker can target slot 0 through what appears to be an array write.
The primary fix is to eliminate user-controlled assembly storage operations entirely and use Solidity’s high-level storage abstractions. When inline assembly is unavoidable, implement a compile-time allowlist of permitted slots.
Related Detector: Access Control Detector
Recommended Fix
Before (Vulnerable)
contract VulnerableStorage {
address public owner; // Slot 0
uint256 public balance; // Slot 1
// VULNERABLE: user controls which slot to write
function setStorageAt(uint256 slot, uint256 value) public {
assembly {
sstore(slot, value) // No validation — slot 0 overwrites owner!
}
}
// VULNERABLE: unbounded array write via assembly
uint256[] private data; // Array length at slot 2; elements at keccak256(2)+i
function writeData(uint256 index, uint256 value) public {
assembly {
// Attacker computes index = type(uint256).max - keccak256(2) + 1
// keccak256(2) + index wraps to 0 (mod 2^256) → overwrites owner
let slot := add(keccak256(0, 32), index)
sstore(slot, value)
}
}
}
After (Fixed)
contract SafeStorage {
address public owner; // Slot 0
uint256 public balance; // Slot 1
// SAFE: Solidity mapping — slot derivation is safe and non-colliding
// Slot = keccak256(key, mappingSlot) — cannot reach slot 0 through normal keys
mapping(uint256 => uint256) private userData;
function setUserData(uint256 key, uint256 value) external {
// No assembly needed; Solidity handles safe slot computation
userData[key] = value;
}
// SAFE: Explicit bounds checking on high-level array writes
uint256[] private items;
uint256 public constant MAX_ITEMS = 1000;
function setItem(uint256 index, uint256 value) external onlyOwner {
require(index < items.length, "Index out of bounds");
require(index < MAX_ITEMS, "Index exceeds limit");
items[index] = value; // Safe high-level write
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
Alternative Mitigations
Slot allowlist for unavoidable assembly — when inline assembly storage operations are necessary (for example, in proxy implementations), restrict writes to a compile-time allowlist of permitted slots:
// ERC-1967 standard proxy storage slots — far from slot 0
bytes32 internal constant IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
bytes32 internal constant ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
function _setImplementation(address impl) internal {
assembly {
// Only permitted slots — never user-controlled
sstore(IMPLEMENTATION_SLOT, impl)
}
}
// Reject any assembly write not targeting an allowlisted slot
function _safeStore(bytes32 slot, uint256 value) internal {
require(
slot == IMPLEMENTATION_SLOT || slot == ADMIN_SLOT,
"Slot not permitted"
);
assembly {
sstore(slot, value)
}
}
Access control on data-modification functions — any function that writes to storage must be restricted to authorized callers. Combining access control with input validation provides defense in depth:
mapping(address => bool) public authorized;
modifier onlyAuthorized() {
require(authorized[msg.sender], "Not authorized");
_;
}
function updateRecord(uint256 key, uint256 value) external onlyAuthorized {
require(key < MAX_KEY, "Key too large");
records[key] = value;
}
Use ERC-1967 proxy slots to avoid layout collisions — for upgradeable proxy contracts, use the ERC-1967 standard proxy storage slots which are derived from hashes far removed from slot 0. This prevents proxy variables from colliding with implementation variables regardless of implementation layout changes:
// ERC-1967: slots derived from keccak256("eip1967.proxy.implementation") - 1
// These hashes are not near slot 0 — no collision risk
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
Static analysis and fuzzing — run Slither with the arbitrary-send-eth and controlled-delegatecall detectors, and fuzz storage-writing functions with slot values 0, 1, and type(uint256).max to confirm boundary behavior before deployment.
Common Mistakes
Assuming access control alone is sufficient — a function protected by onlyOwner that still accepts user-controlled slot arguments is dangerous if the ownership check can itself be bypassed (e.g., through a reentrancy path or another vulnerable function that overwrites the owner slot).
Overlooking indirect array overflow — developers often audit direct sstore(slot, value) patterns but miss array-based overflow paths where a large index wraps around to target slot 0 through the keccak256 element offset arithmetic.
Copying proxy patterns without understanding them — many proxy implementations use assembly storage reads and writes. Copying these patterns without understanding which slots are targeted can introduce unintended storage write vectors if the surrounding logic is modified.
Not auditing inherited contracts — a parent contract or library may expose a storage-writing function that is public or internal. Always audit the full inheritance chain for functions that write to assembly-specified storage slots.