The LendingPool contract provides two repayment functions: one for users repaying their own debt via repay, and another for repaying on behalf of another user via repayOnBehalf. According to the NatSpec documentation, if the onBehalfOf parameter is set to address(0), the repayment should default to repaying the caller's own debt. However, both repayOnBehalf and the internal _repay function enforce a check that reverts if onBehalfOf is zero. This discrepancy creates a denial-of-service (DoS) vulnerability, breaking the intended functionality and preventing users from using the shortcut to repay their own debt via the on-behalf mechanism.
* @notice Allows a user to repay their own borrowed reserve assets
* @param amount The amount to repay
*/
function repay(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
_repay(amount, msg.sender);
}
* @notice Allows a user to repay borrowed reserve assets on behalf of another user
* @param amount The amount to repay
* @param onBehalfOf The address of the user whose debt is being repaid
*/
function repayOnBehalf(uint256 amount, address onBehalfOf)
external
nonReentrant
whenNotPaused
onlyValidAmount(amount)
{
if (!canPaybackDebt) revert PaybackDebtDisabled();
@> if (onBehalfOf == address(0)) revert AddressCannotBeZero();
_repay(amount, onBehalfOf);
}
* @notice Internal function to repay borrowed reserve assets
* @param amount The amount to repay
* @param onBehalfOf The address of the user whose debt is being repaid. If address(0), msg.sender's debt is repaid.
* @dev This function allows users to repay their own debt or the debt of another user.
* The caller (msg.sender) provides the funds for repayment in both cases.
* If onBehalfOf is set to address(0), the function defaults to repaying the caller's own debt.
*/
function _repay(uint256 amount, address onBehalfOf) internal {
if (amount == 0) revert InvalidAmount();
@> if (onBehalfOf == address(0)) revert AddressCannotBeZero();
UserData storage user = userData[onBehalfOf];
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 userDebt = IDebtToken(reserve.reserveDebtTokenAddress).balanceOf(onBehalfOf);
uint256 userScaledDebt = userDebt.rayDiv(reserve.usageIndex);
uint256 actualRepayAmount = amount > userScaledDebt ? userScaledDebt : amount;
uint256 scaledAmount = actualRepayAmount.rayDiv(reserve.usageIndex);
(uint256 amountScaled, uint256 newTotalSupply, uint256 amountBurned, uint256 balanceIncrease) =
IDebtToken(reserve.reserveDebtTokenAddress).burn(onBehalfOf, amount, reserve.usageIndex);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, amountScaled);
reserve.totalUsage = newTotalSupply;
user.scaledDebtBalance -= amountBurned;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, amountScaled, 0);
emit Repay(msg.sender, onBehalfOf, actualRepayAmount);
}
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {StabilityPool} from "../src/core/pools/StabilityPool/StabilityPool.sol";
import {LendingPool} from "../src/core/pools/LendingPool/LendingPool.sol";
import {DEToken} from "../src/core/tokens/DEToken.sol";
import {RToken} from "../src/core/tokens/RToken.sol";
import {DebtToken} from "../src/core/tokens/DebtToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {RAACMinter} from "../src/core/minters/RAACMinter/RAACMinter.sol";
import {RAACHousePrices} from "../src/core/primitives/RAACHousePrices.sol";
import {RAACNFT} from "../src/core/tokens/RAACNFT.sol";
import {CurveCrvUSDVaultMock} from "./mocks/CurveCrvUSDVaultMock.m.sol";
import {crvUSDToken} from "../src/mocks/core/tokens/crvUSDToken.sol";
import {ERC20Mock} from "../src/mocks/core/tokens/ERC20Mock.sol";
import {ILendingPool} from "../src/interfaces/core/pools/LendingPool/ILendingPool.sol";
import {PercentageMath} from "../src/libraries/math/PercentageMath.sol";
import {WadRayMath} from "../src/libraries/math/WadRayMath.sol";
contract PoolsTest is Test {
using PercentageMath for uint256;
using WadRayMath for uint256;
StabilityPool stabilityPool;
LendingPool lendingPool;
DEToken deToken;
RToken rToken;
DebtToken debtToken;
RAACToken raacToken;
RAACMinter raacMinter;
RAACHousePrices raacHousePrices;
RAACNFT raacNft;
CurveCrvUSDVaultMock curveCrvUsdVault;
crvUSDToken crvUsdToken;
ERC20Mock erc20Mock;
address STABILITY_POOL_OWNER = makeAddr("STABILITY_POOL_OWNER");
address LENDING_POOL_OWNER = makeAddr("LENDING_POOL_OWNER");
address DETOKEN_OWNER = makeAddr("DETOKEN_OWNER");
address RTOKEN_OWNER = makeAddr("RTOKEN_OWNER");
address DEBT_TOKEN_OWNER = makeAddr("DEBT_TOKEN_OWNER");
address RAAC_TOKEN_OWNER = makeAddr("RAAC_TOKEN_OWNER");
address RAAC_MINTER_DUMMY = makeAddr("RAAC_MINTER_DUMMY");
address RAAC_HOUSE_PRICES_OWNER = makeAddr("RAAC_HOUSE_PRICES_OWNER");
address RAAC_HOUSE_PRICES_ORACLE = makeAddr("RAAC_HOUSE_PRICES_ORACLE");
address CRV_USD_TOKEN_OWENR = makeAddr("CRV_USD_TOKEN_OWENR");
address CURVE_CRV_USD_VAULT_OWENR = makeAddr("CURVE_CRV_USD_VAULT_OWENR");
address ERC20_MOCK_TOKEN_OWNER = makeAddr("ERC20_MOCK_TOKEN_OWNER");
address RAAC_NFT_TOKEN_OWNER = makeAddr("RAAC_NFT_TOKEN_OWNER");
address RAAC_MINTER_OWNER = makeAddr("RAAC_MINTER_OWNER");
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
address MANAGER_1 = makeAddr("MANAGER_1");
address MANAGER_2 = makeAddr("MANAGER_2");
address MANAGER_3 = makeAddr("MANAGER_3");
address MANAGER_4 = makeAddr("MANAGER_4");
address MANAGER_5 = makeAddr("MANAGER_5");
address MANAGER_6 = makeAddr("MANAGER_6");
address MANAGER_7 = makeAddr("MANAGER_7");
address MANAGER_8 = makeAddr("MANAGER_8");
address MANAGER_9 = makeAddr("MANAGER_9");
address MANAGER_10 = makeAddr("MANAGER_10");
uint256 initialPrimeRate = 5e27;
uint256 initialRaacSwapTaxRateInBps = 200;
uint256 initialRaacBurnTaxRateInBps = 150;
function setUp() public {
vm.warp(block.timestamp + 1 days + 1);
vm.startPrank(CRV_USD_TOKEN_OWENR);
crvUsdToken = new crvUSDToken(CRV_USD_TOKEN_OWENR);
vm.stopPrank();
vm.startPrank(CURVE_CRV_USD_VAULT_OWENR);
curveCrvUsdVault = new CurveCrvUSDVaultMock(address(crvUsdToken), CURVE_CRV_USD_VAULT_OWENR);
vm.stopPrank();
vm.startPrank(RTOKEN_OWNER);
rToken = new RToken("R_Token_V1", "RTKNV1", RTOKEN_OWNER, address(crvUsdToken));
vm.stopPrank();
vm.startPrank(DETOKEN_OWNER);
deToken = new DEToken("DE_Token_V1", "DETKNV1", DETOKEN_OWNER, address(rToken));
vm.stopPrank();
vm.startPrank(DEBT_TOKEN_OWNER);
debtToken = new DebtToken("DEBT_TOKEN_V1", "DEBTKNV1", DEBT_TOKEN_OWNER);
vm.stopPrank();
vm.startPrank(ERC20_MOCK_TOKEN_OWNER);
erc20Mock = new ERC20Mock("ERC20_MOCK_TOKEN", "ERC20MTKN");
vm.stopPrank();
vm.startPrank(RAAC_HOUSE_PRICES_OWNER);
raacHousePrices = new RAACHousePrices(RAAC_HOUSE_PRICES_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_NFT_TOKEN_OWNER);
raacNft = new RAACNFT(address(erc20Mock), address(raacHousePrices), RAAC_NFT_TOKEN_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_OWNER);
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.stopPrank();
vm.startPrank(LENDING_POOL_OWNER);
lendingPool = new LendingPool(
address(crvUsdToken),
address(rToken),
address(debtToken),
address(raacNft),
address(raacHousePrices),
initialPrimeRate
);
vm.stopPrank();
vm.startPrank(STABILITY_POOL_OWNER);
stabilityPool = new StabilityPool(STABILITY_POOL_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER_OWNER);
raacMinter = new RAACMinter(address(raacToken), address(stabilityPool), address(lendingPool), RAAC_MINTER_OWNER);
vm.stopPrank();
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(address(raacMinter));
vm.stopPrank();
vm.startPrank(LENDING_POOL_OWNER);
lendingPool.setCurveVault(address(curveCrvUsdVault));
vm.stopPrank();
vm.startPrank(RTOKEN_OWNER);
rToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.startPrank(DEBT_TOKEN_OWNER);
debtToken.setReservePool(address(lendingPool));
vm.stopPrank();
vm.startPrank(STABILITY_POOL_OWNER);
stabilityPool.initialize(
address(rToken),
address(deToken),
address(raacToken),
address(raacMinter),
address(crvUsdToken),
address(lendingPool)
);
vm.stopPrank();
crvUsdToken.mint(address(lendingPool), 1000_000_000e18);
}
function testRepayDoSIfOnBehalfOfIsZeroAddress() public {
uint256 tokenId = 1;
uint256 housePrice = 1000e18;
vm.startPrank(RAAC_HOUSE_PRICES_OWNER);
raacHousePrices.setOracle(RAAC_HOUSE_PRICES_ORACLE);
vm.stopPrank();
vm.startPrank(RAAC_HOUSE_PRICES_ORACLE);
raacHousePrices.setHousePrice(tokenId, housePrice);
vm.stopPrank();
crvUsdToken.mint(ALICE, 1000e18);
vm.startPrank(ALICE);
crvUsdToken.approve(address(lendingPool), 1000e18);
lendingPool.deposit(1000e18);
vm.stopPrank();
erc20Mock.mint(ALICE, housePrice);
vm.startPrank(ALICE);
erc20Mock.approve(address(raacNft), housePrice);
raacNft.mint(tokenId, housePrice);
vm.stopPrank();
vm.startPrank(ALICE);
raacNft.approve(address(lendingPool), tokenId);
lendingPool.depositNFT(tokenId);
vm.stopPrank();
(, uint256 scaledDebtBalance,,,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After depositNFT... ");
console.log("scaledDebtBalance: ", scaledDebtBalance);
uint256 borrowAmount = 900e18;
vm.startPrank(ALICE);
lendingPool.borrow(borrowAmount);
vm.stopPrank();
(, scaledDebtBalance,,,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After borrow... ");
console.log("scaledDebtBalance: ", scaledDebtBalance);
vm.startPrank(ALICE);
crvUsdToken.mint(ALICE, 500e18);
crvUsdToken.approve(address(lendingPool), 500e18);
vm.expectRevert(bytes4(keccak256("AddressCannotBeZero()")));
lendingPool.repayOnBehalf(500e18, address(0));
vm.stopPrank();
(, scaledDebtBalance,,,,,,) = lendingPool.getAllUserData(ALICE);
console.log("After repay fail... ");
console.log("scaledDebtBalance: ", scaledDebtBalance);
}
}
After applying these changes, rerun the test suite to verify that the intended functionality is restored—allowing a zero address to default to the caller's debt repayment—thus eliminating the denial of service.
By implementing these modifications, the protocol will align with its NatSpec documentation and intended behavior, ensuring a smoother and more resilient debt repayment process.