During the past week (2026/08/17 - 2026/08/23), the following 2 notable security incidents are featured, involving approximately $10.26M in total losses.
| Date | Incident | Type | Estimated Loss |
|---|---|---|---|
| 2026/08/18 | MAYAChain | Business Logic Flaw | ~$1.76M |
| 2026/08/23 | Term Finance | Flawed Governance Design | ~$8.5M |
Reasons for selection
- MAYAChain: Selected because a chained accounting and state-validation failure let a single crafted deposit corrupt outbound reconciliation and inflate a low-liquidity pool's recorded balance with no real backing, showing how several low-level defects, each limited on its own, can compose into a drain on a cross-chain liquidity network.
- Term Finance: Selected because near-zero governance participation left no electorate to vote down a malicious proposal, and no guardian or cancellation path backed the execution delay, letting an attacker with minimal capital dominate the votes, pass a proposal, and drain approximately $8.5M from six protocol vaults.
Best Security Auditor for Web3
Validate design, code, and business logic before launch
Weekly Highlight: Term Finance
Term Finance is featured this week because the failure was not a coding bug but a governance-design flaw, a class of risk that grows as protocols ship a separate on-chain DAO with each vault: when almost no one participates, control of the vote can be bought cheaply, and the vault's own governance becomes the attack surface.
On August 23, 2026, Term Finance, a fixed-rate lending protocol on Ethereum, lost approximately $8.5M when an attacker seized control of its vaults' on-chain governance. Because virtually no depositors had ever minted a vault's governance token, the attacker could acquire a supermajority of its voting power for roughly 0.5 ETH, clear the protocol's support-threshold and minimum-participation checks, and execute a malicious proposal that revoked the vault's strategies and transferred its assets; six of Term's vaults were drained this way [1].
Background
Term Finance is a fixed-rate lending protocol on Ethereum. Term Vaults are a separate product layered on top of it: each vault is an ERC-4626 vault built on Yearn V3 code, where a meta vault accepts a single asset and allocates it across a set of strategy vaults. Term's framework deploys an Aragon OSx DAO and a TokenVoting contract alongside every vault, and that DAO holds upgrade and role authority over the vault it ships with. A vault therefore carries its own governance surface from birth, independent of who curates it or whether any capital is ever allocated into it.
Governance decisions are taken by TokenVoting. Each vault has its own share token and a matching Aragon GovernanceWrappedERC20 governance wrapper; in the analyzed ETH Meta Vault these are tmvETH and gtmvETH. Any share-token holder may call depositFor() to receive the governance token one-to-one, and any governance-token holder may delegate() the resulting votes. When a proposal is created, TokenVoting.createProposal() records a snapshotBlock, a supportThreshold, and a minVotingPower derived from the token supply at that snapshot; voting power is then read from the wrapper at that block.
Vulnerability Analysis
The governance contract at the center of this incident is TokenVoting. Proposal execution is gated only by _canExecute(), which, for a normal (non-early) proposal, requires that the vote is not already executed, that the proposal is closed, and that two checks pass: isSupportThresholdReached() and isMinParticipationReached().

Both checks are correct implementations of majority voting, but each measures a relative proportion rather than an absolute amount. isSupportThresholdReached() requires only that yes-votes outweigh no-votes past the configured ratio:

isMinParticipationReached() requires that the votes cast reach minVotingPower, which is itself derived as minParticipation * totalSupply at the snapshot:

