Core Contracts

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

Tokens can not be rescued from the RToken contract

Summary

The RToken contract has a rescueToken function to recover non-main assets sent to it accidentally, but this function can only be called by the Reserve Pool (LendingPool). However, the LendingPool lacks any mechanism to trigger this rescue functionality, effectively leaving any mistakenly sent tokens permanently locked in the RToken contract.

Vulnerability Details

The issue stems from a misalignment in the permission model between the RToken and LendingPool contracts. Let's analyze this in detail:

RToken's rescue mechanism:

// In RToken.sol
function rescueToken(
address tokenAddress,
address recipient,
uint256 amount
) external onlyReservePool {
if (recipient == address(0)) revert InvalidAddress();
if (tokenAddress == _assetAddress) revert CannotRescueMainAsset();
IERC20(tokenAddress).safeTransfer(recipient, amount);
}

This function is designed to rescue non-main assets, but it's restricted to be called only by the Reserve Pool (LendingPool) through the onlyReservePool modifier:

modifier onlyReservePool() {
if (msg.sender != _reservePool) revert OnlyReservePool();
_;
}

LendingPool's rescue mechanism:

// In LendingPool.sol
function rescueToken(
address tokenAddress,
address recipient,
uint256 amount
) external onlyOwner {
require(
tokenAddress != reserve.reserveRTokenAddress,
"Cannot rescue RToken"
);
IERC20(tokenAddress).safeTransfer(recipient, amount);
}

The LendingPool's rescueToken function can only rescue tokens directly held by the LendingPool contract. It has no mechanism to trigger RToken's rescue function.

The core issue arises because:

  • RToken expects LendingPool to coordinate token rescues

  • LendingPool has no functionality to coordinate with RToken's rescue mechanism

  • The ownership and permission model doesn't align between the contracts

  • There's no administrative bypass or alternative rescue path

This creates a situation where tokens accidentally sent to the RToken contract become permanently locked unless they are the main asset (crvUSD in this case).

PoC

  1. Alice accidentally sends 50 USDC tokens to the RToken contract address

  2. The RToken contract has a rescueToken function, but it requires LendingPool to call it.

  3. LendingPool has no mechanism to call RToken's rescueToken function

  4. The USDC tokens are now permanently locked in the RToken contract

  5. Even the protocol owner cannot rescue these tokens as both contracts' rescue mechanisms are misaligned

Impact

Any non-main asset tokens accidentally sent to the RToken contract will be permanently locked with no recovery mechanism available. This could lead to permanent loss of user funds if they accidentally send tokens to the RToken contract address.

Tools Used

Manual review

Recommendations

Two possible solutions:

Modify RToken's rescueToken to be callable by owner instead of ReservePool:

function rescueToken(
address tokenAddress,
address recipient,
uint256 amount
) external onlyOwner {
if (recipient == address(0)) revert InvalidAddress();
if (tokenAddress == _assetAddress) revert CannotRescueMainAsset();
IERC20(tokenAddress).safeTransfer(recipient, amount);
}

Add a function in LendingPool to trigger RToken's rescue mechanism:

function rescueTokenFromRToken(
address tokenAddress,
address recipient,
uint256 amount
) external onlyOwner {
IRToken(reserve.reserveRTokenAddress).rescueToken(
tokenAddress,
recipient,
amount
);
}
Updates

Lead Judging Commences

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

RToken::rescueToken() can never be called

Support

FAQs

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