Immunefi Audit Competitions
Folks Finance: Staking Contracts

69540 sc insight missing return value on withdraw and missing view function for withdrawable amount

Submitted on Mar 15th 2026 at 13:12:58 UTC by @hcrlen for Audit Comp | Folks Finance: Staking Contracts

Description

Brief/Intro

The withdraw function returns nothing despite computing amountToClaim and rewardToClaim internally. Additionally there is no public view function to query the current withdrawable amount for a stake, forcing users and integrators to replicate the accrual math off-chain.

Vulnerability Details

Issue 1: withdraw returns nothing

function withdraw(uint8 stakeIndex) external nonReentrant {
    _withdraw(stakeIndex);
}

_withdraw computes:

uint256 amountToClaim = accruedAmount - userStake.claimedAmount;
uint256 rewardToClaim = accruedReward - userStake.claimedReward;

But these values are never returned. The caller has no way to know how much they received without:

  • Parsing the Withdrawn event
  • Checking token balance before and after

Every other state-changing function returns useful data:

function stake(...) external returns (uint8)
function stakeWithPermit(...) external returns (uint8)
function addStakingPeriod(...) external returns (uint8)
function migratePositionsFrom(...) external returns (UserStake[] memory)

withdraw is the only user state-changing function that returns nothing.

Issue 2: No view function for withdrawable amount

_getAccrued is internal:

function _getAccrued(uint256 amount, uint256 duration, uint256 elapsed) internal pure returns (uint256) {
    return Math.mulDiv(amount, Math.min(elapsed, duration), duration);
}

Users and integrators cannot query current withdrawable amount on-chain. They must:

  • Call getUserStake(user, stakeIndex) to get raw stake data
  • Manually compute _getAccrued off-chain
  • Subtract claimedAmount and claimedReward

This makes it harder for frontends to display accurate withdrawable amounts and for other contracts to integrate with the staking contract.

Impact Details

Users and integrators cannot determine withdrawable amounts on-chain, and callers of withdraw receive no feedback on how much was claimed.

References

Add any relevant links to documentation or code

Proof of Concept

Observe every other user-facing state-changing function returns useful data

// stake - returns stakeIndex so user knows which index was assigned
function stake(...) external returns (uint8)

// stakeWithPermit - returns stakeIndex
function stakeWithPermit(...) external returns (uint8)

// migratePositionsFrom - returns migrated stakes array
function migratePositionsFrom(...) external returns (UserStake[] memory)

withdraw returns nothing despite computing meaningful values

function withdraw(uint8 stakeIndex) external nonReentrant {
    _withdraw(stakeIndex); // computed values never returned
}

_withdraw computes amountToClaim and rewardToClaim internally but discards them

uint256 amountToClaim = accruedAmount - userStake.claimedAmount;
uint256 rewardToClaim = accruedReward - userStake.claimedReward;

activeTotalStaked -= amountToClaim;
activeTotalRewards -= rewardToClaim;
userStake.claimedAmount += amountToClaim;
userStake.claimedReward += rewardToClaim;

emit Withdrawn(msg.sender, stakeIndex, amountToClaim, rewardToClaim);
TOKEN.safeTransfer(msg.sender, amountToClaim + rewardToClaim);
// nothing returned

A contract integrating with withdraw cannot know how much was claimed

User must rely on Withdrawn event

_getAccrued is internal not callable externally

function _getAccrued(uint256 amount, uint256 duration, uint256 elapsed) internal pure returns (uint256) {
    return Math.mulDiv(amount, Math.min(elapsed, duration), duration);
}

getUserStake(user, stakeIndex) returns only raw stake data with no computed withdrawable amount

 struct UserStake {
        uint256 amount;
        uint256 reward;
        uint256 claimedAmount;
        uint256 claimedReward;
        uint32 aprBps;
        uint64 stakeTime;
        uint64 unlockTime;
        uint64 unlockDuration;
    }

To compute withdrawable amount, user must manually replicate off-chain

// user must replicate this entire calculation manually
uint256 elapsed = block.timestamp - userStake.unlockTime;
uint256 accruedAmount = Math.mulDiv(
    userStake.amount,
    Math.min(elapsed, userStake.unlockDuration),
    userStake.unlockDuration
);
uint256 withdrawable = accruedAmount - userStake.claimedAmount;

No public on-chain function exists to compute this users, frontends, and integrating contracts must replicate _getAccrued logic off-chain

  • Off-chain rounding differences vs on-chain result
  • Incorrect withdrawable amount displayed to users
  • Integrating contracts unable to query withdrawable amount before calling withdraw