Challenge #1 - Unstoppable#
Contract#
- ReceiverUnstoppable: Inherits the IERC3156FlashBorrower contract and is used to initiate a flash loan and execute the callback after the flash loan.
- UnstoppableVault: Vault contract that inherits the IERC3156FlashLender and ERC4626 contracts, supporting flash loans.
Script#
- Deploy the DamnValuableToken and UnstoppableVault contracts in order.
- Deposit TOKENS_IN_VAULT amount of tokens into the vault and transfer INITIAL_PLAYER_TOKEN_BALANCE amount of tokens to the player user.
- Deploy the ReceiverUnstoppable contract.
- Execute the attack script.
- Expect the transaction of the flash loan executed by ReceiverUnstoppable to be reverted.
Solution#
The attack target is to revert the executeFlashLoan method initiated by the ReceiverUnstoppable contract. First, analyze the calling process of executeFlashLoan.
The key point is in the UnstoppableVault.flashLoan method, which performs the following operations:
- Calculate the balance before the flash loan: totalAssets()
- Calculate the current share: convertToShares(totalSupply) and check if it matches the previously calculated balance
- Calculate the flash loan fee: flashFee
- Transfer amount of tokens to the receiver, then call the receiver's onFlashLoan method to execute the callback
- Transfer back amount+fee tokens from the receiver
- Transfer the fee to the feeRecipient account to complete the flash loan
To revert the transaction, the crucial validation point is to make convertToShares(totalSupply) != totalAssets()
Both of these functions are defined in ERC4626, and you can refer to the following article for more information about this protocol:
WTF-Solidity/51_ERC4626/readme.md at main · WTFAcademy/WTF-Solidity
In simple terms, ERC4626 is a combination of ERC20: asset tokens and share tokens. When assets are deposited or withdrawn, the corresponding amount of share tokens is minted or burned.
totalAssets()
: Calculates the number of asset tokens in the vault.convertToShares(totalSupply)
: totalSupply is the total number of share tokens (only generated when depositing or minting). convertToShares calculates: assets * totalSupply / totalAssets()
To make them inconsistent, simply do not transfer tokens to the UnstoppableVault through the deposit or mint method. Therefore, the attack script is as follows:
it('Execution', async function () {
/** CODE YOUR SOLUTION HERE */
const dvtForPlayer = token.connect(player);
await dvtForPlayer.transfer(vault.address,1);
});