When the reserve.liquidityIndex becomes larger than 1e27, which occurs from normal usage of the protocol over time, the withdraw function in the StabilityPool.sol contract causes users to lose more funds over time. This issue is caused by the transfer function in the RToken.sol contract, which scales the amount twice: once in the transfer function and once in the _update function. This double scaling results in users receiving fewer rTokens for their deTokens during withdrawals.
The vulnerability arises from the way the RToken contract handles transfers and updates. Specifically, the transfer function scales the amount by the liquidityIndex, and the _update function also scales the amount by the liquidityIndex. This double scaling leads to an incorrect calculation of the token amounts during transfers and withdrawals.
The impact of this vulnerability is significant as it causes users to lose more funds over time. Specifically, users receive fewer rTokens for their deTokens during withdrawals, leading to a loss of value. This issue can erode user trust and result in financial losses for users of the protocol.
To fix this vulnerability, the double scaling issue in the RToken contract needs to be addressed. Specifically, the transfer and _update functions should be reviewed and modified to ensure that the amount is scaled correctly only once. Here are some recommendations:
Review and Modify transfer Function: Ensure that the transfer function scales the amount correctly and does not apply double scaling.
Review and Modify _update Function: Ensure that the _update function scales the amount correctly and does not apply double scaling.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/contracts/core/pools/LendingPool/LendingPool.sol";
import "../src/contracts/mocks/core/tokens/crvUSDToken.sol";
import "../src/contracts/core/primitives/RAACHousePrices.sol";
import "../src/contracts/core/tokens/RAACNFT.sol";
import "../src/contracts/core/tokens/RToken.sol";
import "../src/contracts/core/tokens/DebtToken.sol";
import "../src/contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../src/contracts/core/tokens/RAACToken.sol";
import "../src/contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../src/contracts/core/tokens/DeToken.sol";
contract StabilityPoolTest is Test {
address owner;
address user1;
address user2;
address user3;
address treasury;
StabilityPool stabilityPool;
LendingPool lendingPool;
RAACMinter raacMinter;
RToken rToken;
DebtToken debtToken;
DEToken deToken;
RAACToken raacToken;
crvUSDToken crvusd;
RAACNFT raacNFT;
RAACHousePrices raacHousePrices;
uint256 currentTime = 1672531200;
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
user3 = address(0x3);
treasury = address(0x4);
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacToken = new RAACToken(owner, 100, 50);
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
debtToken = new DebtToken("DebtToken", "DT", owner);
uint256 initialPrimeRate = 5e27;
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
initialPrimeRate
);
raacHousePrices.setOracle(owner);
stabilityPool = new StabilityPool(owner);
vm.warp(currentTime);
raacMinter = new RAACMinter(
address(raacToken),
address(stabilityPool),
address(lendingPool),
owner
);
lendingPool.setStabilityPool(address(stabilityPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
debtToken.setReservePool(address(lendingPool));
rToken.setReservePool(address(lendingPool));
rToken.setMinter(address(lendingPool));
rToken.setBurner(address(lendingPool));
mintRToken(address(lendingPool), user2, 100, 1);
mintAndDeposit(user1, 100);
forwardTime(365 days);
mintNFTAndBorrow(user1, 1, 50, 25);
forwardTime(365 days);
mintAndDeposit(user3, 1);
}
function forwardTime(uint256 addTime) internal {
currentTime += addTime;
vm.warp(currentTime);
}
function mintAndDeposit(address userF, uint256 amount) internal {
console.log(("Minting and depositing "));
crvusd.mint(userF, amount);
vm.startPrank(userF);
crvusd.approve(address(lendingPool), amount);
lendingPool.deposit(amount);
vm.stopPrank();
}
function mintRToken(address from, address to, uint256 amount, uint256 liquidityIndex) internal {
vm.startPrank(from);
rToken.mint(from, to, amount , liquidityIndex);
vm.stopPrank();
}
function mintNFTAndBorrow(address user, uint256 nftId, uint256 nftValue, uint256 borrowAmount) internal {
console.log(string(abi.encodePacked("Minting NFT and borrowing ", Strings.toString(nftId), " ", Strings.toString(nftValue), " ", Strings.toString(borrowAmount))));
vm.startPrank(owner);
raacHousePrices.setHousePrice(nftId, nftValue);
crvusd.mint(user, nftValue);
vm.stopPrank();
vm.startPrank(user);
crvusd.approve(address(raacNFT), nftValue);
raacNFT.mint(nftId, nftValue);
raacNFT.approve(address(lendingPool), nftId);
lendingPool.depositNFT(nftId);
lendingPool.borrow(nftValue);
vm.stopPrank();
}
function testPartialWithdrawal() public {
vm.startPrank(user2);
uint256 initialAmount = 100;
uint256 withdrawAmount = 50;
rToken.approve(address(stabilityPool), initialAmount);
crvusd.approve(address(lendingPool), initialAmount);
stabilityPool.deposit(initialAmount);
uint256 rTokenBalanceBeforeWithdraw = rToken.balanceOf(user2);
uint256 deTokenBalanceBeforeWithdraw = deToken.balanceOf(user2);
console.log(string(abi.encodePacked("User2 rToken balance after deposit: ", Strings.toString(rTokenBalanceBeforeWithdraw))));
console.log(string(abi.encodePacked("User2 deToken balance after deposit: ", Strings.toString(deTokenBalanceBeforeWithdraw))));
stabilityPool.withdraw(withdrawAmount);
uint256 rTokenBalanceAfterWithdraw = rToken.balanceOf(user2);
uint256 deTokenBalanceAfterWithdraw = deToken.balanceOf(user2);
console.log(string(abi.encodePacked("User2 rToken balance after withdrawal: ", Strings.toString(rTokenBalanceAfterWithdraw))));
console.log(string(abi.encodePacked("User2 deToken balance after withdrawal: ", Strings.toString(deTokenBalanceAfterWithdraw))));
uint256 deTokenLost = (rTokenBalanceBeforeWithdraw + deTokenBalanceBeforeWithdraw) - (rTokenBalanceAfterWithdraw + deTokenBalanceAfterWithdraw);
console.log(string(abi.encodePacked("User2 deToken lost during withdrawal: ", Strings.toString(deTokenLost))));
vm.stopPrank();
}
}