Glamorous Plum Baboon
Medium
The rescueTokens
function in the contract allows a privileged user (the onlyPoolAdmin
) to rescue tokens from the pool by specifying the token, recipient, and amount. However, the amount
parameter lacks adequate limits or validation, allowing an admin to rescue all tokens from the contract, including tokens intended for reserves or other purposes. This issue introduces the risk of centralized abuse or accidental mismanagement of funds.
The rescueTokens
function is designed to provide a mechanism for administrators to recover mistakenly sent or trapped tokens from the pool. While the intention of the function is valid, the implementation does not enforce any constraints or validations on the amount
parameter. As a result:
- Unintended Risks: An admin could inadvertently rescue all tokens of a specific type, even those critical to the protocol's operation (e.g., reserve tokens or liquidity pool assets).
- Centralized Abuse: A malicious admin could misuse this functionality to drain funds from the pool.
The issue stems from the absence of logic to validate the type of token and the maximum amount that can be rescued.
The function does not validate the amount
parameter or ensure that the tokens being rescued are non-essential to the protocol's functionality. The relevant line of code (LoC) is as follows:
PoolLogic.executeRescueTokens(token, to, amount);
This line directly calls the executeRescueTokens
function in the PoolLogic
contract without any prior checks to ensure that:
- The
amount
is within reasonable limits. - The
token
being rescued is not a reserve or critical asset.
The lack of validation exposes the protocol to two main risks:
- Fund Mismanagement: An admin could accidentally rescue all tokens of a given type, disrupting the protocol's liquidity or operations.
- Centralized Abuse: A malicious admin could intentionally drain all tokens from the contract, leading to significant financial loss for users.
This issue increases the protocol's exposure to both intentional and accidental misuse by privileged actors.
Consider the following scenario:
- Assume the contract holds 1,000 USDC as reserves for liquidity purposes.
- The
onlyPoolAdmin
executes the following call:rescueTokens(USDC, adminAddress, 1000);
- All 1,000 USDC tokens are transferred to the admin’s address, leaving the protocol without reserves.
The lack of validation allows such a call to succeed, demonstrating the issue.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
contract RescueTokensTest is Test {
YourContract public contractInstance;
ERC20 public usdc;
address public admin = address(0xABCD); // Admin address
address public recipient = address(0x1234); // Recipient address
function setUp() public {
// Deploy mock USDC token
usdc = new ERC20("Mock USDC", "USDC");
// Deploy the main contract and set the admin
contractInstance = new YourContract();
contractInstance.setPoolAdmin(admin);
// Mint 1,000 USDC to the contract
vm.startPrank(admin); // Ensure admin calls the setup
usdc.transfer(address(contractInstance), 1000 ether);
vm.stopPrank();
}
function testRescueTokensVulnerability() public {
// Assert that the contract initially holds 1,000 USDC
assertEq(usdc.balanceOf(address(contractInstance)), 1000 ether);
// Admin rescues 1,000 USDC
vm.startPrank(admin);
contractInstance.rescueTokens(address(usdc), recipient, 1000 ether);
vm.stopPrank();
// Assert that the recipient now holds the rescued 1,000 USDC
assertEq(usdc.balanceOf(recipient), 1000 ether);
// Assert that the contract's USDC balance is now zero
assertEq(usdc.balanceOf(address(contractInstance)), 0);
}
}
NB: import your contract path to the test, i forgot to add mine
-
Setup Phase (
setUp
):- Deploys a mock USDC token using OpenZeppelin's
ERC20
implementation. - Deploys the main contract (
YourContract
) and sets the admin. - Mints and transfers 1,000 USDC to the contract.
- Deploys a mock USDC token using OpenZeppelin's
-
Test Case (
testRescueTokensVulnerability
):- Ensures the contract initially holds 1,000 USDC.
- Simulates the
rescueTokens
call by the admin to rescue all 1,000 USDC. - Asserts that the recipient receives the 1,000 USDC.
- Validates that the contract's balance of USDC is reduced to zero, leaving it without reserves.
-
Outcome:
- This test highlights the vulnerability by showing that the admin can transfer all tokens without any validation, leaving the contract devoid of reserves.
To address this vulnerability, the following changes are recommended:
-
Validate the
amount
Parameter:
Add logic to ensure that theamount
parameter does not exceed a predefined limit or percentage of the token's balance in the contract.Example:
require(amount <= token.balanceOf(address(this)) / 10, "Exceeds rescue limit");
-
Restrict Token Types:
Maintain a whitelist or blacklist of tokens that can be rescued. For instance, reserve tokens critical to the protocol’s functionality should not be rescueable. -
Review by Multi-Sig:
Require additional checks, such as approvals by multiple administrators, before executing therescueTokens
function.
By implementing these measures, the protocol can significantly reduce the risk of abuse or accidental mismanagement while retaining the ability to rescue tokens when necessary.