前不久研究了下重入攻击的玩法,里面有个重要的特性:合约的fallback()函数。 fallback是合约的底层函数,在没有其它函数匹配的情况下会执行。这个特性导致了重入的风险性,但在另一方面,合约的升级性也会用到这一特性。这充分体现了一物的两面性:是好是坏取决于你!
由于区块链不可篡改性,合约如果有漏洞就很难更改!那么,怎么才能安全地升级合约呢?这里要用到一种解藕的想法。把一个合约分成:代理、数据和逻辑三部分,通常要升级的也就是逻辑部分,其它不受影响,如下图所示:
我们来看下具体实现:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
//定义数据合约
contract storageStructure {
//记录球员和分数
address public implementation;//逻辑合约地址
mapping(address=>uint256) public points;
address public owner;
}
//定义逻辑合约
contract implementationV1 is storageStructure {
modifier onlyowner() {
require(msg.sender == owner, "only owner can do");
_;
}
//增加球员和分数
function addPlayer(address player, uint256 point) public onlyowner {
require(points[player] == 0, "player already exists");
points[player] = point;
}
//修改球员和分数
function setPlayer(address player, uint256 point) public onlyowner {
require(points[player] != 0, "player must already exists");
points[player] = point;
}
}
//代理合约 代理合约调用逻辑合约的逻辑去修改本身(代理合约)的数据
contract proxy is storageStructure {
modifier onlyowner() {
require(msg.sender == owner, "only owner can call");
_;
}
constructor() {
owner = msg.sender;
}
//更新逻辑合约的地址
function setImpl(address _impl) public onlyowner {
implementation = _impl;
}
//fallback函数 调用逻辑合约中的函数,在本地(代理合约)执行
fallback() external {
address impl = implementation; //逻辑合约的地址
require(impl != address(0), "implementation must exists");
//底层调用
assembly {
//调用delegateccall
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
//delegatecall(g, a, in, insize, out, outsize)
let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
//returndatacopy(t, f, s)
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
可以看出,里面最核心的就是assembly中调用delegateccall
。assembly
属于汇编的写法,是比较底层的语法(yul),不太好理解。我们大概可以理解整个地调用过程:用户调用代理合约,代理合约调用逻辑合约修改本身的数据。如果需要升级,代理合约调用逻辑合约时就可以指向新的逻辑合约即可!
//逻辑合约升级
contract implementationV2 is implementationV1 {
function addPlayer(address player, uint256 point) override public onlyowner virtual {
require(points[player] == 0, "player already exists");
points[player] = point;
totalPlayers ++;
}
}
升级合约差不多就是个解藕的思路,分成三个部分,把需要升级的部分单独更改升级就可以啰!