Challenge #1 - Unstoppable#
合约#
- ReceiverUnstoppable:继承 IERC3156FlashBorrower 合约,用于发起闪电贷,执行闪电贷后的回调
- UnstoppableVault:金库合约,继承 IERC3156FlashLender、ERC4626,支持闪电贷
脚本#
- 依次部署 DamnValuableToken、UnstoppableVault 合约
- 存入 TOKENS_IN_VAULT 数量的 token 到金库中,转入 player 用户 INITIAL_PLAYER_TOKEN_BALANCE 数目的 token
- 部署 ReceiverUnstoppable 合约
- 执行攻击脚本
- 期望 ReceiverUnstoppable 执行闪电贷的交易被 revert
题解#
攻击目标是使得通过 ReceiverUnstoppable 合约发起的 executeFlashLoan 方法被 revert,首先分析 executeFlashLoan 的调用流程
重点在 UnstoppableVault.flashLoan 方法,分别会进行以下操作:
- 计算闪电贷开始前的余额:totalAssets ()
- 计算当前的 share:convertToShares (totalSupply) 是否与前面计算出来的余额一致
- 计算闪电贷手续费:flashFee
- 转移 amount 个 token 到 receiver,再调用 receiver 的 onFlashLoan 方法执行回调
- 从 receiver 方转回 amount+fee 数目的 token
- 将 fee 转移给 feeRecipient 账户,完成本次闪电贷
若要使交易 revert,关键的校验点在于使得:convertToShares(totalSupply) != totalAssets()
这两个函数都是 ERC4626 中的定义,关于此协议可参考下面的文章:
WTF-Solidity/51_ERC4626/readme.md at main · WTFAcademy/WTF-Solidity
简单来说就是 ERC20 的组合:资产代币 asset 和份额代币 share,存入资产或提取资产时都会相对应的铸造或销毁对应数目的 share 代币
totalAssets()
:计算的是当前金库中的资产代币数目convertToShares(totalSupply)
:totalSupply 是总的 share 代币数目(只有 deposit 或 mint 时才会产生),convertToShares 就是计算:assets * totalSupply /totalAssets ()
要想使得两者不一致,只要不通过 depost 或 mint 方法向 UnstoppableVault 中转入 token 即可,因此攻击脚本内容如下:
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const dvtForPlayer = token.connect(player);
await dvtForPlayer.transfer(vault.address,1);
});