1、存储冲突漏洞(StorageCollisionVulnerability)在代理合约模式中,代理合约和逻辑合约共享同一存储空间。当代理合约将调用委托给逻辑合约时,逻辑合约中的变量实际写入的是代理合约的存储槽位。如果代理合约和逻辑合约都在槽位0存储关键数据(例如代理合约存储实现地址,而逻
在代理合约模式中,代理合约和逻辑合约共享同一存储空间。当代理合约将调用委托给逻辑合约时,逻辑合约中的变量实际写入的是代理合约的存储槽位。如果代理合约和逻辑合约都在槽位 0 存储关键数据(例如代理合约存储实现地址,而逻辑合约存储访客地址),那么逻辑合约中的写操作会覆盖代理合约中的实现地址。攻击者可以利用这一点,通过调用逻辑合约中的函数(如 foo),将代理合约中的实现地址替换为攻击者控制的地址,从而接管代理合约的控制权。
代理合约 (Proxy Contract)
contract Proxy {
address public logicContract; // 指向 LogicV1 的地址
uint public number; // 存储实际数据
// 将所有函数调用转发到 LogicV1
fallback() external {
(bool success, ) = logicContract.delegatecall(msg.data);
require(success);
}
}
逻辑合约 (Logic Contract)
contract LogicV1 {
function getNumber() public view returns (uint) { /* ... */ }
function addNumber() public { /* ... */ }
}
delegatecall 和 call 的主要区别delegatecall:
msg.sender 和存储)执行目标合约的代码。call:
msg.sender 是调用者。代理合约(Proxy)和逻辑合约(Logic)都在相同的存储槽(槽位 0)中存储重要变量, 即代理合约中的实现地址(implementation address)和逻辑合约中的访客地址(GuestAddress)。
contract Proxy {
address public implementation; //slot0
constructor(address _implementation) {
implementation = _implementation;
}
function testcollision() public {
bool success;
(success, ) = implementation.delegatecall(
abi.encodeWithSignature("foo(address)", address(this))
);
}
function getSlot0() external view returns (bytes32 data) {
assembly {
data := sload(0) // 读取 slot 0
}
}
}
contract Logic {
address public GuestAddress; //slot0
constructor() {
GuestAddress = address(0x0);
}
function foo(address _addr) public {
GuestAddress = _addr;
}
}
1、 初始化代理合约,slot0 位置存储逻辑合约的 implementation 地址。
2、 slot0 用于保存逻辑合约的地址信息,作为代理合约与逻辑合约的关键连接点。
3、 调用 testCollision 函数,通过代理合约的 delegatecall 调用逻辑合约中的 foo(address) 函数。
4、 foo 函数将传入的地址赋值给 GuestAddress 变量,而该变量也存储在 slot0 位置。

5、 由于代理合约和逻辑合约共享 slot0,foo 函数会覆盖 implementation 地址,导致代理合约逻辑被篡改,完成攻击并接管合约。

pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "./Storage-collision.sol";
contract ContractTest is Test {
Proxy ProxyContract;
Logic LogicContract;
address Koko;
address Aquarius;
function setUp() public {
LogicContract = new Logic();
ProxyContract = new Proxy(address(LogicContract));
console.log("address:");
console.log("address(this):", address(this));
console.log("ProxyContract:", address(ProxyContract));
console.log("LogicContract:", address(LogicContract));
console.log("-------------------------------");
}
function test() public {
// 获取proxy的 slot 0 数据
bytes32 slotdata = ProxyContract.getSlot0();
console.log(
"slot0 contract address:",
address(uint160(uint256(slotdata)))
);
ProxyContract.testcollision();
slotdata = ProxyContract.getSlot0();
console.log(
"overwritten slot0 implementation contract address:",
address(uint160(uint256(slotdata)))
);
}
}
1、通过使用 initializer 修饰符,确保逻辑合约只能被正确初始化一次,防止攻击者在未初始化期间利用存储冲突漏洞。
2、采用 EIP-1967 存储布局标准,此标准使用 keccak256("eip1967.proxy.implementation") - 1 作为实现地址的存储槽,确保该槽位不会与逻辑合约中的其他变量发生冲突。OpenZeppelin 的升级合约实现已经基于此标准。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!