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 的升级合约实现已经基于此标准。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!