Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: medium
Valid

rToken Redemption Failure Due to Insufficient Liquidity for Accrued Interest

Summary

The protocol's documentation states that RTokens represent a user's deposit plus any accrued interest, implying that users can redeem their RTokens 1:1 for the underlying asset (crvUSD) at any time. However, the accrued interest in RTokens is dependent on borrowers repaying their debt or new users depositing assets into the lending pool. If neither of these scenarios occurs, the protocol may lack sufficient liquidity to honor the redemption of RTokens for the full amount (deposit + accrued interest). This breaks the protocol's invariant and undermines user trust.

Vulnerability Details

The protocol's documentation says the following: "Users that have crvUSD can participate by depositing their crvUSD to the lending pool allowing them to be used for the borrows described above. By doing so, user will receives a RToken that represents such deposit + any accrued interest." This implies that the Rtoken is redeemable for a user's assets and at any time, a user can exchange their Rtokens 1:1 for the underlying asset which in this case is crvUSD.

The issue is that the interest accrued in the rToken is completely dependent on user's repaying their debt and/or more users depositing assets to the rToken contract via LendingPool::deposit. A situation can arise where none of these scenarios are the case and it means that the user cannot redeem their full rToken amount when interest is taken into consideration which breaks the protocol's invariant which declares that the Rtoken represents deposit + accrued interest because the contract can be in a state where this is not the case.

Proof Of Code (POC)

This test was run in LendingPool.test.js in the "Borrow and Repay" describe block.

it("where does interest come from if borrowers dont repay?", async function () {
//c for testing purposes
const reserve = await lendingPool.reserve();
console.log("reserve", reserve.lastUpdateTimestamp);
await raacHousePrices.setHousePrice(2, ethers.parseEther("1000"));
const amountToPay = ethers.parseEther("1000");
//c mint nft for user2
const tokenId = 2;
await token.mint(user2.address, amountToPay);
await token.connect(user2).approve(raacNFT.target, amountToPay);
await raacNFT.connect(user2).mint(tokenId, amountToPay);
//c depositnft for user2
await raacNFT.connect(user2).approve(lendingPool.target, tokenId);
await lendingPool.connect(user2).depositNFT(tokenId);
//c first borrow that updates liquidity index and interest rates as deposits dont update it
const depositAmount = ethers.parseEther("100");
await lendingPool.connect(user2).borrow(depositAmount);
await time.increase(365 * 24 * 60 * 60);
await ethers.provider.send("evm_mine");
//c make sure rtoken contract has no assets before transfer
const rtokenassetbal = await token.balanceOf(rToken.target);
await lendingPool.connect(user2).withdraw(rtokenassetbal);
//c user deposits tokens into lending pool to get rtokens
await lendingPool.connect(user1).deposit(depositAmount);
await time.increase(365 * 24 * 60 * 60);
await ethers.provider.send("evm_mine");
await lendingPool.updateState(); //bug if a user checks their balance after time has passed without calling updateState(), it will display their amount without the accrued interest
//c a year has passed and user now wants to withdraw their assets and accrue the interest they have been promised. This is what the docs say "Users that have crvUSD can participate by depositing their crvUSD to the lending pool allowing them to be used for the borrows described above. By doing so, user will receives a RToken that represents such deposit + any accrued interest.". This means at any point, i should be able to redeem my assets and get myy original assets + all my accrued interest which is not the case as I will show
const user1RTokenBalance = await rToken.balanceOf(user1.address);
console.log("user1RTokenBalance", user1RTokenBalance);
const rtokenassetbalprewithdraw = await token.balanceOf(rToken.target);
console.log("rtokenassetbalprewithdraw", rtokenassetbalprewithdraw);
assert(user1RTokenBalance > depositAmount);
await expect(lendingPool.connect(user1).withdraw(user1RTokenBalance)).to
.be.reverted;
});

Impact

Broken Protocol Invariant: The protocol fails to uphold its promise that RTokens represent deposits plus accrued interest.

User Loss: Users may be unable to redeem their RTokens for the full amount, leading to financial losses.

Loss of Trust: Users may lose trust in the protocol, leading to reduced participation and adoption.

Tools Used

Manual Review, Hardhat

Recommendations

To address this issue, the protocol should ensure that sufficient liquidity is always available to honor RToken redemptions.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool faces withdrawal DoS risk due to liquidity mismatch between instantly redeemable RTokens and illiquid NFT-collateralized loans

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool faces withdrawal DoS risk due to liquidity mismatch between instantly redeemable RTokens and illiquid NFT-collateralized loans

Appeal created

anonymousjoe Auditor
7 months ago
io10 Submitter
7 months ago
inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool faces withdrawal DoS risk due to liquidity mismatch between instantly redeemable RTokens and illiquid NFT-collateralized loans

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!