The DebtToken contract's totalSupply() function scales down instead of up. This leads to an underreported total debt in the protocol and could cause serious accounting issues.
The function incorrectly uses rayDiv when it should use rayMul to scale up the total supply. This is evident because:
Below is a Proof of Concept that shows that there is a missmatch between the balance of the user and the totalSupply. After borrowing the total liquidty of the LendingPool the total usage and the totalSupply are out of sync, while the users DebtToken balance shows the real amount.
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {StabilityPool} from "../../contracts/core/pools/StabilityPool/StabilityPool.sol";
import {crvUSDToken} from "../../contracts/mocks/core/tokens/crvUSDToken.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {RAACHousePrices} from "../../contracts/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../../contracts/core/tokens/RAACNFT.sol";
import {RToken} from "../../contracts/core/tokens/RToken.sol";
import {DebtToken} from "../../contracts/core/tokens/DebtToken.sol";
import {DEToken} from "../../contracts/core/tokens/DEToken.sol";
import {LendingPool} from "../../contracts/core/pools/LendingPool/LendingPool.sol";
import {RAACMinter, IRAACMinter} from "../../contracts/core/minters/RAACMinter/RAACMinter.sol";
import {PercentageMath} from "../../contracts/libraries/math/PercentageMath.sol";
import {ILendingPool} from "../../contracts/interfaces/core/pools/LendingPool/ILendingPool.sol";
import {IStabilityPool} from "../../contracts/interfaces/core/pools/StabilityPool/IStabilityPool.sol";
import {WadRayMath} from "../../contracts/libraries/math/WadRayMath.sol";
contract FoundryTest is Test {
using PercentageMath for uint256;
using WadRayMath for uint256;
StabilityPool public stabilityPool;
LendingPool public lendingPool;
RAACMinter public raacMinter;
crvUSDToken public crvusd;
RToken public rToken;
DEToken public deToken;
RAACToken public raacToken;
RAACNFT public raacNFT;
DebtToken public debtToken;
RAACHousePrices public raacHousePrices;
address public owner;
uint256 public constant INITIAL_BALANCE = 1000e18;
uint256 public constant INITIAL_PRIME_RATE = 1e27;
uint256 constant INITIAL_BATCH_SIZE = 3;
uint256 constant HOUSE_PRICE = 100e18;
uint256 constant TOKEN_ID = 1;
function setUp() public {
owner = address(this);
crvusd = new crvUSDToken(owner);
crvusd.setMinter(owner);
raacToken = new RAACToken(owner, 100, 50);
raacHousePrices = new RAACHousePrices(owner);
raacHousePrices.setOracle(owner);
raacHousePrices.setHousePrice(TOKEN_ID, HOUSE_PRICE);
raacNFT = new RAACNFT(address(crvusd), address(raacHousePrices), owner);
rToken = new RToken("RToken", "RToken", owner, address(crvusd));
debtToken = new DebtToken("DebtToken", "DT", owner);
deToken = new DEToken("DEToken", "DEToken", owner, address(rToken));
lendingPool = new LendingPool(
address(crvusd),
address(rToken),
address(debtToken),
address(raacNFT),
address(raacHousePrices),
INITIAL_PRIME_RATE
);
stabilityPool = new StabilityPool(owner);
vm.warp(block.timestamp + 2 days);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), owner);
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));
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvusd),
address(lendingPool)
);
raacToken.setMinter(address(raacMinter));
raacToken.manageWhitelist(address(stabilityPool), true);
}
function test_DebtTokenTotalSupplyWrong() public {
uint256 initialDeposit = 1000e18;
uint256 housePrice = 1000e18;
raacHousePrices.setHousePrice(TOKEN_ID, housePrice);
address lender = makeAddr("lender");
address borrower = makeAddr("borrower");
crvusd.mint(lender, initialDeposit);
crvusd.mint(borrower, housePrice);
assertEq(crvusd.balanceOf(lender), initialDeposit);
assertEq(crvusd.balanceOf(borrower), housePrice);
assertEq(crvusd.balanceOf(address(rToken)), 0);
vm.startPrank(lender);
crvusd.approve(address(lendingPool), initialDeposit);
lendingPool.deposit(initialDeposit);
assertEq(crvusd.balanceOf(address(rToken)), initialDeposit);
vm.stopPrank();
vm.startPrank(borrower);
crvusd.approve(address(raacNFT), housePrice);
raacNFT.mint(TOKEN_ID, housePrice);
assertEq(crvusd.balanceOf(borrower), 0);
assertEq(raacNFT.balanceOf(borrower), 1);
raacNFT.approve(address(lendingPool), TOKEN_ID);
lendingPool.depositNFT(TOKEN_ID);
assertEq(raacNFT.balanceOf(address(lendingPool)), 1);
lendingPool.borrow(housePrice / 2);
assertEq(crvusd.balanceOf(borrower), housePrice / 2);
vm.warp(block.timestamp + 10 days);
lendingPool.updateState();
lendingPool.borrow(housePrice / 2);
vm.stopPrank();
_printReserveState();
_printAddressBalance(borrower, "borrower");
assertEq(crvusd.balanceOf(borrower), housePrice);
uint256 totalSupply = debtToken.totalSupply();
console2.log("totalSupply", totalSupply);
uint256 scaledTotalSupply = debtToken.scaledTotalSupply();
console2.log("scaledTotalSupply", scaledTotalSupply);
assertLt(totalSupply, scaledTotalSupply);
assertNotEq(totalSupply, debtToken.balanceOf(borrower));
assertEq(scaledTotalSupply, debtToken.scaledBalanceOf(borrower));
}
function _printAddressBalance(address addr, string memory label) internal view {
console2.log("Printing address balance for:", label);
console2.log("crvUSD balance", crvusd.balanceOf(addr));
console2.log("Balance of DebtToken", debtToken.balanceOf(addr));
console2.log("scaledBalance DebtToken", debtToken.scaledBalanceOf(addr));
console2.log("================================================");
}
function _printReserveState() internal view {
(
address reserveRTokenAddr,
address reserveAssetAddr,
address reserveDebtTokenAddr,
uint256 totalLiquidity,
uint256 totalUsage,
uint128 liquidityIndex,
uint128 usageIndex,
uint40 lastUpdateTimestamp
) = lendingPool.reserve();
uint256 underlyingCrvUsdReserve = crvusd.balanceOf(address(rToken));
console2.log("Reserve Data");
console2.log("totalLiquidity", totalLiquidity);
console2.log("totalUsage", totalUsage);
console2.log("liquidityIndex", liquidityIndex);
console2.log("usageIndex", usageIndex);
console2.log("lastUpdateTimestamp", lastUpdateTimestamp);
console2.log("underlyingCrvUsdReserve", underlyingCrvUsdReserve);
console2.log("================================================");
}
}