FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: low
Likelihood: low

Missing generic rescue function leads to permanently stuck ERC20 tokens sent by mistake

Author Revealed upon completion

Root + Impact

Description

  • Normal Behavior:
    The ConfidencePool contract handles the staking and sweeping of a specific, designated stakeToken. Funds related to this specific token are managed securely through the pool's lifecycle states.

  • Specific Issue/Problem:
    Smart contracts frequently receive unsupported ERC20 tokens sent by users either by mistake or due to front-end errors. Because the ConfidencePool contract lacks a generic rescueTokens administrative function, any ERC20 token other than the designated stakeToken that is accidentally sent to the pool will be permanently locked. There is no mechanism for the pool owner or the recovery address to retrieve these stuck funds.

// Root cause: The contract only handles `stakeToken`
// @> No fallback or rescue function exists for unsupported tokens
IERC20 public stakeToken;

Risk

Likelihood:

  • Reason 1 // Users interacting directly with the contract or using a buggy UI accidentally transfer incorrect tokens (e.g., USDC instead of the designated protocol token).

Impact:

  • Impact 1 // Permanent loss of funds for the users.

  • Impact 2 // The tokens remain stuck inside the contract balance forever with no administrative bypass to recover them.

Proof of Concept

Explanation:
The PoC below illustrates a scenario where a user accidentally transfers random ERC20 tokens to the ConfidencePool address. Because there is no withdrawal function for non-stake tokens, the tokens are permanently stuck, resulting in a total loss of those assets.

function test_TokensPermanentlyStuck() public {
// 1. User accidentally sends an unsupported token (e.g., randomToken) to the pool
ERC20Mock randomToken = new ERC20Mock("Random", "RNDM");
randomToken.mint(user, 1000 ether);
vm.prank(user);
randomToken.transfer(address(pool), 1000 ether);
// 2. The tokens are now in the pool's balance
assertEq(randomToken.balanceOf(address(pool)), 1000 ether);
// 3. There is no function in ConfidencePool.sol to extract `randomToken`.
// The owner cannot rescue it, and the sweep function only works for `stakeToken`.
// The 1000 ether of randomToken is permanently lost.
}

Recommended Mitigation

Explanation:
Add a rescueTokens function restricted to the contract owner. This function must explicitly prevent the withdrawal of the core stakeToken to maintain the protocol's trust model, while allowing the owner to return accidentally sent unsupported tokens to their rightful owners.

+ error CannotRescueStakeToken();
+
+ function rescueTokens(address tokenAddress, address to, uint256 amount) external onlyOwner {
+ if (tokenAddress == address(stakeToken)) revert CannotRescueStakeToken();
+ IERC20(tokenAddress).safeTransfer(to, amount);
+ }

Support

FAQs

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

Give us feedback!