← All sample reports
sample-org/contracts/StakingVault.sol·234 LOC · scanned May 22, 2026, 11:34 AM
2 HIGH1 MED4 LOW3 INFO

Sample report — staking vault

A realistic AEDSC report on a typical staking vault. 2 HIGH (reentrancy + unchecked return) + 1 MED + 4 LOW. Classic patterns we still see weekly in 2026.

Recommendation

Two ship-blockers. Both have one-line fixes. Do not deploy to mainnet until F-01 (reentrancy in withdraw) and F-02 (unchecked reward token transfer) ship. Everything else is hardening and gas.

  1. HIGHF-01 · reentrancy-eth (slither) + confirmed by mythrilStakingVault.sol:97–104

    Reentrancy in withdraw() — external call before state update

    withdraw() sends ETH to msg.sender BEFORE setting balances[msg.sender] to 0. A malicious contract can re-enter withdraw() inside its fallback and drain the vault. Both Slither and Mythril flag this with HIGH confidence.

    StakingVault.solvulnerable
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "nothing to withdraw");
        (bool ok, ) = msg.sender.call{value: amount}('');
        require(ok, "send failed");
        balances[msg.sender] = 0;
    }
    suggested patch drop-in fix
      function withdraw() external {
          uint256 amount = balances[msg.sender];
          require(amount > 0, "nothing to withdraw");
    +     balances[msg.sender] = 0;
          (bool ok, ) = msg.sender.call{value: amount}('');
          require(ok, "send failed");
    -     balances[msg.sender] = 0;
      }
    Fix: Apply checks-effects-interactions: set balances to 0 BEFORE the external call. Add ReentrancyGuard.nonReentrant as a belt-and-braces if your codebase has cross-function shared state.
    reentrancy-ethCWE-841SWC-107
  2. HIGHF-02 · unchecked-transferStakingVault.sol:146

    Reward token transfer return value ignored in claim()

    claim() calls rewardToken.transfer(msg.sender, pending) without checking the return value. If the reward token is later configured to be USDT or any non-reverting ERC-20, a silent failure leaves the user thinking they were paid while pending is reset.

    StakingVault.solvulnerable
    pendingRewards[msg.sender] = 0;
    rewardToken.transfer(msg.sender, pending);
    suggested patch drop-in fix
    - rewardToken.transfer(msg.sender, pending);
    + rewardToken.safeTransfer(msg.sender, pending);
    Fix: Use SafeERC20.safeTransfer or require(rewardToken.transfer(...)).
    unchecked-transfererc20
  3. MEDIUMF-03 · missing-input-validationStakingVault.sol:61

    Reward rate setter has no upper bound

    setRewardRate(uint256 _rate) accepts any value. A compromised owner could set it to type(uint256).max and drain the reward pool on the next claim() call.

    StakingVault.solvulnerable
    function setRewardRate(uint256 _rate) external onlyOwner {
        rewardRate = _rate;
    }
    Fix: require(_rate <= MAX_REWARD_RATE) with MAX_REWARD_RATE pinned to ~2× expected. Move the setter behind a timelock for mainnet.
    centralization-risk
  4. LOWF-04 · timestampStakingVault.sol:182

    Use of block.timestamp for reward accrual

    Reward accrual uses block.timestamp. Miners can shift by ±15s. For seconds-granularity reward rates this is negligible; flag for awareness only.

    StakingVault.solvulnerable
    uint256 elapsed = block.timestamp - lastUpdate;
    Fix: Accept (drift is bounded). For ultra-precise scheduling consider block.number with a known average block time.
    timestamp
  5. LOWF-05 · missing-zero-checkStakingVault.sol:55

    Missing zero-address check on setRewardToken()

    setRewardToken accepts address(0) silently.

    StakingVault.solvulnerable
    rewardToken = IERC20(_token);
    Fix: require(_token != address(0), 'zero token').
    missing-zero-check
  6. LOWF-06 · storage-packingStakingVault.sol:12–18

    Storage slot ordering wastes one SSTORE

    Two uint128 followed by a uint256 cost 2 storage slots instead of the minimum 1.5. Reordering saves ~20k gas per deposit().

    StakingVault.solvulnerable
    uint128 public rewardRate;
    uint256 public lastUpdate;
    uint128 public rewardPool;
    Fix: Reorder so two uint128 sit together (one slot), with uint256 separate (one slot).
    gas-optimizationstorage-packing
  7. LOWF-07 · calls-loopStakingVault.sol:208–215

    Unbounded loop in distributeRewards() can hit block gas limit

    distributeRewards() iterates over stakers[] with no cap. If stakers.length grows past ~500, the function reverts on out-of-gas, locking rewards.

    StakingVault.solvulnerable
    for (uint256 i = 0; i < stakers.length; i++) { ... }
    Fix: Use pull-payments (each user calls claim()) instead of push. Standard pattern for staking.
    dos-with-block-gas-limit
  8. INFOF-08 · pragmaStakingVault.sol:1

    Floating pragma

    pragma solidity ^0.8.20

    StakingVault.solvulnerable
    pragma solidity ^0.8.20;
    Fix: Pin to 0.8.24.
    pragma
  9. INFOF-09 · external-functionStakingVault.sol:190

    Internal function visibility could be private

    _settle() is internal but only called from within the contract. private is more restrictive and signals intent.

    StakingVault.solvulnerable
    function _settle() internal { ... }
    Fix: function _settle() private { ... }
    style
  10. INFOF-10 · event-coverageStakingVault.sol:97, 142

    No Withdrawn / Claimed events on hot paths

    withdraw() and claim() change user state without emitting events, making off-chain indexing harder.

    StakingVault.solvulnerable
    // (no event)
    Fix: emit Withdrawn(msg.sender, amount); emit Claimed(msg.sender, pending);
    events

Get one of these for your contract.

Free for the first scan. Founder Pro €29/mo with a 7-day free trial unlocks unlimited scans, priority queue, and a human triaging every report — rate locked for life.