Back to Blog

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly

Code Auditing
April 29, 2026
24 min read
Key Insights

During the past week (2026/04/20 - 2026/04/26), BlockSec detected and analyzed eight attack incidents, with total estimated losses of ~$7.04M. The table below summarizes these incidents, and detailed analyses for each case are provided in the following subsections.

Date Incident Type Estimated Loss
2026/04/19* Custom Rebalancer Contract Arbitrary Call ~$64K
2026/04/20 REVLoans (Juicebox) Improper Validation ~$50.7K
2026/04/22 Volo Vault / Navi Key Compromise ~$3.5M
2026/04/22 Kipseli Router Improper Validation ~$72.35K
2026/04/23 GiddyDefi Incomplete Signature Validation ~$1.3M
2026/04/25 Purrlend Key Compromise ~$1.5M
2026/04/26 SingularityFinance Oracle Misconfiguration ~$413K
2026/04/26 Scallop Accounting Flaw ~$142.7K

*The Custom Rebalancer Contract incident was not covered in last week’s report and is included here for completeness.

Get Started with Phalcon Explorer

Dive into Transactions to Act Wisely

Try now for free

Weekly Highlight: GiddyDefi

The attacker did not crack the signature, did not use a flash loan, and did not manipulate any price. They replayed a legitimate signature with its unsigned fields swapped for their own contract. "Use your own signature against you" is the cleanest demonstration of how partial EIP-712 coverage turns a valid signature into a generic permit.

On April 23, 2026, GiddyVaultV3 on Ethereum was exploited for approximately $1.3M. The signature scheme covered only SwapInfo.data, leaving aggregator, fromToken, toToken, and amount outside the EIP-712 hash, so a valid signature could be replayed with those fields tampered. The attacker pointed aggregator at a malicious contract and fromToken at the strategy's LP token, draining approximately $1.3M.

Background

GiddyVaultV3 (0x5f0a...4318) is a yield farming vault contract where users deposit and withdraw funds via deposit() and withdraw(). Every operation must carry a VaultAuth authorization struct signed by the backend, which includes an EIP-712 signature and a SwapInfo[ ] array describing the token swap routes. When executing a swap, the contract calls GiddyLibraryV3.executeSwap(), which performs a forceApprove on swap.fromToken granting allowance to swap.aggregator, then executes the swap via aggregator.call(swap.data). The strategy contract subsequently manages the funds according to its configured strategy.

EIP-712 is a standard for signing structured off-chain data: the protocol that consumes the signature reconstructs the same struct on-chain, hashes it under an agreed domain separator, and recovers the signer's address. The security of any EIP-712 flow therefore depends on the on-chain hash covering every field whose value affects execution. In Giddy's design, the backend signs a VaultAuth that contains both the user's intent and the routing instructions for any required swaps, and _validateAuthorization() reconstructs that struct to verify the signature before the strategy is allowed to move funds.

Vulnerability Analysis

The vulnerability lies in the _validateAuthorization() function of GiddyVaultV3. When constructing the signed payload, only the data field of each SwapInfo is hashed; aggregator, fromToken, toToken, and amount are all excluded from the signature. This means anyone in possession of a valid signature can freely replace the remaining fields of SwapInfo while still passing signature verification.

Each excluded field is a separate lever the signature leaves free: aggregator becomes both the spender and the call target via forceApprove and aggregator.call(swap.data); fromToken selects which strategy asset is approved; amount sets the allowance ceiling; toToken only feeds a returnAmount > 0 check. The signed data does not constrain any of these because none of these targets are referenced inside it.

Attack Analysis

The following analysis is based on the transaction 0x5edb66...5482e5.

  • Step 1: The attacker obtained a legitimately backend-authorized VaultAuth signature from on-chain, keeping the data field intact. Because every prior deposit() or withdraw() call broadcasts the full VaultAuth payload on-chain, any historical transaction was a free source of a reusable signature; the attacker only needed one whose data field was suitable for the intended swap call.

  • Step 2: Using the obtained signature, the attacker kept the original signature, nonce, and data unchanged while tampering with the remaining fields. fromToken was set to the LP Token held by the strategy contract (a real asset), so the forceApprove granted allowance over a token the protocol actually held. aggregator was replaced with the attacker's malicious contract, so both the approval and the subsequent aggregator.call() were directed at attacker-owned code. Since these fields were outside the signature validation scope, _validateAuthorization() accepted the tampered struct unchanged. To bypass the final require(returnAmount > 0, "SWAP_NO_TOKENS_RECEIVED") check, the malicious aggregator implemented a mint function that minted fake tokens back to the protocol, satisfying the check without performing any real swap.

  • Step 3: Since the malicious aggregator had been granted approval in step 2, the attacker called transferFrom to move the vault's LP tokens directly into the malicious aggregator, completing the theft. This step lay outside the protocol's protected execution path entirely; by the time executeSwap() returned, the allowance had already been written and the post-call balance check had already passed, so the protocol had no further opportunity to intervene.

