Back to Blog

~$23M Lost: Cosmos EVM, Moonwell Exploits | BlockSec Weekly

Code Auditing
September 4, 2026
26 min read
Key Insights
  • Five incidents this week caused approximately $22.7M in losses across Ethereum, Solana, Base, Cronos, and a further six Cosmos EVM chains such as TAC.

  • Shared code turned two incidents into multi-protocol events. The Cosmos EVM balance-handling flaw exploited six Cosmos EVM chains the same week, including TAC Chain; Rain's outdated card contract exposed Avici, Tria, and other card programs. One defect in common infrastructure can fan out to every vulnerable downstream that runs it.

  • Price manipulation was the most common attack type, behind two of the five incidents and a large amount of the week's losses. The Moonwell and Tectonic exploits took the same shape on Compound-style markets: each inflated both an oracle price and the market's receipt-token exchange rate, then borrowed against the overvalued collateral.

During the reporting period (2026/08/24 - 2026/08/30), we observed 5 security incidents with a total estimated loss of approximately $22.7M.

Date Incident Type Estimated Loss
2026/08/20* The Cosmos EVM Exploit Series (MANTRA, TAC, KiiChain, +3) Arithmetic Underflow/Overflow ~$5.7M*
2026/08/27 Moonwell Price Manipulation ~$9.1M
2026/08/28 Ajna Improper Business Logic ~$775K
2026/08/28 Rain Card Contract Exploit Series (Avici, Tria, and others) Signature Verification Bypass ~$1.1M†
2026/08/30 Tectonic Price Manipulation ~$6M‡

*The Cosmos EVM series began on 08/20 (MANTRA), before this week's reporting period and not covered in last week's report; it is included here for completeness. The ~$5.7M is what attackers realized across the six affected chains (~$2.87M via DEXes and ~$2.85M via centralized exchanges, since frozen), at August-19 prices, per the official Cosmos post-mortem. Nominal drains were larger where a token amount is known (TAC ~3B TAC, ~$7.5M; MANTRA 720.9M tokens, ~$3.6M; KiiChain 148.3M KII), but most tokens went unsold, frozen, or recoverable on-chain.

†The ~$1.1M is the estimated aggregate across the Rain-supported programs exposed by the shared contract; Avici and Tria are the two largest, disclosing ~$500,859 (1,685 users) and ~$431,945 (636 users) respectively.

