0x5d16b8ba2a9a4eca6126635a6ffbf05b52727d50EthereumAn unprivileged adversary drained Sorra's public staking pool by repeatedly calling withdraw(1) on a matured tier-0 staking position. Each withdrawal paid the full matured reward of the original deposit again while reducing principal by only one smallest unit, allowing the same reward to be realized hundreds of times. The exploit removed 3071721792764835962145225 SOR from the staking contract and converted the proceeds into ETH. The originating EOA ended the exploit transaction with 4.693714741705678851 ETH net profit.
The reward token is SOR at 0xe021baa5b70c62a9ab2468490d3f8ce0afdd88df. The victim staking contract is sorraStaking at 0x5d16b8ba2a9a4eca6126635a6ffbf05b52727d50. SOR traded in a live public Uniswap V2 pool against WETH at 0xa15c4914be0b454b0b7c27b4839a4a01da8ed308, so an attacker could permissionlessly acquire inventory and later unwind it.
The critical staking configuration was tier 0: a 14-day vesting period with 500 basis points reward. sorraStaking exposed public deposit(uint256,uint8) and withdraw(uint256) entrypoints. No privileged roles, private keys, or attacker-specific infrastructure were required; the exploit depended only on public liquidity, public contract calls, and waiting for maturity.
The root cause is reward-accounting failure in . The contract computes the full matured reward before reducing the caller's principal, and the reward path does not net out previously distributed rewards. That makes payout depend on the original matured deposit rather than the amount actually being withdrawn in the current call. A caller can therefore withdraw unit of principal and still receive the reward for the entire matured position. Because the remaining principal is almost unchanged after each call, the same reward remains available on the next call. This violates the basic invariant that cumulative rewards across withdrawals must stay bounded by the remaining matured principal and any newly accrued reward.
sorraStaking.withdraw(uint256)1The priming transaction was 0x72a252277e30ea6a37d2dc9905c280f3bc389b87f72b81a59aa8f50baebd8eaa. Its trace shows helper contract 0xfa39257c629f9a5da2c0559debe2011eef7c1e9f approving sorraStaking and depositing its full SOR balance into tier 0:
0xFa39257C629F9A5DA2c0559deBe2011eEF7C1E9f::deposit(100000000000000000000000, 0)
SorraV2::balanceOf(...) -> 122868871710593438486048
SorraV2::approve(sorraStaking, 122868871710593438486048)
sorraStaking::deposit(122868871710593438486048, 0)
emit Depositx(user: 0xFa39257C629F9A5DA2c0559deBe2011eEF7C1E9f, amount: 122868871710593438486048)
The same trace records the tier-0 position metadata, including deposit time 0x6766ad53 and reward basis points 500. After the 14-day vesting period elapsed, transaction 0x6439d63cc57fb68a32ea8ffd8f02496e8abad67292be94904c0b47a4d14ce90d executed the drain. Consecutive trace entries show the exploit primitive repeating:
sorraStaking::withdraw(1)
emit Withdraw(user: 0xFa39257C629F9A5DA2c0559deBe2011eEF7C1E9f, amount: 1)
SorraV2::transfer(..., 6143443585529671924280)
emit RewardDistributed(user: 0xFa39257C629F9A5DA2c0559deBe2011eEF7C1E9f, amount: 6143443585529671924279)
The position-related storage values decrement by one unit between calls while each payout remains reward-sized. That is the core exploit condition: principal reduction is negligible relative to the reward being paid. The validated code-level breakpoint is that rewardAmount = getPendingRewards(_msgSender()) is evaluated before the position update, and _calculateRewards does not checkpoint prior reward distributions. As a result, the contract keeps pricing the position as though the original matured reward were still fully claimable.
The adversary EOA was 0xdc8076c21365a93aac0850b67e4ca5fdec5fab9b. The traces show it calling helper contract 0xfa39257c629f9a5da2c0559debe2011eef7c1e9f, which delegated attack-loop logic to 0xb575b2599b9dcf242bb9dca60dc2ad36a1ca8cd7.
The execution flow was:
sorraStaking tier 0.withdraw(1) hundreds of times.This satisfies the ACT model because every step uses public contracts, public state, and permissionless entrypoints.
The exploit transaction balance diff shows sorraStaking losing 3071721792764835962145225 SOR. The adversary EOA's native balance increased from 0.464323822924538962 ETH to 5.158038564630217813 ETH, for a net increase of 4.693714741705678851 ETH after gas and direct validator payment. Additional fee-recipient ETH inflows were secondary consequences of liquidating stolen SOR through the live market rather than a separate exploit path.
0x72a252277e30ea6a37d2dc9905c280f3bc389b87f72b81a59aa8f50baebd8eaa0x6439d63cc57fb68a32ea8ffd8f02496e8abad67292be94904c0b47a4d14ce90d0x5d16b8ba2a9a4eca6126635a6ffbf05b52727d500xe021baa5b70c62a9ab2468490d3f8ce0afdd88df0xa15c4914be0b454b0b7c27b4839a4a01da8ed308