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

Overloaded `InvalidAmount` error prevents integrators from distinguishing four distinct failure conditions

Author Revealed upon completion

Root + Impact

Overloaded InvalidAmount error prevents integrators from distinguishing four distinct failure conditions

Description

  • Custom errors are meant to let callers and front-ends react to the specific reason a call reverted.

  • The single InvalidAmount error is reverted from seven sites for four semantically different conditions — a zero input amount, "nothing to withdraw", "already claimed", and "nothing eligible to claim" — so an integrator cannot tell them apart from the revert alone.

// src/ConfidencePool.sol — four different meanings, one error
function stake(uint256 amount) external ... {
@> if (amount == 0) revert InvalidAmount(); // (1) zero input
}
function withdraw() external ... {
@> if (amount == 0) revert InvalidAmount(); // (2) nothing to withdraw
}
function claimSurvived() external ... {
@> if (hasClaimed[msg.sender]) revert InvalidAmount(); // (3) already claimed
@> if (userEligible == 0) revert InvalidAmount(); // (4) nothing eligible
}
function claimExpired() external ... {
@> if (hasClaimed[msg.sender]) revert InvalidAmount(); // (3) already claimed
}

Risk

Likelihood:

  • Triggered on every occurrence of these common user paths (re-claim, empty withdraw, zero-amount input).

Impact:

  • A front-end or integrating contract cannot distinguish "you already claimed" from "you passed amount 0", degrading error handling and UX. No funds are at risk.

Proof of Concept

The test file ConfidencePool.qa.t.sol includes a characterization test test_QA1_invalidAmountIsOverloaded that explicitly reproduces the four distinct failure scenarios and confirms they all revert with the identical IConfidencePool.InvalidAmount.selector custom error.

Step-by-Step Reproduction

  1. Zero Input Amount (stake(0)):
    alice tries to stake 0 tokens. The transaction reverts with IConfidencePool.InvalidAmount.selector because amount == 0.

  2. Empty Withdraw (withdraw()):
    alice (who has not staked anything) calls withdraw(). The transaction reverts with IConfidencePool.InvalidAmount.selector because the staker's eligible balance is zero.

  3. Nothing Eligible to Claim (claimSurvived()):
    The registry moves to PRODUCTION, the pool is flagged as SURVIVED, and bob (who never staked) calls claimSurvived(). The transaction reverts with IConfidencePool.InvalidAmount.selector because his userEligible stake is zero.

  4. Already Claimed (claimSurvived()):
    carol stakes and claims her funds successfully in a survived pool. When she tries to claim a second time, the transaction reverts with the exact same IConfidencePool.InvalidAmount.selector because hasClaimed[carol] is true.

Test Code

function test_QA1_invalidAmountIsOverloaded() external {
// (a) zero input amount
vm.prank(alice);
vm.expectRevert(IConfidencePool.InvalidAmount.selector);
pool.stake(0);
// (b) nothing to withdraw (caller never staked)
vm.prank(alice);
vm.expectRevert(IConfidencePool.InvalidAmount.selector);
pool.withdraw();
// (c) nothing eligible to claim
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.prank(bob); // bob never staked
vm.expectRevert(IConfidencePool.InvalidAmount.selector);
pool.claimSurvived();
// (d) already claimed — SAME error as (a)-(c), the overload
ConfidencePool p2 = _freshSurvivedPoolWithStaker(carol);
vm.prank(carol);
p2.claimSurvived();
vm.prank(carol);
vm.expectRevert(IConfidencePool.InvalidAmount.selector);
p2.claimSurvived();
}

Running the Proof of Concept

Run the test suite using forge:

forge test --match-test test_QA1_invalidAmountIsOverloaded

The test passes, confirming that all four distinct and independent failure conditions revert with the exact same InvalidAmount error selector, making it impossible for frontends or on-chain integrations to differentiate between them programmatically.

Recommended Mitigation

+ error AlreadyClaimed();
+ error NothingToClaim();
+ error NothingToWithdraw();
function withdraw() external nonReentrant {
- if (amount == 0) revert InvalidAmount();
+ if (amount == 0) revert NothingToWithdraw();
}
function claimSurvived() external nonReentrant {
- if (hasClaimed[msg.sender]) revert InvalidAmount();
+ if (hasClaimed[msg.sender]) revert AlreadyClaimed();
- if (userEligible == 0) revert InvalidAmount();
+ if (userEligible == 0) revert NothingToClaim();
}

Support

FAQs

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

Give us feedback!