Core Contracts

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

Incorrect Share Burning in Vault Withdrawals

Summary

When withdrawing funds from the Curve Vault via LendingPool::_withdrawFromVault, the function burns the shares of msg.sender instead of LendingPool. Since the user never deposited directly into the vault (only the LendingPool did), they do not possess any shares, causing a revert. This affects both deposit and withdrawal operations.

Vulnerability Details

Issue Breakdown

  1. Deposits:

    • LendingPool deposits funds into the vault.

    • The vault mints shares to LendingPool in exchange.

    • Users do not directly interact with the vault and do not receive shares.

  2. Withdrawals:

    • The function _withdrawFromVault(uint256 amount) calls:

      curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
    • Here, msg.sender is expected to burn their vault shares to redeem crvUSD, but msg.sender never received any shares—LendingPool did.

    • This results in a revert due to insufficient shares.

POC

get the vyper contract from https://etherscan.io/address/0xd8063123BBA3B480569244AE66BFE72B6c84b00d

put this contract in the contracts folder of the protocol

edit the contract as follows :

replace the existing def init(): with the below one :

@external
def __init__():
# Set `asset` so it cannot be re-initialized.
# self.asset = self
pass

now there exists a parameter in this contract known as deposit_limit by default it is 0 and inorder to accept the deposits, it must be set to uint256 max but can only be done by certain roles.

On trying to set the roles was unable to do so due to lack of vyper code knowledge, decided to skip past this role part by commenting out the below code in our pasted contract,

commenting this code would just stop checking how much is LendingPool allowed to actually deposit which does not play any important role for this bug :

@internal
def _deposit(recipient: address, assets: uint256, shares: uint256):
...
...
# @> assert assets <= self._max_deposit(recipient), "exceed deposit limit"
...
...
...

Next, for testing purpose make the LendingPool::_depositIntoVault & LendingPool::_withdrawFromVault functions public in LendingPool.sol.

now paste the below code in Lendpool.test.js :

describe("LordsTest", function () {
it("deposit crvUSD and reedem shares", async function () {
// deploying curveVault-YearnVault contract
const YearnVault = await ethers.getContractFactory("YearnVault");
let curveVault = await YearnVault.deploy();
await curveVault.waitForDeployment();
await curveVault.initialize(token, "crvUSD-shares", "crvShares", lendingPool.target, 31536000);
// set the above deployed vault
// onlyOwner of LendingPool could do it
const tx = await lendingPool.setCurveVault(curveVault.target);
await tx.wait();
// minting some crvUSD to LendingPool - [ instead this should come naturally in the contract ]
await token.mint(lendingPool.target, ethers.parseEther("100"));
// depositing in the vault via LendingPool
await lendingPool._depositIntoVault(ethers.parseEther("100"));
// checking that wether the correct crvUSD been deducted from LendingPool.sol
expect(await token.balanceOf(lendingPool.target)).to.equal(ethers.parseEther("0"));
// checking that wether the correct crvUSD been transfered from Curve Vault
expect(await token.balanceOf(curveVault.target)).to.equal(ethers.parseEther("100"));
// checking that wether the correct crvUSD shares been transfered from Curve Vault to LendingPool for depositing its assets
expect(await curveVault.balanceOf(lendingPool.target)).to.equal(ethers.parseEther("100"));
// Next lets try withdrawing, our assets back by burning LendingPool shares
// the below code tells that when tries to burn shares of `msg.sender` fails because it does not have shares
await expect(lendingPool._withdrawFromVault(ethers.parseEther("100"))).to.be.revertedWith(
"insufficient shares to redeem"
);
});
});

Impact

  1. Withdrawals Always Fail:

  • Since msg.sender never owns any shares, all withdrawal attempts fail.

  • This affects _rebalanceLiquidity() and _ensureLiquidity(amount), leading to liquidity issues.

  • Depositors Cannot Withdraw Their Funds:

  1. Even if LendingPool has enough funds, the incorrect share-burning mechanism prevents withdrawal.

Tools Used

Manual Review
Hardhat Testing

Recommendations

Modify _withdrawFromVault to burn LendingPool's shares instead of msg.sender:

function _withdrawFromVault(uint256 amount) internal {
- curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
+ curveVault.withdraw(amount, address(this), address(this), 0, new address[](0));
totalVaultDeposits -= amount;
}
Updates

Lead Judging Commences

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

LendingPool::_withdrawFromVault incorrectly uses msg.sender instead of address(this) as the owner parameter, causing vault withdrawals to fail

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

LendingPool::_withdrawFromVault incorrectly uses msg.sender instead of address(this) as the owner parameter, causing vault withdrawals to fail

Support

FAQs

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

Give us feedback!