Reproduced Exploit

Beraji KO: deligateStake pulls 1000 from a victim allowance into staking

deligateStake does safeTransferFrom(_account, address(this), _amount). _account comes from the caller. After a user or the owner approves StakingKo, anyone can front-run the intended call and stake that allowance. Here the victim approves 1000 and the attacker stakes it. The tokens land in staking…

Mar 2025•Other•access-control•3 min read

Chain

Other

Category

access-control

Date

Mar 2025

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 49513-h-01-attacker-can-redirect-funds-to-staking-via-allowances-s. Standalone Foundry PoC and full write-up: 49513-h-01-attacker-can-redirect-funds-to-staking-via-allowances-s_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/access-control/missing-auth · vuln/logic/missing-check

Key info#

ProtocolBeraji KO
FindingShieldify H-01, AuditVault 49513
Vulnerable codeStakingKo.deligateStake
HarmAn attacker calls deligateStake for a victim who has approved the staking contract and pulls 1000 KO into staking. The control requires the caller to be that account, so the attacker is rejected and the victim keeps 1000 until the victim stakes it
Chainlocal synthetic

TL;DR#

deligateStake does safeTransferFrom(_account, address(this), _amount). _account comes from the caller. After a user or the owner approves StakingKo, anyone can front-run the intended call and stake that allowance. Here the victim approves 1000 and the attacker stakes it. The tokens land in staking and the stake is credited to the victim, who did not choose the lock. The control reverts unless msg.sender == _account. The victim's own call then stakes the remaining 1000.

The vulnerable code#

SOLIDITY
require(_amount != 0, "amount = 0");
Pool storage pool = pools[_poolId];
// @> VULN: from is the caller-supplied _account, so any open allowance is staked by anyone
pool.stakingToken.safeTransferFrom(_account, address(this), _amount);

The fix uses the caller's funds, or requires the caller to be _account.

Root cause#

The allowance is granted to the staking contract, and transferFrom spends it as the staking contract no matter who called deligateStake. Nothing binds the caller to _account. An open approval for a later stake, createReward, or depositRewardAmount can be consumed as a stake instead.

Preconditions#

The victim holds 2000 KO and approves both staking contracts for 1000. The attacker is a different address. The amount is 1000. run() does not warp.

Attack walkthrough#

  1. Mint 2000 KO to the victim and approve StakingKo for 1000.
  2. The attacker calls deligateStake(victim, 1000).
  3. safeTransferFrom moves 1000 from the victim into staking and credits staked[victim].
  4. The same call against the control reverts with not owner. The victim still holds 1000.
  5. The victim calls deligateStake for itself. The owner check passes and the remaining 1000 is staked.

Diagrams#

flowchart TD A["Victim approves StakingKo for 1000"] --> B["Attacker calls deligateStake with account = victim"] B --> C["VULN: transferFrom pulls the victim, not the caller"] C --> D["1000 KO lands in staking and is credited to the victim"] D --> E["Control requires msg.sender == account"] E --> F["Attacker call reverts and the victim keeps 1000"] F --> G["Victim stakes that 1000 itself"]

Impact#

1000 KO that the victim approved for a later action is staked by the attacker. The victim has to wait out the lock before those tokens can be used as rewards or as the stake the victim actually wanted. The control does not move the allowance unless the victim is the caller, and that owner call does stake 1000.

How to reproduce#

BASH
cd 49513-h-01-attacker-can-redirect-funds-to-staking-via-allowances-s_exp
forge test -vv

run() does not warp. StakingKo ends holding 1000 of the victim's KO. The control staking receives 1000 only from the victim's own call.

Sources#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.