Conclusion

The root cause of this attack was incomplete EIP-712 signature coverage. The core fields of SwapInfo that directly govern fund flow were left unprotected, allowing the attacker to substitute the swap route and aggregator address while presenting a valid signature. Developers integrating external aggregators should:

  • Ensure EIP-712 signatures cover all fields that affect execution outcomes, including aggregator, fromToken, toToken, and amount.

  • Enforce an aggregator whitelist to prevent calls to unaudited external contracts.

  • Restrict toToken to expected base tokens to prevent fake tokens from bypassing balance checks.

More broadly, EIP-712 in any approval-then-call architecture must hash every field that influences the resulting on-chain state, not just the user-facing intent. Whenever a backend signature is the sole gatekeeper between user-supplied parameters and a privileged contract action, every parameter that flows into that action (call target, asset, amount, recipient) must sit inside the signed struct. Treating data as a proxy for the call's identity is a category error: the call's identity is the tuple of all its parameters, and any parameter left outside the signature is, by definition, controlled by whoever submits the transaction.

Best Security Auditor for Web3

Validate design, code, and business logic before launch


More Incidents This Week


Custom Rebalancer Contract

On April 19, 2026, an sAVAX rebalancer contract on Avalanche was exploited to draw approximately $64K (~7,000 WAVAX) from a user's Aave V3 credit delegation. A public function ran an arbitrary target.call(data) while still holding the user's delegation, so the attacker could invoke Aave's borrow() with onBehalfOf set to the victim. A whitehat bot frontran the exploit and recovered the funds before any withdrawal.

Background

The rebalancer contract (0x7a7b...a8c9) exposes a function b2a13230() designed to rebalance a user's leveraged position on Aave. The function operates on behalf of the user via Aave V3 credit delegation: the user grants the rebalancer permission to borrow on their behalf, and the rebalancer combines those borrowings with user-supplied funds to adjust the position (e.g., a borrow + supply workflow).

Vulnerability Analysis

The root cause is that b2a13230() includes a step target.call(data) whose target and calldata are both caller-controlled. This call runs while the contract is still operating under the user's Aave V3 credit delegation, so any logic invoked during that step inherits the user's borrowing power. There is no allowlist of permitted targets and no shape constraint on the calldata, so the call can invoke any contract method, including Aave's borrow() with onBehalfOf set to the user.

Attack Analysis