‡The ~$6M is the realized loss, bridged to Ethereum before the rollback. Estimates of the total drained range from ~$74M (tracked in the attacker's wallets) to ~$119.5M (gross market outflow); the bulk stayed on Cronos and was erased when validators rolled the chain back to its pre-exploit state. Neither Tectonic nor Cronos has confirmed a final loss figure.

Best Security Auditor for Web3

Validate design, code, and business logic before launch

Weekly Highlight: The Cosmos EVM Exploit Series (Traced on TAC Chain)

This series was selected for what it reveals about shared-infrastructure disclosure: because the bug was misjudged as not a serious security threat, its fix shipped as a silent public patch rather than through coordinated private distribution, and a third-party fork then described the exploit path in the open, exposing every unpatched chain still running the module.

Between 2026/08/20 and 08/25, attackers ran a single exploit chain combining two vulnerabilities in the shared cosmos/evm module across six Cosmos EVM chains. Both bugs sat in the code that reconciles EVM state with the Cosmos x/bank ledger: a balance underflow and a matching overflow, chained inside one supply-neutral transaction.

Per the official Cosmos post-mortem, the bug was reported through the bounty program in April and initially misjudged as not threatening production funds, so it went through the silent public-patch process and shipped in v0.6.2 and v0.7.2 on 08/19. On 08/20 a third-party fork publicly described the exploit path, and the first drains began hours later, hitting MANTRA first (08/20), then TAC and KiiChain (08/22) [1][2]. Across all six chains, the attackers realized roughly $5.7M at August-19 prices (about $2.87M sold on DEXes and ~$2.85M on centralized exchanges, since frozen) [1].

This report analyzes TAC Chain in detail as the worked example; it was the hardest-hit chain in the series, with a staking-pool loss of ~$7.5M nominal [3].

Background

TAC Chain runs both the Cosmos SDK and the EVM. A tac1... address and a 0x... address share the same underlying 20 bytes, so a single address can first be created as a Cosmos vesting account and later have an EVM contract deployed to it. These are not two separate accounts: the same address simultaneously carries vesting state and contract code. The two run side by side, and TAC exposes native Cosmos actions such as staking to EVM callers through precompiled contracts at fixed addresses, so an EVM contract can invoke them like any other call.

A vesting account's Bank total balance includes locked tokens. The locked portion cannot be transferred before it vests, while the spendable balance is what remains after subtracting the locked amount from the total. When the EVM loads an account, it initializes the StateDB balance from the spendable balance, and contract transfers during execution operate on that balance.

At the end of a transaction, the balance changes in the StateDB are settled back into x/bank, the authoritative ledger, and only what settles there is real, transferable TAC. TAC runs the v0.7.x line, which writes an EVM balance into x/bank directly, but only if the value survives a uint256-to-int256 conversion, so a balance near 2^256 cannot be settled at all.

Locked tokens cannot be transferred, but they can still be delegated for staking. Delegation checks the Bank total balance, which includes locked tokens, and then keeps the vesting restriction through DelegatedVesting. For a fully locked account with total=1, delegating that unit reduces the Bank total balance to 0 and updates DelegatedVesting, while the spendable balance correctly remains 0 both before and after.

Vulnerability Analysis

The buggy component is the balance synchronization in the shared cosmos/evm handler that runs on every stateful staking precompile call, exposed through the staking precompile at 0x0000...0800 and implemented in [4]. It reconciles EVM balances with the Cosmos ledger through unchecked uint256 arithmetic, and this exposes two complementary defects: a balance underflow on the staking write-back path, and a matching overflow on the ordinary addition path. Neither is dangerous alone; the risk comes from their combination on a shared arithmetic path.

The first defect is an underflow on the balance write-back. When a stateful staking precompile call executes, the native Cosmos action runs between a BeforeBalanceChange and an AfterBalanceChange hook, and AfterBalanceChange is responsible for reflecting the native balance change back into the EVM StateDB.

Delegation validates against the Bank total balance, so a fully locked account can pass the delegation check while its spendable balance stays 0. The native delegation deducts the delegated amount from the Bank total balance and emits a coin_spent event. The defect is in how AfterBalanceChange consumes that event: instead of reloading the account's current spendable balance and assigning it, it replays the coin_spent amount as stateDB.SubBalance(spender, amount), subtracting from the existing EVM balance.

Because SubBalance performs the subtraction with unchecked uint256 arithmetic, any account whose EVM balance is smaller than the event amount underflows. For an account with EVM balance 0 and a coin_spent amount of 1, 0 - 1 wraps to MAX_UINT256.

The second defect is a matching overflow on the addition path. Every balance transfer runs SubBalance on the sender and AddBalance on the recipient through the same StateDB, so both directions share one arithmetic path.

Both resolve to the same stateObject primitives, where AddBalance and SubBalance compute new(uint256.Int).Add(s.Balance(), amount) and .Sub(s.Balance(), amount) with no range check. Just as the subtraction underflows past 0, a large enough addition overflows past MAX_UINT256 and wraps back down.

The two defects are complementary. On its own, the underflow produces a MAX_UINT256 balance that is inert, because a value near 2^256 cannot survive the uint256-to-int256 conversion that settlement into x/bank requires. The unchecked addition is the counterpart: it is the only path that can bring such an oversized balance back below that settlement ceiling.

Attack Analysis

The following analysis is based on the transaction 0xae4e9b...da46fc.

  • Step 1: In transaction 0x4da591...df1af7, the attacker deployed a CREATE2 factory so that the attack contract address 0x5711...c978 could be computed before the contract was deployed.

  • Step 2: In transaction 95F43742...6A885BA, the attacker used MsgCreateVestingAccount to transfer and lock one base unit (1utac) at that future contract address, giving it a Bank total balance that could pass the delegation check while keeping its spendable balance at 0.

  • Step 3: In transaction 0x2400f8...c57c81, the attacker used CREATE2 to deploy the attack contract to the same 0x5711...c978 address, making it both a Cosmos vesting account and an EVM contract so an external account could pay gas while the contract address remained the delegator with spendable=0.
  • Step 4: The attack contract delegated the locked base unit through the staking precompile. The staking flow accepted the delegation against the Bank total balance, and the balance write-back then wrapped the contract's EVM balance to MAX_UINT256.

  • Step 5: The wrapped MAX_UINT256 balance could not be settled back to the Cosmos ledger as-is, so the attack contract first brought it down to a settleable value. It sent almost the entire balance to bonded_tokens_pool, the chain's largest account and the one holding all staked TAC, choosing the amount so that the pool's balance overflowed on addition and wrapped to 0. Because that amount was deducted from the attack contract's own MAX_UINT256, the contract was left holding exactly the pool's former balance, with no new supply created. Zeroing the pool takes its entire balance, the most this overflow could yield, which is why the attacker targeted the chain's largest account in the first place.

  • Step 6: The attack contract then transferred that balance, 2,985,651,403.40 TAC, to the attacker's address.

Our transaction-level analysis matches the official Cosmos post-mortem, which describes two chained vulnerabilities: the underflow above produces the anomalous balance, and the overflow in Attack Analysis Step 5 converts it into the pool's real funds [1]. An earlier KiiChain post-mortem, published before that official report, held that at least three upstream defects were involved and that only the underflow has been patched [2]. Independent press coverage summarized the same disagreement over how many upstream defects remain [5].

Conclusion

The root cause of the Cosmos EVM Exploit Series was an inconsistency in how the EVM and Cosmos layers accounted for the same balance, combined with arithmetic that was never bounds-checked. When two runtimes share one ledger, they must agree on balance semantics down to each individual account, and every balance change must be checked for overflow and underflow. Because the flaw lived in a shared module rather than any single chain's code, one defect exposed every chain running it, which is what turned a single bug into a multi-chain event. Beyond the code, the incident is a lesson in severity assessment. The vulnerability was initially judged not to threaten production funds, so its fix shipped as a silent public patch; by the time that judgment was corrected, the patch was already public, and a third party's disclosure of the exploit path turned the misjudgment into six exploited chains. A shared-infrastructure bug that can move real funds needs private, coordinated distribution from the start, not a silent public patch anyone can decode.

Get Started with Phalcon Explorer

Dive into Transactions to Act Wisely

Try now for free

More Incidents This Week


Moonwell

On 2026/08/27, Moonwell on Base was exploited for about $9.1M by combining collateral-accounting inflation with oracle price manipulation of MAMO, a low-liquidity asset listed in its Core Market. Beyond pushing up the MAMO price, the attacker transferred MAMO directly into the mMAMO market contract without minting shares, which raised each share's backing and inflated the collateral value on top of the price move. Against the doubly inflated collateral the attacker took roughly $11.03M in gross borrows across cbBTC, WETH, USDC, and wstETH, leaving about $9.13M in residual obligations after liquidations [6][7].

Vulnerability Analysis

Moonwell's Core Market runs on Compound v2 code, where one market share (mMAMO) is worth (cash + totalBorrows - totalReserves) / totalSupply. Two weaknesses combine here. First, the MAMO supply cap only checks the formal mint path, so a direct transfer of MAMO into the mMAMO contract adds to the market's cash without minting shares; this raises the computed exchange rate (exchangeRateStored()) and lifts the collateral value of every existing share while bypassing the cap entirely. Second, MAMO was listed as collateral with a 50% collateral factor despite thin liquidity, so its oracle price can be moved with modest capital. Because collateral value is computed as shares times exchange rate times oracle price, both the exchange rate and the price are manipulable surfaces, and inflating them together multiplies borrowing power against far more liquid assets.

Attack Analysis

The following analysis is based on the transaction 0x09687d...395593e. The operation was seeded with about $1.947M (799 ETH converted to USDC and bridged to Base); gross MAMO purchasing volume reached about $7.50M once borrowed assets were recycled.

  • Step 1: The attacker supplied MAMO formally to mint mMAMO, then transferred 53,393,290 MAMO directly into the mMAMO contract in two transactions that emitted no Mint event, raising the market's computed exchange rate about 3.68x and revaluing the mMAMO shares the attacker had just minted along with every other holder's.

  • Step 2: The attacker bought MAMO across DEX pools while liquidity was thin, driving the MAMO/USD feed from about $0.0106 up to about $0.43.

  • Step 3: With both the exchange rate and the price inflated, the attacker completed 18 borrows across cbBTC, WETH, USDC, and wstETH (about $11.03M gross), then converted and consolidated the proceeds, bridging about 8.729M USDC to Ethereum through CCTP and converting it to about 8.728M DAI.

Conclusion

The root cause was that Moonwell valued a low-liquidity collateral asset on two independently manipulable surfaces at once: its oracle price, which thin liquidity let the attacker move with modest capital, and its receipt-token exchange rate, which a direct transfer of the underlying inflated because the supply cap guarded only the formal mint path. Since collateral value multiplies the two, inflating both together multiplied borrowing power against far more liquid assets. A lending market should treat both the price and the share-accounting of collateral as manipulable: exclude unsolicited transfers from the exchange-rate calculation, and pair conservative collateral factors with liquidity-aware pricing and strict supply and borrow caps for thin assets.

Ajna

On 2026/08/28, Ajna was exploited for about $775K across seven Ethereum pools through a business-logic flaw in its liquidation path. Ajna is a lending protocol that uses no external oracle; it prices positions through the LUP (Lowest Utilized Price) derived from bucket liquidity. The attacker first manipulated the LUP to create a severely insolvent position, then had the protocol liquidate a controlled position at a Dutch-auction price the protocol still held far above market, so that bucket deposit claims were consumed at their face value in quote token to repay debt while the attacker received real collateral in return [8].

Background

Ajna is a permissionless protocol where anyone can create a pool of paired tokens for supplying and borrowing, similar to a Uniswap pool. Each pool is divided into buckets, similar to price ticks in Uniswap V3, where each tick represents a fixed amount of quote token per unit of collateral. Lenders choose a bucket according to their preferred loan-to-value ratio and provide liquidity there, receiving LP (a claim on that bucket's deposits) in return. Because there is no external oracle, the LUP is derived directly from the bucket deposit distribution and the total debt.

A position becomes liquidatable once its threshold price (its debt divided by its collateral) rises above the LUP. Anyone can then kick it into liquidation by posting a bond, which opens a Dutch auction. The auction price is not tied to the spot market: it starts at 32 times a reference price and halves every hour, and it is held flat for the first hour (a cure period) before it begins to decay.

Auctioned collateral can be taken in two ways. A caller can take() collateral by paying quote token at the current auction price, or call bucketTake(), which uses a chosen bucket's quote deposits to buy the auctioned collateral and repay the borrower. In a bucketTake(), the collateral is credited into that bucket, so its lenders are the ones acquiring it; for an arbitrage bucket take, the caller is additionally paid LP rewards (a claim on that bucket) on the spread between the bucket price and the auction price as the incentive to trigger the liquidation. The design assumes a taker only steps in once the auction price has fallen to or below what the collateral is worth: with no oracle, this descending auction is how Ajna discovers a fair price, and a bucket's lenders profit by acquiring collateral below their own bucket price. The auction itself ends only when the borrower's debt is cleared or the loan becomes collateralized again; a separate settlement path resolves an auction after a 72-hour grace period, or earlier once no collateral remains, absorbing any residual bad debt and returning leftover collateral to the borrower.

Three implementation details of this machinery determine the figures a take produces. First, the auction price is not market-derived; it decays as 32 * max(kickMomp, neutralPrice) * 2^(-max(elapsedHours - 1, 0)), so it stays flat through the first-hour cure period and only then begins to fall, remaining near 32 times its reference price shortly after that period.

Second, a kick is admitted only when the borrower is already undercollateralized at its pre-penalty debt: _kick() runs the _isCollateralized() check and reverts with BorrowerOk() before it adds the three-month-interest penalty, so that penalty raises the recorded post-kick debt without having helped the position qualify.

Third, the first take on an auction adds a 7% penalty to the borrower's debt before the repayment and collateral amounts for the take are calculated, so a single bucketTake() need not clear the debt on its own.

Vulnerability Analysis

The buggy contract is the Ajna pool (0xad24...178e); the liquidation logic described here is taken from that verified source. Inside bucketTake(), the protocol uses a bucket's deposit claims at their face value in quote token to repay an auction borrower's debt, and, on an arbitrage bucket take, pays the taker LP rewards based on the spread between the bucket price and the auction price.

It never checks the actual recoverable value of those deposit claims or whether the auction price is economically reasonable; it simply assumes the claims can be redeemed later at face value. The two prices it relies on are computed independently, and neither is validated against the other: the LUP follows the bucket deposit distribution and the pool's total debt, while the auction price decays purely on elapsed time from a reference price fixed at kick. A claim's on-chain face value can therefore diverge from what it can actually recover, and bucketTake() still settles debt against that face value.

Internally, bucketTake() routes through _takeBucket() into _rewardBucketTake(), where, on an arbitrage take, the taker's LP reward is computed as the collateral taken multiplied by the spread between the bucket price and the auction price.

Attack Analysis

The following is based on on-chain analysis of the cbETH pool, one of the seven affected, using transactions 0x8a8793...016e64 and 0x12dfde...14e4f5.

  • Step 1: In the setup transaction, the attacker added about 49.343 WETH of quote liquidity to bucket index 2000, a price far above the market. Backed by that inflated bucket, the attacker opened an almost unsecured position, pledging about 0.001 cbETH to borrow about 49.319 WETH. This position holds almost no collateral, so it is not the prize; it serves two setup purposes. First, as an insolvent debt it drags the LUP down once the market returns to normal. Second, because nearly all of the 49.343 WETH deposited into bucket 2000 has now been borrowed back out, leaving only a dust amount, bucket 2000 is left with a deposit claim whose face value, about 49.343 WETH, far exceeds what it can actually recover; that impaired claim is the ammunition the attacker later spends at face value in Step 4.
  • Step 2: In the same setup transaction, the attacker used a second controlled address, 0x02d329...6f5f, to open a borrower position at the normal price level, pledging about 48.128 cbETH of collateral and borrowing about 49.319 WETH against it. This returned the market price to a normal range, leaving the first position severely insolvent while the second position sat just around its health threshold. This second position, which holds the real collateral, is the one the attacker actually intends to drain; the insolvent first position is only the lever that moved the LUP.

  • Step 3: The attacker then kicked the second position (the one opened by 0x02d329...6f5f) into auction rather than the insolvent first one. A kick is only accepted if the position is already undercollateralized at its pre-penalty debt against the current LUP, and the insolvent first position had by now dragged the LUP low enough that the second position's accrued debt met that bar. Only after the eligibility check does the protocol add a three-month-interest kick penalty, which is why the recorded post-kick debt comes to about 49.362 WETH. The kick opened the position's Dutch auction.

  • Step 4: The attacker waited until just after the one-hour cure period, when the auction price had only begun to decay and was still close to 32 times the reference price. Calling bucketTake() on the second position at bucket 2000, the same bucket now holding the impaired claim, liquidated it at that still-high, far-above-market price. Because this was the first take on the auction, the protocol added a 7% penalty to the borrower's debt before calculating the take's repayment and collateral amounts. The collateral is priced at the auction price, so consuming about 49.34 WETH of bucket 2000's deposit repaid only part of that debt and removed only about 1.51 cbETH of collateral, leaving most of the collateral untouched while the caller collected a large amount of LP rewards on the spread.
  • Step 5: The attacker redeemed those LP rewards through removeCollateral(), taking part of the profit out as collateral.

  • Step 6: To close out, the attacker took a Balancer flash loan and called Ajna's take() to pay the residual debt the bucketTake() had left, this time with real quote token, until the borrower's debt reached zero and the auction exited. A final repayDebt() call then withdrew the now-unencumbered collateral, about 46.51 cbETH, with quoteRepaid=0: no quote token was returned for it. The gain came at the pool's expense. The shortfall did not disappear, it moved: with the second position closed, the first position's still-unpaid, near-unbacked loan remained in the pool as bad debt borne by the other lenders.

Conclusion

The root cause is that Ajna's liquidation path settles an auction borrower's debt against a bucket's deposit claims at face value, without checking their real recoverable value or whether the auction price is economically reasonable. That missing check is what let a manufactured impaired claim be spent at face value to drain real collateral, leaving the shortfall in the pool as bad debt. The liquidation path should value deposit claims by their real recoverable amount rather than face value, confirm the auction price is economically reasonable before completing a take, and guard against consuming claims that a pool's existing bad debt has already impaired. The affected pool contracts are immutable and expose no administrative pause, so once the drain began there was no way to halt it; users could only exit on their own.

The Rain Card Contract Exploit Series (Traced on Avici)

On 2026/08/28 (UTC), an outdated version of Rain's shared Solana card-collateral program was exploited through an Ed25519 signature-verification bypass. By making the program accept a fabricated admin approval, the attacker took control of user collateral accounts and drained the token balances held in them. Because the flaw sat in program code shared across Rain-powered card programs, a single bug exposed all of them at once: the exploit drained an estimated ~$1.1M in total, with Avici and Tria the two largest, disclosing about $500,859 (1,685 users) and $431,945 (636 users) respectively [9].

The analysis below uses Avici as the worked example.

Background

On Solana, signature checks are performed by a native Ed25519 precompile as part of transaction processing: if a referenced signature is invalid, the whole transaction fails. A business program does not receive that result directly; it inspects the other instructions in the same transaction (through the Instructions sysvar) and relies on the validation having passed. When a user tops up a Rain-powered card (as with Avici), the balance is held in a per-user collateral account managed by the shared Rain program, and that account's token authority is derived from the program, so the account's admin can move the assets in that account through the program's transfer flow.

Changing a collateral account's admin requires two signatures. One must come from the protocol-designated admin; the other signer has no special identity requirement. This design relies on off-chain authorization: once the protocol admin has signed the admin-change message, the program treats it as approved.

An Ed25519 verification instruction begins with a one-byte count of the signatures to check and a padding byte, followed by one Ed25519SignatureOffsets structure per signature. Each structure specifies not only the byte offsets of the signature, public key, and message, but also the instruction index where each of those should be read [10]. These indexes are an intentional feature: they let one verification read its inputs from the data of any indexed instruction in the transaction, for instance to verify a signature over another instruction's data without copying it. The native verifier simply reads whatever the offsets and instruction indexes point to.

Vulnerability Analysis

The flaw is in the collateral program (3zVB...yBzDuc): it trusts a public key without confirming that key is the one the verifier actually checked. To record the admin-approving signer, it reads a public key from a fixed position inside the Ed25519 verify instruction it inspects, then takes the mere presence of a passed verification as proof that the holder of this key signed the admin-change message.

What it never confirms is where that verification actually looked. Because those instruction-index fields are caller-controlled and the runtime hands every instruction's data to the native verifier, the verify instruction can hold one public key in its own body while its instruction indexes steer the verifier onto a different instruction, to verify a signature under a different public key over a separately sourced message.

So two reads of "the admin key" exist with nothing tying them together: the program trusts the key sitting in the instruction it inspects, while the verifier only ever checked whatever the indexes pointed to. The admin's key can be present in the transaction as data even though no signature valid under that key was ever verified. That decoupling is the vulnerability, and the missing check is the binding that would force the public key and message the verifier actually verified to be the same key and message the program trusts.

Attack Analysis

The following analysis is based on the transaction ZmpBgn...mqWL.

  • Step 1: The attacker built a transaction with two Ed25519 verification instructions followed by SubmitSignatures. Instruction 0 carried the attacker's own public key (cafa…53db) and a real signature over the admin-change message, giving the transaction one genuinely valid Ed25519 verification.
  • Step 2: Instruction 1 placed the protocol admin's public key (a2fc…959a) in the public-key slot but filled the signature slot with fake 0x09 bytes, so this instruction would fail if its own data were actually verified.

  • Step 3: The attacker set instruction 1's Ed25519 header to 01003000000010000000700020000000. Only the three instruction-index fields do the redirection, and decoded they are all zero, so the signature, public key, and message are all read from instruction 0 (the byte offsets still point within that instruction's data):

    num_signatures      = 1,    padding = 0
    signature_offset    = 48,   signature_instruction_index   = 0
    public_key_offset   = 16,   public_key_instruction_index  = 0
    message_data_offset = 112,  message_data_size = 32,  message_instruction_index = 0

    The second verification therefore re-read instruction 0 and re-verified the attacker's own valid signature instead of the invalid 0x09 payload in instruction 1.

  • Step 4: The attacker called SubmitSignatures. The program saw two successful Ed25519 verifications, but when recording the second signer it read the protocol admin public key embedded in instruction 1, so it accepted that admin key as an approved second signer.

  • Step 5: With that false approval recorded, the attacker used the admin-change flow to set the victim collateral account's admin to the attacker, converting the verification bypass into direct control of that account.

  • Step 6: Having validated the caller as the collateral admin, the program moved assets out of the user's card-collateral token account through its transfer flow, invoking the transfer with its own program-derived address (PDA) as that token account's authority.

Conclusion

The root cause of the Rain Card Contract Exploit Series was that the collateral program confirmed a native Ed25519 verification had run but never confirmed that the public key and message it trusted were the ones the verifier had actually checked. A program that leans on native signature verification must bind the authorization data it consumes to that verification: either require a self-contained Ed25519 layout that reads its inputs from the current instruction, or resolve every referenced instruction and compare the verified public key and message, byte for byte, against the data it acts on. That the same flaw ran unpatched across many Rain-powered card programs is what turned one bug into a multi-program event.

Tectonic

On 2026/08/30, Tectonic, a Compound-style lending protocol on Cronos, was exploited because its low-liquidity governance token TONIC was allowed as collateral with a 20% collateral factor. The attacker inflated TONIC-denominated collateral on two surfaces at once: a direct-transfer manipulation of the tTONIC exchange rate, paired with DEX buying that pushed the oracle price up. Against the doubly inflated collateral, the attacker borrowed across multiple lending markets. About $6.29M (2,592 ETH) was bridged to Ethereum and stands as the realized loss; the bulk of the drain stayed on Cronos and was erased when validators rolled the chain back to a pre-exploit state. Neither Tectonic nor Cronos has confirmed a final loss figure [11].

Vulnerability Analysis

The root cause was listing TONIC, a low-liquidity governance token, as collateral with a 20% collateral factor. Despite its governance role, TONIC had extremely thin on-chain liquidity, so its valuation could be moved sharply with limited capital. As in the Moonwell exploit, two surfaces were manipulable at once: the TONIC/USD oracle price, and the tTONIC receipt-token exchange rate, which a plain transfer of TONIC into the market lifts without canceling the matching debt [11]. Any non-zero collateral factor then converted that inflated valuation into borrowing power against far more liquid assets.

Attack Analysis

The reconstruction of the attack below is based on on-chain intelligence and a detailed archive-node reconstruction [11][12].

  • Step 1: The attacker supplied about 5M USDC to Tectonic as collateral. While the TONIC/USD oracle still valued the token near $1.06e-8, one controlled account borrowed roughly 376.54T TONIC and moved it to a second account.

  • Step 2: The second account supplied about 41.87T TONIC normally, receiving tTONIC (the market's receipt token) in return.

  • Step 3: The attacker transferred most of the remaining borrowed TONIC directly into the tTONIC market contract without repaying the original TONIC debt, raising the tTONIC exchange rate and lifting the collateral value of the tTONIC held by the second account.

  • Step 4: Using the inflated tTONIC collateral, the attacker borrowed 200,000 USDC and about 6.96M CRO, then bought roughly 16.23T more TONIC across the TONIC/USDC, TONIC/WCRO, and TONIC/VVS pools and routed it back into the market. This lifted the exchange rate further and drove the DEX spot price up; Tectonic's offchain TONIC/USD feed then accepted a series of rapidly rising quotes (from about $1.06e-8 at 12:19 UTC to about $2.08e-6 by 12:49 UTC). This is the external surface.

  • Step 5: Further borrows of about 3.31M USDC and 21.21M CRO bought another 7.68T TONIC, again routed into the market, reinforcing both the inflated exchange rate and the manipulated price immediately before extraction.

  • Step 6: In the final transaction, the attacker drew against the doubly inflated collateral across multiple lending markets, extracting roughly 55.24M USDC, 45.65M USDT, 98 WBTC, 1,895 WETH, 16.75M CRO, and other assets. About $6.29M was bridged to Ethereum (~2,592 ETH) before validators halted the chain. Block production resumed at 2026/08/30 23:49 UTC (announced 08/31) after validators rolled the state back to a pre-exploit block, erasing the on-Cronos balance; only the bridged ~$6.29M stands as the realized loss.

Conclusion

Like the Moonwell exploit earlier this week, this was a price-manipulation attack on a Compound-style market that accepted a low-liquidity token as collateral, inflating the collateral's value on two surfaces at once: the oracle price and the receipt-token exchange rate. Lending protocols should avoid listing low-liquidity assets as collateral, apply strict supply and borrow caps where they must be supported, and use time-weighted or liquidity-aware pricing so a thin spot market cannot move the oracle. The exchange-rate surface needs its own guard: a market's collateral accounting should exclude unsolicited transfers of the underlying, so a direct transfer cannot inflate the receipt-token exchange rate while the matching debt stays outstanding. On-chain monitoring of abnormal price and borrowing activity can further shorten the response window before value is bridged out.

Get Started with Phalcon Security

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

Try now for free

References

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
Web3 Attack Surfaces: A Penetration Testing Overview
Security Services

Web3 Attack Surfaces: A Penetration Testing Overview

Crypto institutions keep every traditional attack surface and add the money-handling chain on top of it. This article gives testers a practical abstraction of the running system: a four-component model—Application, Authorization and Signing, Blockchain Interaction, and Infrastructure—with each component's responsibility, representative implementations, and inherited attack surfaces. It then structures web3-specific coverage into five attack-surface areas, from production and automation operations through signing intent, approval and withdrawal chains, and fund logic to on-chain transactions and deployed contracts.

Rules of Engagement and Production Safety for Institutional Blockchain Penetration Testing
Security Services

Rules of Engagement and Production Safety for Institutional Blockchain Penetration Testing

A penetration test that touches signing, withdrawal, and ledger systems is prepared before it runs. This article follows the engagement lifecycle: turning a business decision into objective, scope, named owners, and authorized access; recording authority, permitted techniques, operating limits, prohibited activity, communications, and evidence handling in a Rules of Engagement document; and protecting live service with measurable stop criteria, monitoring, change coordination, and named pause authority. It closes with the remediation and retest that turn findings into validated controls.

What Is Blockchain Penetration Testing? Definitions and Boundaries
Security Services

What Is Blockchain Penetration Testing? Definitions and Boundaries

No widely accepted definition of blockchain penetration testing exists, and many proposed ones tangle it with audit, scanning, and bug bounty. This article sets out a working definition—an adversarial, hands-on assessment of a running system, under agreed scope and rules of engagement, that validates exploitable paths and control chains—and what web3 adds: a money-handling threat model whose defining composition gap is the off-chain-to-on-chain handoff. It then maps the five testable capabilities of that chain and routes nearby objectives to code audit, wallet security audit, web3 security testing, scanning, and bug bounty.

Best Security Auditor for Web3

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

BlockSec Audit