ACryptoS Exploit: A One-Item Array Drained $665K
Originally published at panda-security.xyz On 8 November 2026, the concentrated-liquidity vaults of the yield aggregator ACryptoS were drained of about $665,429 across 77 of 126 vaults on five chains: Arbitrum, BNB Chai
Originally published at panda-security.xyz
On 8 November 2026, the concentrated-liquidity vaults of the yield aggregator ACryptoS were drained of about $665,429 across 77 of 126 vaults on five chains: Arbitrum, BNB Chain, Polygon, Base and Optimism. There was no flash loan, no oracle trick and no price manipulation. The attackers simply called depositMulti with an amounts array that had one entry instead of two. That single missing length check made a two-token vault value itself as if it held only its first token, so a deposit of a few dollars could mint most of a vault's shares.
According to the project's own post-mortem, 135 addresses exploited the bug between 02:02 and 16:22 UTC. Deposits were disabled on every vault by 16:40 UTC, withdrawals stayed open, and ACryptoS announced that the protocol is being deprecated. This article walks through the root cause, why the attack could be repeated round after round, and what vault builders should take from it.
What Happened
ACryptoS ran vault families on top of Uniswap V3, SushiSwap V3, PancakeSwap V3 and Biswap V3 pools. Each vault held a concentrated-liquidity position in a two-token pool and issued shares to depositors. Users could deposit through depositMulti(uint256[] amounts, uint256 minShares) and exit through withdrawMulti, which pays out a pro-rata slice of both tokens.
The post-mortem gives the loss by chain, priced at about 17:00 UTC on 8 November:
| Chain | Vaults hit | Extracted |
|---|---|---|
| Arbitrum | 28 of 40 | $230,213 |
| BNB Chain | 12 of 33 | $190,845 |
| Polygon | 11 of 16 | $134,686 |
| Base | 13 of 18 | $69,813 |
| Optimism | 13 of 16 | $39,871 |
| Linea | 0 of 3 | $0 |
The total excludes about 19.4K ACS, the project's own token, taken from a BNB Chain vault that the team did not price. The losses fell on ordinary depositors: their shares were diluted by the over-minted ones, and their tokens went to the exploiters.
The first exploit landed at 02:02 UTC on an Arbitrum WBTC/WETH vault from address 0x5dc8df8d0d6cfae0443bb8d9eacdfbb72b2ef322, which the post-mortem says was funded through a Mayan Swift cross-chain order shortly before. Over the following hours the same pattern appeared on BNB Chain, Polygon, Base and Optimism. From about 09:30 UTC, more than 100 copycat addresses, many of them EIP-7702 delegated wallets, ran identical one-shot rounds on every chain. The bug was reported to the project on Telegram at 09:34 UTC, but the developer was only reached at 14:19 UTC. From there, the team closed the first chain in 80 minutes and all chains within 2 hours 21 minutes, using one beacon upgrade per vault family.
The Root Cause: Trusting the Length of a Caller's Array
The bug is small enough to state in one sentence: depositMulti never checked that amounts.length matched the number of tokens in the vault.
That matters because the same array drove two calculations. When a user passed one amount to a two-token vault:
- The deposit was valued using token0 only. For a token0-only deposit, that is correct.
- The vault's existing holdings were also valued using token0 only. Everything the vault held in token1 was ignored.
The second point is the bug. Share minting compares the value of a deposit to the value of the vault. If the vault's value is understated, each deposit buys too many shares. In a vault whose position sat almost entirely in token1, for example a position that had moved out of range, the "token0-only" value of the vault was tiny. A small token0 deposit could then be worth several times "the vault" and mint most of its shares.
withdrawMulti had no such blind spot. It paid out shares pro rata across both tokens. So the attacker entered at a price that ignored token1 and exited at a price that included it.
A simplified model (illustrative, not ACryptoS's code)
The Solidity below is a simplified sketch to show the pattern. It is not the protocol's source.
// Simplified and illustrative only
function depositMulti(uint256[] calldata amounts, uint256 minShares) external {
uint256 depositValue;
uint256 vaultValue;
// Loop bound comes from the caller's array, not the vault's token count
for (uint256 i = 0; i < amounts.length; i++) {
tokens[i].safeTransferFrom(msg.sender, address(this), amounts[i]);
depositValue += valueOf(i, amounts[i]);
vaultValue += valueOf(i, holdings(i)); // token1 skipped if length == 1
}
uint256 shares = totalSupply == 0
? depositValue
: depositValue * totalSupply / vaultValue;
require(shares >= minShares, "slippage");
_mint(msg.sender, shares);
}
The fix in a design like this is one line at the top of the function:
require(amounts.length == tokens.length, "bad amounts length");
Even better, the vault's own value should be computed in a separate function that always iterates over the vault's full token list, so the caller's input can never shape how the vault prices itself.
The fork proof
The post-mortem includes a reproduction on a Polygon fork at block 96940861, just before the first deposit on the Polygon USDC/USDT0 vault. Before the attack, the vault held about 4.96 USDC and 72,825 USDT0, which is exactly the lopsided state the bug needs. The results:
-
depositMulti([10000000]), a 10 USDC deposit with a one-element array, minted 66.84% of the share supply. -
depositMulti([10000000, 0]), the same 10 USDC with a correctly sized array, minted about 0.01%. - The round trip put in 10 USDC and took out about 9.99 USDC plus 48,634 USDT0, matching the on-chain transaction.
The only difference between a normal deposit and the exploit was the length of the array. Pool prices were not touched, and the team says the flaw had existed since the vaults were first deployed rather than being introduced by a recent upgrade.
Why Each Vault Could Be Hit Again and Again
Each attack round was two transactions about one to two minutes apart:
- Approve token0, then call
depositMulti([small token0 amount], 0)to mint most of the vault's shares. Note theminSharesof zero: no slippage bound was needed because the attacker was the one being overpaid. - Call
withdrawMulti(allShares, false, [0, 0])to receive the deposit back plus most of the vault's token1, minus a 0.1% withdrawal fee in shares.
Because each round takes a fixed fraction of what is left, a vault never "runs out" of exploitability after one hit. The post-mortem notes that the first Polygon vault lost 67%, then 67%, then 90% of its remaining balance in successive rounds. This is also why copycats were profitable hours after the first attacker: the condition the bug needs, a vault holding little token0, stayed true after every round.
The exploit was also trivially scriptable. The post-mortem says about 1,000 deposits were identified by decoding their calldata, with an amounts array shorter than two as the signature, and another 220 or so went through attacker contracts or EIP-7702 wallets. It adds that 28 addresses each hit at least three chains with identical rounds, which suggests a shared script.
Funds and Response
Most proceeds were swapped into ETH, WETH or WBNB and bridged, according to the post-mortem, with less than $1K in freezable stablecoins left in exploiter wallets. It names Mayan, Across, Squid/Axelar, Relay and LayerZero/Ethena as the routes used. Several large exploiter wallets still held WETH or WBNB at the time of writing, and the team asked exchanges to flag them. The OAK incident record notes reports of a 10% bounty offer for returned funds, which it could not find in the project's own post, and no returns at last report.
On 9 November the team also upgraded one Base vault where a single exploiter still held 99.99% of the shares and had started draining it. They burned the exploiter's shares and excess fee shares, restoring the pre-exploit share supply. About 1,800 USDbC had been lost before the burn, and 6,187 USDbC was left for the original depositors to withdraw.
The response itself was well built: an upgradeable beacon per family meant one transaction closed a whole family, each upgrade was dry-run on a mainnet fork first, and the patch auto-pauses each vault on its next transaction. The weak point was the five-hour gap between the first public report and reaching the developer, while copycats piled in.
Lessons for Vault Builders
Validate array lengths against protocol state, not caller intent. Any function that takes per-token arrays must check them against the vault's own token count. This is the cheapest check in Solidity and one of the most often skipped.
Never let user input shape how the vault values itself. Total vault value should come from one internal function that always covers every asset the vault holds. If the caller's input can change which assets are counted, share pricing is attacker-controlled.
Test the asymmetric states. The bug only paid well when a vault was heavily skewed toward token1, which is a normal condition for an out-of-range concentrated-liquidity position. Fuzzing deposits and withdrawals across skewed balances, with invariants like "a deposit followed by an immediate withdrawal cannot return more value than it put in", would flag this class of error.
Make every entry point agree on value. Here, deposit and withdraw disagreed about what a share was worth. A round-trip invariant test between paired functions catches this directly.
Have a reachable emergency path. Upgrade design worked well once the developer was reached. Monitoring that auto-pauses on abnormal share minting, plus an on-call contact that does not depend on one person, would have closed most of the copycat window.
We have seen share and value mispricing drain vaults before: the Premia V3 empty option pool exploit, the Loopscale Genesis Vault NAV exploit and the Moonwell and Mamo donation supply-cap case.
What Users Should Do
ACryptoS says deposits are permanently disabled, withdrawals remain open and no relaunch is planned, so remaining depositors are being told to withdraw. Use only the project's official channels, and treat unsolicited "recovery" or refund offers as phishing. This analysis is technical commentary, not financial advice.
Sources
- ACryptoS post-mortem: root cause, fork proof, per-chain losses, timeline, exploiter addresses and response
- 0xCERT-2026-0329: advisory citing SlowMist's record of about $665,429 across 77 vaults
- OAK incident record: independent summary, bounty reports and announcement timing
- crypto.news: coverage of SlowMist's attribution and what depositors can still withdraw
- Altcoin Reporter: coverage of the shutdown and fork reproduction
Closing Thoughts
The ACryptoS exploit did not need any market manipulation. It needed one input the contract forgot to check: how many items were in an array. When that input also decided how the vault valued its own holdings, a 10 USDC deposit could buy two-thirds of a vault. If you build vaults or any contract that mints shares against pooled assets, talk to our team or check our audit pricing to have input validation and share accounting reviewed before someone else tests them for you.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.