As the scaling is applied twice, the amount of tokens to be sent will be less than it should be.
The scenarios above are represented in this PoC.
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import "../contracts/core/governance/boost/BoostController.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/interfaces/core/tokens/IveRAACToken.sol";
import "../contracts/core/pools/StabilityPool/StabilityPool.sol";
import "../contracts/core/pools/LendingPool/LendingPool.sol";
import "../contracts/core/tokens/RToken.sol";
import "../contracts/core/tokens/DebtToken.sol";
import "../contracts/core/tokens/DEToken.sol";
import "../contracts/core/tokens/RAACToken.sol";
import "../contracts/core/tokens/RAACNFT.sol";
import "../contracts/core/minters/RAACMinter/RAACMinter.sol";
import "../contracts/libraries/math/WadRayMath.sol";
import "../contracts/core/primitives/RAACHousePrices.sol";
import "../contracts/mocks/core/tokens/crvUSDToken.sol";
contract RTokenTest is Test {
using WadRayMath for uint256;
StabilityPool stabilityPool;
LendingPool lendingPool;
RToken rToken;
DEToken deToken;
DebtToken debtToken;
RAACMinter raacMinter;
crvUSDToken crvUSD;
RAACToken raacToken;
RAACHousePrices public raacHousePrices;
RAACNFT public raacNFT;
address owner = address(1);
address user1 = address(2);
address user2 = address(3);
address user3 = address(4);
address[] users = new address[](3);
function setUp() public {
users[0] = user1;
users[1] = user2;
users[2] = user3;
vm.label(user1, "USER1");
vm.label(user2, "USER2");
vm.label(user3, "USER3");
vm.warp(1738798039);
vm.roll(100);
vm.startPrank(owner);
_deployAndSetupContracts();
vm.stopPrank();
_mintCrvUsdTokenToUsers(1000e18);
}
function test_RToken_transfer_send_incorrectAmount() public {
uint256 depositAmount = 100e18;
vm.prank(user1);
lendingPool.deposit(depositAmount);
uint256 liquidityIndexLendingPoolBefore = _liquidityIndexFromLendingPool();
uint256 liquidityIndexRTokenBefore = rToken.getLiquidityIndex();
_accrueYieldForRToken();
assertGt(_liquidityIndexFromLendingPool(), liquidityIndexLendingPoolBefore);
assertEq(rToken.getLiquidityIndex(), liquidityIndexRTokenBefore);
address newUser = address(0x3342432);
uint256 user1BalanceBefore = rToken.balanceOf(user1);
console.log("user balance before: %e", user1BalanceBefore);
vm.startPrank(user1);
rToken.transfer(newUser, rToken.balanceOf(user1));
vm.stopPrank();
console.log("new user received: %e", rToken.balanceOf(newUser));
console.log("amount of rToken user1 has: %e", rToken.balanceOf(user1));
assertLt(rToken.balanceOf(newUser), user1BalanceBefore);
assertGt(rToken.balanceOf(user1), 0);
}
function test_RTokenTransfer_leadToLossOfFunds() public {
uint256 depositAmount = 100e18;
vm.prank(user1);
lendingPool.deposit(depositAmount);
uint256 liquidityIndexLendingPoolBefore = _liquidityIndexFromLendingPool();
uint256 liquidityIndexRTokenBefore = rToken.getLiquidityIndex();
_accrueYieldForRToken();
assertGt(_liquidityIndexFromLendingPool(), liquidityIndexLendingPoolBefore);
assertEq(rToken.getLiquidityIndex(), liquidityIndexRTokenBefore);
uint256 user1BalanceBefore = rToken.balanceOf(user1);
console.log("user balance before: %e", user1BalanceBefore);
vm.startPrank(user1);
rToken.approve(address(stabilityPool), type(uint256).max);
stabilityPool.deposit(rToken.balanceOf(user1));
vm.stopPrank();
assertEq(rToken.balanceOf(user1), 0);
console.log("deToken minted after deposit: %e", deToken.balanceOf(user1));
vm.startPrank(user1);
stabilityPool.withdraw(deToken.balanceOf(user1));
vm.stopPrank();
assertEq(deToken.balanceOf(user1), 0, "user should not have deToken after withdraw the entire amount");
assertEq(deToken.totalSupply(), 0, "deToken supply should be zero");
console.log("amount of rToken in stabilityPool: %e", rToken.balanceOf(address(stabilityPool)));
console.log("User lost %e in RTokens after withdraw:", rToken.balanceOf(address(stabilityPool)));
assertLt(rToken.balanceOf(user1), user1BalanceBefore);
}
function _deployAndSetupContracts() internal {
crvUSD = new crvUSDToken(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",
"RTK",
owner,
address(crvUSD)
);
deToken = new DEToken(
"DEToken",
"DET",
owner,
address(rToken)
);
debtToken = new DebtToken(
"DebtToken",
"DEBT",
owner
);
lendingPool = new LendingPool(
address(crvUSD),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
0.8e27
);
raacMinter = new RAACMinter(
address(raacToken),
address(0x1234324423),
address(lendingPool),
owner
);
stabilityPool = new StabilityPool(owner);
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvUSD),
address(lendingPool)
);
raacMinter.setStabilityPool(address(stabilityPool));
lendingPool.setStabilityPool(address(stabilityPool));
rToken.setReservePool(address(lendingPool));
debtToken.setReservePool(address(lendingPool));
rToken.transferOwnership(address(lendingPool));
debtToken.transferOwnership(address(lendingPool));
deToken.setStabilityPool(address(stabilityPool));
deToken.transferOwnership(address(stabilityPool));
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
}
function _liquidityIndexFromLendingPool() internal view returns(uint128) {
(, , , , , uint128 liquidityIndex, ,) = lendingPool.reserve();
return liquidityIndex;
}
function _depositCrvUsdIntoLendingPoolForAllUsers(uint256 initialDeposit) internal {
for (uint i = 0; i < users.length; i++) {
vm.prank(users[i]);
lendingPool.deposit(initialDeposit);
}
}
function _mintCrvUsdTokenToUsers(uint256 initialBalance) internal {
for (uint i = 0; i < users.length; i++) {
_mintCrvUsdTokenToUser(initialBalance, users[i]);
}
}
function _mintCrvUsdTokenToUser(uint256 initialBalance, address user) internal {
vm.prank(owner);
crvUSD.mint(user, initialBalance);
vm.startPrank(user);
crvUSD.approve(address(raacNFT), initialBalance);
crvUSD.approve(address(lendingPool), initialBalance);
rToken.approve(address(stabilityPool), initialBalance);
vm.stopPrank();
}
function _advanceInTime(uint256 time) internal {
vm.warp(block.timestamp + time);
vm.roll(block.number + 10000);
}
function _advanceInTimeAndAccrueInterestInLendingPool(uint256 time) internal {
uint256 usageIndex = lendingPool.getNormalizedDebt();
_advanceInTime(time);
lendingPool.updateState();
}
function _setupHousePrices(uint256 housePrice) internal {
vm.startPrank(owner);
raacHousePrices.setHousePrice(1, housePrice);
raacHousePrices.setHousePrice(2, housePrice);
raacHousePrices.setHousePrice(3, housePrice);
vm.stopPrank();
}
function _setupHousePrice(uint256 housePrice, uint256 newValue) internal {
vm.startPrank(owner);
raacHousePrices.setHousePrice(newValue, housePrice);
vm.stopPrank();
}
function _mintNFTwithTokenId(uint256 tokenId, uint256 housePrice) internal {
raacNFT.mint(tokenId, housePrice);
raacNFT.approve(address(lendingPool), tokenId);
}
function _mintAndDepositNftInLendingPool(uint256 tokenId, uint256 housePrice) internal {
_mintNFTwithTokenId(tokenId, housePrice);
lendingPool.depositNFT(tokenId);
}
function _borrowCrvUsdTokenFromLendingPool(uint256 amount) internal {
lendingPool.borrow(amount);
}
function _depositNftBorrowFundsAndMakeUserLiquidatable(address user, uint256 tokenId, uint256 nftPrice) internal {
_setupHousePrice(nftPrice, tokenId);
vm.startPrank(user);
_mintAndDepositNftInLendingPool(tokenId, nftPrice);
_borrowCrvUsdTokenFromLendingPool(nftPrice/2);
vm.stopPrank();
_advanceInTimeAndAccrueInterestInLendingPool(365 days);
_setupHousePrice(nftPrice/2, tokenId);
}
function _accrueYieldForRToken() internal {
uint256 housePrice = 300e18;
_setupHousePrice(housePrice, 1);
vm.startPrank(user2);
_mintAndDepositNftInLendingPool(1, housePrice);
_borrowCrvUsdTokenFromLendingPool(50e18);
vm.stopPrank();
_advanceInTimeAndAccrueInterestInLendingPool(365 days);
vm.startPrank(user2);
lendingPool.updateState();
lendingPool.repay(debtToken.balanceOf(user2));
vm.stopPrank();
assertEq(lendingPool.getUserDebt(user2), 0);
assertEq(debtToken.totalSupply(), 0);
}
}
Run the PoC again, and notice that for both tests there is no loss of funds/incorrect amounts: