Core Contracts

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

Ownership Parameter Mismatch in LendingPool’s Vault Withdrawal Logic

Summary

This issue involves the LendingPool contract incorrectly using msg.sender instead of its own address (address(this)) as the owner parameter when calling the vault’s withdrawal function. Since the vault recognizes address(this) as the entity that holds the shares (due to the deposit transaction), passing msg.sender violates the expected ownership logic. As a result, the vault either reverts—if it strictly checks ownership—or silently allows withdrawals under incorrect assumptions, potentially compromising the security model of the protocol.

Vulnerability Details

Within the LendingPool contract, the _withdrawFromVault function incorrectly passes msg.sender rather than address(this) as the owner parameter when invoking the vault’s withdraw function. During the deposit phase, address(this) is designated as the share owner. Consequently, calling withdraw with msg.sender causes the vault to reject the transaction if it strictly verifies share ownership. This inconsistency interrupts the expected rebalancing and withdrawal process. If the vault does not validate ownership thoroughly, there is a risk that unauthorized addresses could withdraw shares, compromising the protocol's security model.

Impact

If the vault enforces correct share ownership, any attempt to withdraw using msg.sender instead of address(this) will fail, preventing the LendingPool from accessing necessary liquidity and breaking the rebalancing mechanism. In cases where the vault does not strictly check the owner parameter, an unauthorized address could potentially withdraw shares, compromising the protocol’s security and allowing unintended funds transfer.

Proof of Concept

The _withdrawFromVault function currently calls:

curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));

However, the typical curveVault.withdraw(...) signature expects two critical parameters:

  1. recipient: the address that will receive the withdrawn tokens.

  2. owner: the address holding the shares in the vault, thus authorized to initiate the withdrawal.

When depositing, the contract registers address(this) (the LendingPool contract itself) as the owner of the shares, because the code calls curveVault.deposit(amount, address(this)).
Therefore, when withdrawing, the owner should also be address(this).

In the current code, msg.sender is incorrectly used as the owner. This inconsistency can cause the vault to reject the transaction or, if it unexpectedly accepts it, compromise the security assumption that only address(this) (the legitimate depositor) can withdraw those funds.

Code Analysis

Below is a simplified excerpt highlighting the deposit and withdrawal logic to the vault (_depositIntoVault and _withdrawFromVault) with the finding marked by (!) for clarity:

function _depositIntoVault(uint256 amount) internal {
// Approve 'amount' for the vault
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
// Deposit into the vault, setting 'address(this)' as the owner of the shares
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}
function _withdrawFromVault(uint256 amount) internal {
// (!) Calls the vault using 'msg.sender' as 'owner' when it should be 'address(this)'
curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}

Explanation

  • In _depositIntoVault, the LendingPool deposits funds, thereby becoming the owner of those shares in the vault (curveVault).

  • In _withdrawFromVault, the withdrawal must be initiated by the same entity that owns those shares. If msg.sender is used, the vault will not recognize the caller as the legitimate owner.

This flaw can cause the withdraw(...) call to fail if the vault strictly checks share ownership. Even if it does not fail due to some vault-specific quirk, it breaks the security assumption that only address(this) can manage the deposited funds.

Vulnerable Scenario

  1. A user triggers a function (e.g., _rebalanceLiquidity()) that internally attempts to withdraw funds by calling _withdrawFromVault.

  2. The internal call uses curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));.

  3. The vault detects that msg.sender is not the actual owner of the shares, causing the transaction to fail.

    • Result: Liquidity becomes unavailable, and the rebalancing mechanism breaks.

  4. In a rarer situation, if the vault does not validate ownership properly, it could introduce inconsistencies in share accounting.

Test and Result

The test confirms that when the LendingPool tries to withdraw from the MockVault, it incorrectly uses msg.sender instead of address(this) as the owner parameter. Consequently, the vault reverts with "WrongOwner()". This validates the logic flaw: only the entity that deposited (i.e., address(this)) can withdraw those shares, ensuring consistent ownership.

MockVault is a simplified contract that mimics a typical vault’s deposit and withdrawal mechanisms. It assigns "shares" upon deposit and expects the owner parameter in the withdraw function to match address(this). If a different address attempts to withdraw, it fails with "WrongOwner()". This setup allows to isolate and confirm the incorrect owner parameter usage in the LendingPool.

TestLendingPool inherits from the actual LendingPool and exposes the normally internal functions (_depositIntoVault and _withdrawFromVault) as public (testDepositIntoVault and testWithdrawFromVault). This lets the test directly call those methods and verify that using msg.sender instead of address(this) triggers the expected error in the MockVault.

  • Add the following Mock to contracts/mocks/MockVault.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract MockVault {
mapping(address => uint256) public sharesOf;
function deposit(uint256 amount, address owner) external returns (uint256) {
sharesOf[owner] += amount;
return amount;
}
function withdraw(
uint256 amount,
address recipient,
address owner,
uint256,
address[] calldata
) external returns (uint256) {
require(sharesOf[owner] >= amount, "Not enough shares");
if (owner != address(this)) revert("WrongOwner()");
sharesOf[owner] -= amount;
return amount;
}
}
  • Add the following Mock to contracts/mocks/TestLendingPool.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {LendingPool} from "contracts/core/pools/LendingPool/LendingPool.sol";
contract TestLendingPool is LendingPool {
constructor(
address _reserveAssetAddress,
address _rTokenAddress,
address _debtTokenAddress,
address _raacNFTAddress,
address _priceOracleAddress,
uint256 _initialPrimeRate
)
LendingPool(
_reserveAssetAddress,
_rTokenAddress,
_debtTokenAddress,
_raacNFTAddress,
_priceOracleAddress,
_initialPrimeRate
)
{}
// Public function that exposes _depositIntoVault for testing purposes
function testDepositIntoVault(uint256 amount) external {
_depositIntoVault(amount);
}
// Public function that exposes _withdrawFromVault for testing purposes
function testWithdrawFromVault(uint256 amount) external {
_withdrawFromVault(amount);
}
}
  • Add the following test to test/unit/core/pools/LendingPool/LendingPool.test.js

describe("CurveVault Withdrawal Logic", function () {
let lendingPool, owner, user1, mockVault;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
// Deploy the TestLendingPool contract using dummy addresses for dependencies.
const TestLendingPool = await ethers.getContractFactory("TestLendingPool");
const initialPrimeRate = ethers.parseUnits("0.1", 27);
lendingPool = await TestLendingPool.deploy(
owner.address, // reserveAssetAddress (dummy)
owner.address, // rTokenAddress (dummy)
owner.address, // debtTokenAddress (dummy)
owner.address, // raacNFTAddress (dummy)
owner.address, // priceOracleAddress (dummy)
initialPrimeRate
);
await lendingPool.waitForDeployment();
// Deploy the MockVault contract that simulates the behavior of a Curve vault.
const MockVaultFactory = await ethers.getContractFactory("MockVault");
mockVault = await MockVaultFactory.deploy();
await mockVault.waitForDeployment();
// Set the MockVault instance in the LendingPool contract.
await lendingPool.connect(owner).setCurveVault(mockVault.target);
// Simulate a deposit in the MockVault to assign shares to user1.
const depositAmount = ethers.parseEther("100");
await mockVault.deposit(depositAmount, user1.address);
});
it("should revert when using the wrong owner parameter", async function () {
const depositAmount = ethers.parseEther("100");
// Call testWithdrawFromVault from user1.
// This internally invokes _withdrawFromVault, which mistakenly passes msg.sender (user1)
// as the 'owner' parameter instead of the expected address(this).
// As a result, the MockVault should revert with "WrongOwner()".
await expect(
lendingPool.connect(user1).testWithdrawFromVault(depositAmount)
).to.be.revertedWith("WrongOwner()");
});
});
LendingPool
CurveVault Withdrawal Logic
✔ should revert when using the wrong owner parameter

Confirmation

This test conclusively demonstrates that the withdrawal mechanism fails if the vault expects address(this) as the owner, yet the LendingPool passes msg.sender instead. The vault reverts with "WrongOwner()", confirming the misalignment in ownership parameters and validating the logical error in _withdrawFromVault.

Tools Used

Manual Code Review was the primary approach, focusing on the vault interaction code paths and parameter usage during internal deposit and withdrawal processes.

Recommendations

Ensuring the LendingPool contract always uses its own address (address(this)) instead of msg.sender when withdrawing from the vault will resolve the ownership discrepancy.

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;
}
  • Remove the call that passes msg.sender as the owner.

  • Add address(this) as the correct owner parameter to maintain consistent ownership logic with the deposit flow.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month 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 about 1 month 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.