The root cause is that this governance design set no absolute floor on the capital or breadth needed to move a proposal. Both gates are purely relative to the votes cast and the token supply, and because almost no tmvETH holder had ever taken part in governance by wrapping into gtmvETH, that supply, and with it the minimum-participation floor, sat near zero. A relative majority of a near-empty electorate was therefore all it took to clear both checks, and with so few voters there was no one to cast the opposing votes that could have defeated the proposal. A minimum voting duration delayed execution, but with no guardian or cancellation path, the delay only postponed the passing proposal rather than preventing it.
Attack Analysis
The attacker acquired a controlling share of a vault's governance token, then used it to pass and execute a proposal that emptied that vault; the same technique was applied across six of Term's vaults. The following analysis traces one vault, based on the transactions 0xd354a1...d3014129 and 0x9f273f...44c2e8a0.
-
Step 1: The attacker swapped
0.5 ETHfor0.485 tmvETHthrough a Mayan Finance forwarder, which routed the swap and delivered the vault shares to the attacker. -
Step 2: The attacker wrapped the
0.485 tmvETHinto0.485 gtmvETHone-to-one, obtaining the voting power used in the following steps. -
Step 3: The attacker called
propose(), which read its own voting power and invokedTokenVoting.createProposal(). The contract recorded the snapshot, reading the total governance supply at that block as only0.535 gtmvETH, so the attacker's stake was about 90.66% of the entire electorate. -
Step 4: The attacker called
vote()to cast its entire voting power in favor of the proposal, becoming the only participant. -
Step 5: After the voting period, the attacker called
executeProposal(). The gatecanExecute()checkedisSupportThresholdReached(): with the attacker the sole yes-voter, the support ratio far exceeded the 50% threshold. It then checkedisMinParticipationReached(): the attacker's own voting power alone exceededminVotingPower. Both passed. -
Step 6: The malicious proposal then executed, withdrawing the funds the
tmvETHvault had allocated to its strategy, converting them back into liquidWETHheld by the vault, and letting the attacker withdraw the assets. Applied across six of Term's vaults, the attack caused approximately $8.5M in total losses.
Conclusion
This incident was caused by a flawed governance design rather than a coding error: the support and participation checks were purely relative, so with a near-empty electorate a cheaply acquired majority faced no opposing vote, and the execution delay had no guardian or cancellation path behind it. Governance that controls custody of assets should enforce an absolute quorum or participation floor and back any execution delay with a guardian or cancellation path and a monitored proposal feed, since a timelock only postpones a passing proposal unless someone can act during it.
More Incidents This Week
MAYAChain
On August 18, 2026, MAYAChain, a Cosmos-SDK cross-chain liquidity network, lost approximately $1.76M when a single crafted deposit corrupted the chain's internal accounting. By making valid withdrawals appear to have failed, the deposit triggered a recovery path that inflated a low-liquidity pool's recorded native-token balance with no real backing; the attacker then drained that inflated value by adding and withdrawing liquidity [2]. Confirmed on-chain outflows were about $1.36M, mainly 20.83 BTC, with residual native-token holdings bringing the official estimate to approximately $1.76M.
Background
MAYAChain is a Cosmos-SDK cross-chain liquidity network whose native asset is CACAO. Events on external chains are observed by validators and replayed into MAYAChain as observed transactions. When an inbound action needs to send assets out, the node schedules one or more TxOutItems and later reconciles the observed outbound transactions against those scheduled records. All pool assets are custodied together in shared Asgard vaults; each liquidity pool is an accounting position over that shared custody rather than a segregated balance, and withdrawals are paid from Asgard against a pool's recorded balances.
Trade accounts are native MAYAChain accounting positions for trade assets such as ARB~ETH and ARB~LINK. A trade-account withdrawal is initiated through a native MsgDeposit with a memo such as trade-:ARB~LINK. Although the user signs a single native transaction, the deposit handler reconstructs an internal observed transaction and stores an ObservedTxVoter keyed by the native transaction hash.
Vulnerability Analysis
The root cause was not one isolated bug; it was a chain of accounting and state-validation defects that composed. The buggy logic lives in MAYANode's deposit and outbound handlers.
First, in handler_deposit.go, each message in a native transaction builds a fresh ObservedTxVoter keyed by the transaction hash and saves it with SetObservedTxInVoter():
txIn := ObservedTx{Tx: tx}
txInVoter := NewObservedTxVoter(txIn.Tx.ID, []ObservedTx{txIn})
txInVoter.Height = ctx.BlockHeight()
txInVoter.FinalisedHeight = ctx.BlockHeight()
txInVoter.Tx = txIn
h.mgr.Keeper().SetObservedTxInVoter(ctx, txInVoter)
Because every message in a batch shares the same tx.ID, a later message overwrites the voter state written by earlier ones, discarding their outbound scheduling metadata.
Second, in handler_common_outbound.go, outbound reconciliation starts from voter.OutboundHeight (or voter.FinalisedHeight when the former is zero) and scans forward by the signing period:
outHeight := voter.OutboundHeight
if outHeight == 0 {
outHeight = voter.FinalisedHeight
}
for height := outHeight; height <= ctx.BlockHeight(); height += signingTransPeriod {
txOut, err = h.mgr.Keeper().GetTxOut(ctx, height)
...
}
When the voter has been overwritten this way, the scan starts at the finalised deposit height and never inspects the block that holds the scheduled outbounds, so the handler treats them as missing and invokes the slash-recovery path.
Third, in helpers.go, the recovery path values the "missing" asset as a CACAO subsidy using the raw observed amount, with no cap against the pool's actual asset depth:
f.stolenAsset = f.stolenAsset.Add(coin.Amount)
runeValue := pool.AssetValueInRune(coin.Amount)
f.subsidiseRune = f.subsidiseRune.Add(runeValue)
In a pool holding only about 0.11 LINK of asset-side depth, this uncapped conversion could map an observed LINK amount to an enormous CACAO value. The same routine then commits the inflated pool balance to state before attempting the funding transfer:
pool.BalanceCacao = pool.BalanceCacao.Add(f.subsidiseRune)
pool.BalanceAsset = common.SafeSub(pool.BalanceAsset, f.stolenAsset)
if err = mgr.Keeper().SetPool(ctx, pool); err != nil {
...
}
runeToAsgard := common.NewCoin(common.BaseAsset(), f.subsidiseRune)
if err = mgr.Keeper().SendFromModuleToModule(ctx, ReserveName, AsgardName, common.NewCoins(runeToAsgard)); err != nil {
ctx.Logger().Error("fail to send subsidy from bond to asgard", "error", err)
return err
}
Because the pool write is committed before the transfer, if the transfer cannot be funded the pool's BalanceCacao is never reverted. Finally, in handler_observed_txout.go, the caller swallows the returned error, marks the voter done, and continues, so an inconsistent pool state can persist:
_, err = handler(ctx, m)
if err != nil {
ctx.Logger().Error("handler failed:", "error", err)
slashObservedOutbound("failed_outbound")
voter.SetDone()
h.mgr.Keeper().SetObservedTxOutVoter(ctx, voter)
continue
}
Attack Analysis
The attacker composed these defects in a single native transaction, then extracted value from the inflated pool. The following analysis is based on the MAYAChain transaction 516BA14D...E9B7.
-
Step 1: The attacker submitted native transaction
516BA14D...E9B7with 23 messages: 20trade-:ARB~ETHwithdrawals, 2trade-:ARB~LINKwithdrawals, and a finalDONATE:ARB.LINKof one base unit. -
Step 2: The trade withdrawals created valid scheduled outbounds, but the final
DONATEmessage overwrote the shared inbound voter for the same native transaction hash, erasing the outbound scheduling of the earlier withdrawals. -
Step 3: When the
ARB.LINKoutbounds were later observed, the reconciliation scan used the clobbered voter's height fields and never reached the block that held the scheduled outbounds, so it treated them as missing. -
Step 4: The slash-recovery path valued the missing LINK against the thin
ARB.LINKpool and inflated the pool'sCACAOside by about49.45M CACAO. The Reserve-to-Asgard transfer that should have funded the subsidy failed, since the Reserve held only about168K CACAO, far short of the inflated subsidy; but the mutated pool balance persisted and the failed handler was marked done. -
Step 5: At a later height, the attacker added liquidity to the distorted pool with
100 CACAOand a small amount of LINK. Because one side of the pool had been driven far out of proportion, the add-liquidity formula granted the attacker about1TLP units against roughly731Mexisting units. -
Step 6: The attacker immediately withdrew almost the entire position, receiving approximately
48.87M CACAOand98.82 LINKpaid out from the shared Asgard vaults against the inflated pool balance, and swapped the extractedCACAOthrough MAYAChain pools into BTC and other assets. TheCACAOprice fell from about$0.115to a low near$0.013during the sell-off.
The 48.87M CACAO the attacker withdrew was notionally worth more than $5M at the pre-exploit price, but it was not all realized as external proceeds: much of it was swapped back into pools, crashing the CACAO price, and opportunistic arbitrageurs unrelated to the attacker also removed value during the crash. The directly confirmed on-chain extraction was about $1.36M, mainly 20.83 BTC, and counting the attacker's residual CACAO and trade-account balances brings the figure to the official estimate of approximately $1.76M.
Conclusion
This incident was caused by a chain of accounting and state-validation defects rather than a single bug: no step was catastrophic alone, but together they let a crafted deposit turn a thin pool's recorded balance into unbacked value. The core requirement is atomicity and bounded trust: an accounting mutation and the transfer meant to fund it must succeed or fail together, a failed recovery path must never be marked done, and valuations against low-liquidity pools must be capped. Shared inbound state should also never be silently overwritten by a later message in the same transaction.
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.
-
Official website: https://blocksec.com/
-
Official Twitter account: https://twitter.com/BlockSecTeam
-
π Phalcon Compliance