The following analysis is based on the transaction: 0xaaa1b2...35001b.

  • Step 1: The attacker performed a flashloan for an amount of sAVAX and USDC. Then supplied the borrowed USDC into Aave V3 via the rebalancer contract to establish sufficient collateral for borrowing. Meanwhile, the borrowed sAVAX was transferred directly to the rebalancer contract to prepare for the subsequent supply step after borrowing.

  • Step 2: The attacker invoked the function b2a13230(). The function first performed a normal borrow operation, then reached the arbitrary call section. At this point, the attacker crafted the call to directly invoke Aave V3's borrow() function with onBehalfOf set to the victim address. Since the victim had granted credit delegation to the rebalancer contract, the borrow succeeded. The borrowed WAVAX was transferred into the rebalancer contract.

  • Step 3: The attacker invoked the function b2a13230() again, this time using the rebalancer to borrow WAVAX on behalf of themselves. The contract then used the previously borrowed WAVAX (originating from the victim's position) to supply into the attacker's position and repay, allowing the attacker to extract profit.

Conclusion

The defect is the combination of an arbitrary external call inside a privileged context that holds delegated credit. Either layer alone would be safe: a constrained external call cannot misuse the delegation, and an arbitrary call without delegation cannot move the user's funds. Contracts that hold credit delegation should never expose an arbitrary external call; if such calls are necessary, targets must be pinned to an allowlist and calldata shape-checked.


REVLoans (Juicebox)

On April 20, 2026, REVLoans, a borrowing extension on Juicebox, was exploited on Ethereum for approximately $50.7K. borrowFrom() accepted a caller-supplied accounting source without verifying it was registered with the protocol; a forged 36-decimal context triggered a same-currency shortcut that mis-rescaled balances by 1e18. Two transactions, one to seed the inflated accounting entry and one to borrow against the legitimate pool at the inflated share price, drained 21.77 ETH.

Background

Juicebox is a hybrid fundraising and lending protocol on Ethereum. Each project has its own ERC20 share token (referred to here as REV) and a treasury split across one or more terminals, where a terminal is the contract that physically custodies a subset of the project's assets and acts as the user-facing entry/exit point. One project can have multiple terminals registered against it in JBDirectory, and every (terminal, project, token) triple carries a JBAccountingContext declaring the (decimals, currency) used for that token's bookkeeping inside that terminal. REV is therefore a claim on the union of surpluses across all terminals of the project, not a claim against any single terminal.

A user can deposit an asset into a terminal in exchange for freshly minted REV, or redeem REV at a terminal for a proportional share of its surplus (modulo a configurable cash-out tax that leaves some value behind for remaining holders). REVLoans (0x2db6...1846), a separate contract layered on top, adds a borrow facility: a user burns REV as collateral and draws a loan against one of the project's terminals, with the loan repayable later in exchange for re-minting the collateral. The borrow amount is priced with the exact same math as a redemption, so a borrow is economically equivalent to cashing out the same collateral.

REV's share price is (totalSurplus + totalBorrowed) / (REV.totalSupply + totalCollateral). Including totalBorrowed in the numerator keeps borrow/repay price-neutral; it also means an inflated totalBorrowed directly raises the share price and lets a small collateral cash out disproportionately.

Vulnerability Analysis

The root cause is unverified input on the source parameter. borrowFrom() accepts a caller-supplied REVLoanSource source (a struct with fields .terminal and .token) without checking that this pair is registered for the given revnetId. Both fields flow directly into the cash-out math, so the accounting context returned by source.terminal is fully caller-controlled. When that context's currency field matches the destination terminal's, the protocol takes a same-currency shortcut, skips the price oracle, and treats the supplied decimals and balance figures as authoritative.

The unvalidated source is then written into _loanSourcesOf[revnetId] and totalBorrowedFrom[revnetId][source.terminal][source.token] by _addTo(), which likewise performs no registration check.

Once (source, revnetId) is in the bookkeeping, _borrowableAmountFrom() is the function that translates a borrow request into a payable amount. It builds surplus = totalSurplus + totalBorrowed from _totalBorrowedFrom(), then passes that surplus into JBCashOuts.cashOutFrom() together with the caller's collateral count and the share supply.

The decimal bug lives one level deeper, in _totalBorrowedFrom(). It iterates _loanSourcesOf and folds each entry via mulDiv(tokensLoaned, 10**decimals, pricePerUnit). On the same-currency path, pricePerUnit = 10**decimals (the target's 18-decimal precision), so the formula reduces to tokensLoaned unchanged, and a balance stored under 36-decimal accounting lands in the 18-decimal ETH sum 1e18 times too large.

The amplification happens in cashOutFrom(). base = mulDiv(surplus, cashOutCount, totalSupply): with surplus dominated by inflated totalBorrowed, even a tiny cashOutCount (collateral) maps to a disproportionately large payout.

Attack Analysis

The attack uses two transactions. The first pollutes REVLoans's bookkeeping: 0xc46cb7...dead1f. The second drains the pool against a legitimate terminal: 0x9adbd6...a8f938.

  • Step 1: The attacker called borrowFrom() with both terminal and token in the loan source pointing to a fake contract, posting a small amount of REV as collateral. REVLoans did not check whether the supplied terminal is registered for the revnet, nor whether the token is recognized by it.
  • Step 2: REVLoans queried the fake terminal for an accounting context, which returned a forged (decimals=36, currency=ETH-code(61166)). Because the source and target currencies matched, REVLoans took a same-currency shortcut and skipped the price oracle, then ran the cash-out math over the legitimate terminals' real ETH surpluses re-expressed in the attacker's 36-decimal target unit, inflating the figure by 1e18.
  • Step 3: REVLoans registered (fake terminal, fake token) into _loanSourcesOf and wrote the inflated figure into totalBorrowedFrom. The fake terminal "paid out" by simply confirming receipt; no real ETH moved. The first transaction ended with totalBorrowed manipulated upward and only the small REV collateral burned.
  • Step 4: The attacker called borrowFrom() again, this time passing the legitimate ETH terminal as the loan source and a tiny REV collateral. The cash-out math ran in real 18-decimal ETH units.
  • Step 5: While computing totalBorrowed, REVLoans iterated _loanSourcesOf and hit the entry from step 3. Because that entry's currency still matched ETH, the same-currency shortcut fired again and the 36-decimal stored balance was folded into the 18-decimal ETH sum 1e18 times too large. totalBorrowed was now dominated by fake debt and the share-price numerator was massively inflated.
  • Step 6: The cash-out math returned a borrow amount sized to the inflated numerator, which the attacker had pre-tuned to sit just below the legitimate terminal's real surplus. The legitimate terminal paid it out, draining nearly the entire pool to the attacker.

Conclusion

The root cause is two compounding gaps: (terminal, token) pairs are accepted without checking revnet registration, and the same-currency shortcut folds source balances into the destination sum without renormalizing for decimal differences. Either gap alone would be less dangerous; together they let a caller inject an arbitrary totalBorrowedFrom entry and cash it out at face value. Mitigation: validate (terminal, token) against the revnet's registered terminals, and renormalize balances by the source's stored decimal scale before folding.


Volo Vault

On April 22, 2026, Volo, a yield vault on Sui that earns lending yield by routing user deposits into the Navi lending protocol, lost approximately $3.5M after the operator private key was leaked. The vault contract had no code-level bug; the attacker simply ran the legitimate operator path with stolen credentials and drained Volo's Navi deposits.

Background

Volo is the user-facing vault (0xcd86...27fefa); Navi is the underlying lending protocol. The vault holds a Navi AccountCap (a Sui capability object that authorizes withdrawals from Volo's Navi account) and delegates strategy moves to an operator role. To deposit or withdraw on Navi, the operator calls start_op_with_bag_v2() to lift the AccountCap from the vault into a temporary bag, then deposit_with_account_cap() / withdraw_with_account_cap_v2() use that cap to move funds.

Vulnerability Analysis

The root cause is an operational/key-custody failure rather than a contract-level vulnerability. The Volo strategy path delegates withdrawal authority to whoever holds the operator private key: start_op_with_bag_v2() performs only two checks (assert_operator_not_freezed(operation, cap) and assert_single_vault_operator_paired(operation, vault.vault_id(), cap)), both of which only verify that the supplied capability is the registered operator. withdraw_with_account_cap_v2() then accepts any caller that can present the lifted AccountCap. Anyone in possession of the operator private key can therefore execute the same path that legitimate operations use, indistinguishably.

Attack Analysis

The following analysis is based on the transaction AQw9wM...3RUS.

  • Step 1: The attacker called start_op_with_bag_v2 on @volosui/volo-vault::operation with the leaked operator key, lifting the Navi AccountCap into a temporary bag.
  • Step 2: The attacker used bag::remove to extract the AccountCap from the temporary bag.

  • Step 3: The attacker called withdraw_with_account_cap_v2 on @navi-protocol/lending::incentive_v3 with the extracted AccountCap, pulling Volo's deposits off Navi.

  • Step 4: The attacker used bag::add to put the AccountCap back, closed the operation, and transferred the funds out.

Conclusion

The defect is structural: one operator key, full withdrawal authority, no second check. Three changes reduce the damage from a key compromise. Splitting the operator role into a multisig or threshold scheme means a leaked key cannot authorize a withdrawal on its own. Adding a time-lock on outgoing withdrawals gives abnormal calls a contestable window before settlement. Scoping the operator's powers to deposit-and-rebalance only, with user-facing withdrawals routed through a separate path, prevents the operator role from reaching user funds at all.


Kipseli Router

On April 22, 2026, the Kipseli Router on Base was exploited for approximately $72.35K. The router uses a quote returned by an external USDC-only quoter as the raw output-token transfer amount, without verifying that the output token equals the quote token. An attacker swapped 0.04 WETH for cbBTC on a path the quoter does not actually support, receiving the quoter's USDC-scaled return value (92,610,395) as raw cbBTC units (≈0.926 cbBTC).

Background

Kipseli Router (0x579f...9a07) is a swap execution contract backed by an external quoting system. The contract is not open-sourced; the analysis below is based on its decompiled bytecode, which is why function names appear as 4-byte selectors (0xcce096f3(), 0x592(), 0xd88()). Rather than computing swap prices directly from on-chain AMM pools, it queries the quoter for an output amount (amountOut) and then executes the token transfer based on that value. In normal operation, the user sends tokenIn to the protocol wallet, and the router pulls tokenOut from the same wallet and forwards it to the recipient. The protocol is configured with a single QUOTE_TOKEN, and the quoting logic is denominated in USDC using 6-decimal accounting; the system is only designed to support USDC-denominated quotes.

Vulnerability Analysis

The defect spans two layers that compound. On the router side, function 0xcce096f3() retrieves a quote v0 via the quoter function 0x592() and passes it unchanged into 0xd88() as tokenOut.transferFrom(_wallet, receiver, v0). The router never checks that tokenOut equals the protocol's QUOTE_TOKEN, so a USDC-scaled value (6-decimal precision) is transferred as if it were a cbBTC quantity (8-decimal precision). On the quoter side, the underlying PropAMM AMM is designed exclusively for token-to-USDC pairs but accepts unsupported routing paths (WETHcbBTC) without reverting, silently ignoring tokenIn and returning a USDC-scaled value as if the swap were valid.

Attack Analysis

The following analysis is based on the transaction 0x96edee...3db3bb.

  • Step 1: The attacker called the router with tokenIn=WETH and tokenOut=cbBTC. The underlying AMM did not support this path but did not revert, and the quoter 0x592() returned a USDC-scaled value of 92,610,395 (≈92.61 USDC).
  • Step 2: The router used that value directly as the cbBTC transfer amount. 0.04 WETH (≈$95) flowed in via transferFrom; 92,610,395 raw cbBTC units (≈0.926 cbBTC, ≈$72.35K) flowed out from the protocol wallet to the attacker.

Conclusion

The exploit lands because two assumptions are unchecked on either side of the quoter call. The quoter assumes its output is consumed in its own USDC 6-decimal frame; the router assumes whatever the quoter returns is denominated in the requested tokenOut. Either fix removes the bug:

  • On the router: assert tokenOut == QUOTE_TOKEN, or convert the USDC-scaled quote into tokenOut units via an oracle before transfer.

  • On the quoter: revert on routing paths whose tokens are not registered for the supported pair set, instead of silently returning a USDC-scaled fallback.


Purrlend

On April 25, 2026, Purrlend, a lending protocol on HyperLiquid and MegaETH, lost approximately $1.5M after a private key compromise. The attacker took over the bridge role and minted unbacked pTokens (Purrlend's Aave-like receipt tokens), then used those pTokens as collateral to borrow real assets out of the pool.

Background

Purrlend (0x81d5...a702) is a lending protocol with an Aave-like accounting model. When users supply assets into the protocol, they receive corresponding pTokens, similar to Aave's aTokens, which represent their supplied position and can be used as collateral for borrowing other assets.

The protocol also includes privileged roles, including pool admin, risk admin, and bridge. The bridge role is intended for cross-chain accounting: it can mint pTokens to mirror deposits that happened on a counterpart chain. The other admin roles modify risk parameters and configure borrowable assets.

Vulnerability Analysis

The immediate trigger was a privileged key compromise: the attacker obtained the keys controlling Purrlend's admin and bridge roles. A contract-level design flaw amplified the leak: the bridge role's pToken mint path is not anchored to any verifiable proof of cross-chain escrow. The function lets a caller with the bridge role issue pTokens to any address, in any amount, without checking that a corresponding deposit occurred on the source chain. Anywhere else in the protocol, pTokens are treated as valid collateral, and the borrow path does not re-check backing at borrow time. So an unauthorized bridge-role mint translates directly into borrowing power, with no second gate between minting and asset withdrawal.

Attack Analysis

The following analysis is based on the transaction 0xb96cff...dbbf24 on MegaETH.

  • Step 1: The attacker, holding compromised privileged keys, used a MultiSendCallOnly batch via GnosisSafeProxy to set themselves as pool admin, risk admin, bridge, and emergency admin through ACLManager, then enabled WETH as a borrowable asset and set its BorrowCap to 200.
  • Step 2: Acting as the bridge, the attacker minted a large amount of pTokens to their own address. The bridge mint path performed no verification of cross-chain escrow, so the new pTokens had no underlying assets backing them.

  • Step 3: The attacker used the unbacked pTokens as collateral. Because the borrow path treats any pToken balance as a valid supply position without re-checking backing, the collateral check passed and WETH was borrowed out of the pool.

Conclusion

This was a private key compromise amplified by a contract-level design flaw. The leaked keys gave the attacker only the bridge role's intended authority, but that authority included unconstrained pToken minting, which translates directly into borrowable collateral. Each layer can be tightened independently. On the operational layer, split the bridge role into a multisig or threshold scheme so a single key leak cannot exercise it. On the contract layer, require the bridge mint to carry a verifiable proof of escrow (e.g., a message commitment from a trusted cross-chain verifier) and revert when no proof is supplied. Verifying the proof at mint time is the more durable fix because it removes the reliance on key custody altogether.


SingularityFinance

On April 26, 2026, the dynBaseUSDCv3 vault of SingularityFinance on Base lost approximately $413K. The vault was configured with an invalid Uniswap V3 fee tier (42, which does not exist in V3), so every non-USDC asset's price oracle resolved to a non-existent pool. The pricing function returned 0 silently instead of reverting, the vault valued its non-USDC reserves at zero, and an attacker minted nearly the entire share supply by depositing a tiny amount of USDC, then redeemed for the actual underlying assets.

Background

The dynBaseUSDCv3 vault (0x67b9...4dcd) holds multiple yield-bearing tokens and prices non-USDC reserves through Uniswap V3. The pricing helper getPrice(base, fee, quote, amount) resolves the (base, quote, fee) tuple to a Uniswap V3 pool via the factory, then reads the TWAP from that pool. The vault's totalAssets() aggregates the priced reserves; share minting and redemption ratios are derived from this total.

Vulnerability Analysis

The defect is in the early-return branch of getPrice(). When IUniswapV3Factory.getPool(base, quote, fee) returns address(0) (no pool exists for the supplied fee tier), the function falls through and returns its zero-initialized price variable instead of reverting. The vault was deployed with fee=42, which is not one of Uniswap V3's supported tiers (500/3000/10000), so every non-USDC token's lookup hits this branch. totalAssets() therefore sums to roughly the vault's USDC balance only, while the actual yield-bearing tokens contribute zero. Mint and redeem ratios that depend on totalAssets() are computed against this near-zero denominator.

Attack Analysis

The following analysis is based on the transaction 0x00b949...8d3732.

  • Step 1: The attacker flash-loaned approximately 100K USDC.

  • Step 2: The attacker deposited the USDC into the vault. Because totalAssets() only counted the USDC balance, the vault valued itself at roughly the deposit amount and the attacker received nearly 100% of the share supply.

  • Step 3: The attacker redeemed the shares, which distributes the underlying reserves proportionally to share ownership. The attacker received a large fraction of every yield-bearing token the vault held.

  • Step 4: The attacker repaid the flash loan and kept the drained yield-bearing tokens as profit.

Conclusion

Two checks were missing. The deployment did not validate fee=42 against Uniswap V3's supported tiers (500/3000/10000); getPrice() returned 0 on a missing pool instead of reverting. Either fix is sufficient: validate oracle parameters at config time, or revert on getPool() == address(0). As defense-in-depth, share-mint logic should sanity-check totalAssets() against an external reference before accepting deposits.


Scallop

On April 26, 2026, Scallop's staking-rewards program on Sui lost approximately $142.7K. The function that updates a user's accrued rewards did not verify that the rewards-tracking object passed in matched the user's account, letting an attacker pull a fictitious points balance from an abandoned, long-dormant rewards-tracking object and redeem it against the legitimate rewards pool until the balance was drained.

Background

Scallop is a lending protocol on Sui. On top of its lending product, Scallop runs a spool program: users deposit a single asset into Scallop's market to receive MarketCoin<T> (the lending receipt; for SUI deposits this is MarketCoin<SUI>, the on-chain representation of "sSUI"), then stake that MarketCoin into a Spool to earn protocol points over time, which they later redeem against a paired RewardsPool for actual reward tokens. Each Spool is a Sui shared object that tracks a global per-share index; each user holds a personal SpoolAccount recording their staked balance and accrued points.

Vulnerability Analysis

The defect is in spool::user::update_points: the function does not assert account.spool_id == object::id(spool) (nor account.stake_type == spool.stake_type). Sibling entries stake, unstake, and redeem_rewards all perform that binding check at entry; only update_points skips it. Without the check, spool_account::accrue_points computes account.points += stake * (spool.index − account.index) / 1e9 against any Spool passed in, treating its index as if it were this account's own reward stream.

The path becomes exploitable because Sui never garbage-collects shared objects: an abandoned Scallop Spool whose stakes has decayed to dust keeps accruing reward share (per-period increment 1e9 * reward / stakes), so its index cumulatively increases over time and can reach arbitrarily large values. With the binding check missing, update_points can use this inflated index to write a huge points delta into any account. The polluted points then redeem 1:1 against the target spool's RewardsPool, because the account is legitimately bound to that target spool and redeem_rewards's own binding check passes.

Attack Analysis

The following analysis is based on the transaction 6WNDjC...NfVL.

  • Step 1: With 0.2 SUI as bait, the attacker minted MarketCoin<SUI>, then called new_spool_account + stake against the target spool to create a legitimately bound SpoolAccount with account.spool_id = target_spool.

  • Step 2: The attacker called update_points<MarketCoin<SUI>>(donor_spool, account, clock) with donor_spool set to an abandoned Spool. The donor's index (≈8.91e14) was written into the account as points: points = stake * (8.91e14 − 1.19e9) / 1e9 ≈ 1.62e14.

  • Step 3: The attacker called redeem_rewards<MarketCoin<SUI>, SUI>(target_spool, target_rp, account). The binding assertions accepted the target-bound account, the inner re-accrue early-returned, and the polluted points were converted at the rewards pool's 1:1 rate up to its balance: rewards = 150,098,061,595,978 raw SUI.

  • Step 4: The attacker called unstake and redeem to recover the 0.2-SUI bait, then TransferObjects to move everything out.

Conclusion

The fix is to add the same assert!(account.spool_id == object::id(spool)) check at the entry of update_points that stake, unstake, and redeem_rewards already perform. As defense-in-depth, the protocol could also cap the index delta accepted by a single accrue_points call (reject deltas larger than a configured ceiling), so that even if the binding check were bypassed again in the future, no single call could credit an account with a points quantity disproportionate to its actual stake duration.

Get Started with Phalcon Security

Detect every threat, alert what matters, and block attacks.

Try now for free

About BlockSec

BlockSec is a full-stack blockchain security and crypto compliance provider. We build products and services that help customers to perform code audit (including smart contracts, blockchain and wallets), intercept attacks in real time, analyze incidents, trace illicit funds, and meet AML/CFT obligations, across the full lifecycle of protocols and platforms.

BlockSec has published multiple blockchain security papers in prestigious conferences, reported several zero-day attacks of DeFi applications, blocked multiple hacks to rescue more than 20 million dollars, and secured billions of cryptocurrencies.

Sign up for the latest updates
The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis
Security Insights

The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis

This BlockSec deep-dive analyzes the KelpDAO $290M rsETH cross-chain bridge exploit (April 18, 2026), attributed to the Lazarus Group, tracing a causal chain across three layers: how a single-point DVN dependency enabled the attack, how DeFi composability cascaded the damage through Aave V3 lending markets to freeze WETH liquidity exceeding $6.7B across Ethereum, Arbitrum, Base, Mantle, and Linea, and how the crisis forced decentralized governance to exercise centralized emergency powers. The article examines three parameters that shaped the cascade's severity (LTV, pool depth, and cross-chain deployment count) and provides an exclusive technical breakdown of Arbitrum Security Council's forced state transition, an atomic contract upgrade that moved 30,766 ETH without the holder's signature.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026

This BlockSec weekly security report covers four DeFi attack incidents detected between April 6 and April 12, 2026, across Linea, BNB Chain, Arbitrum, Optimism, Avalanche, and Base, with total estimated losses of approximately $928.6K. Notable incidents include a $517K approval-related exploit where a user mistakenly approved a permissionless SquidMulticall contract enabling arbitrary external calls, a $193K business logic flaw in the HB token's reward-settlement logic that allowed direct AMM reserve manipulation, a $165.6K exploit in Denaria's perpetual DEX caused by a rounding asymmetry compounded with an unsafe cast, and a $53K access control issue in XBITVault caused by an initialization-dependent check that failed open. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit