Major Web3 exploits often begin with a small mismatch between intended behavior and what the code actually permits. These five published findings show how cross-asset accounting, replay protection, reentrancy, share conversion, and state recording can become direct security boundaries.
Small Logic Mistakes Can Create Major Exploit Paths
Many serious Web3 security failures are not caused by broken cryptography. They begin with a missing authorization modifier, a share formula that behaves differently at zero supply, an external call made before state is finalized, or a signature that is valid in more than one execution domain.
A smart contract security audit tests those assumptions under adversarial conditions. Researchers ask whether a caller can reach privileged behavior, whether accounting remains conserved at boundary values, whether a state transition can be repeated, and whether an integration returns exactly what the protocol expects.
The five examples below are educational rewrites of findings documented in published Kann Audits reports. The report cards preserve the protocol, finding title, severity, date, and remediation status stated in the source. Simplified code illustrates the vulnerability class without reproducing a client codebase.
#1 — Cross-Asset Collateral Accounting Corruption
Fluton's confidential lending adapter recalculated borrowing limits for every supported asset using the encrypted scaled balance of only the asset most recently finalized. The result depended on transaction order rather than the user's complete collateral portfolio. This was a Critical finding and the published status is Fixed.
What Was the Bug?
The adapter stored a separate balance and borrowing limit for each user and asset. After supply, borrow, withdrawal, or repayment finalization, a shared helper received one currentBalance value. It then looped over the entire asset list and used that single balance to overwrite every per-asset borrowing limit.
The intended model was cross-collateralized: each asset should contribute according to its own balance, value, decimals, and loan-to-value parameter. The implemented model silently replaced that portfolio with whichever asset happened to finalize last.
Vulnerable Logic
This simplified example preserves the core accounting error. The loop variable changes the destination asset, but the balance input never changes with it.
function updateBorrowLimits(
address user,
uint256 latestAssetBalance
) internal {
for (uint256 i; i < supportedAssets.length; ++i) {
address asset = supportedAssets[i];
uint256 ltv = riskConfig[asset].ltv;
// latestAssetBalance belongs to only one asset
maxBorrowable[user][asset] = latestAssetBalance * ltv / 10_000;
}
}
Exploit Scenario
A user could supply a small position in one asset and then finalize a much larger balance in another. The larger balance would be reused when calculating limits for both assets. Reversing the order could instead erase legitimate borrowing capacity. Because the adapter represented users through one aggregate Aave position, incorrect user-level capacity could propagate into pooled solvency risk.
Potential Impact
The report documents two opposite failure modes. Users could see borrowing capacity collapse toward zero, creating denial of service, or receive limits far above their actual collateral value, enabling under-collateralized borrowing. Since the Diamond held a pooled Aave position, the latter state could contribute to liquidation of collateral backing multiple users.
How Kann Audits Found It
Researchers traced the meaning of currentBalance across four duplicated helper implementations and compared the source asset with every destination mapping entry. They then modeled multi-asset finalization in different orders. The bug was not visible from one transaction alone; it emerged from following portfolio state across assets and operations.
How Developers Can Prevent It
Compute each asset's contribution from that asset's own normalized balance and risk configuration, then aggregate the portfolio once. Make asset and unit context explicit in helper parameters, remove duplicated accounting logic, subtract existing debt when calculating remaining capacity, and test every permutation of multi-asset operation order.
Before deploying similar logic, verify
- Is every balance paired with its exact asset, decimals, price, and risk parameters?
- Can finalizing Asset B overwrite limits derived from Asset A?
- Does the same portfolio produce identical capacity regardless of operation order?
- Are existing debt and aggregate protocol liabilities included in the invariant?
- Do multi-asset fuzz tests cover large value and decimal differences?
#2 — Reentrancy Before Claim State Is Finalized
The castr.fun report documented a Critical double-claim path in LockManager.claimRewards. An ETH transfer occurred before the position NFT was burned and before claim accounting was cleared, allowing a malicious recipient to call back into the function while the original claim remained valid.
What Was the Bug?
The function treated an external value transfer as if execution would resume only after the recipient returned. A contract recipient controls that callback. During reentry, ownerOf(tokenId) still identified the attacker as the owner and the reward record still held the original amount.
Because all lockers' assets were held in one shared contract, a repeated claim was not limited to the attacker's contribution. It could draw from balances backing other positions.
Vulnerable Logic
The educational example shows why ordering matters even when ownership is checked at the beginning.
function claim(uint256 positionId) external {
require(ownerOf(positionId) == msg.sender, "not owner");
uint256 amount = claimable[positionId];
payable(msg.sender).call{value: amount}(""); // reentry point
delete claimable[positionId];
_burn(positionId);
}
Exploit Scenario
The second call observes the same pre-claim state as the first. Repeating that cycle turns one legitimate position into multiple withdrawals until balances or gas stop execution.
Potential Impact
The published impact states that all tokens and ETH locked in the shared LockManager could be drained. The report does not state a finding-level remediation status, so this article does not infer one.
How Kann Audits Found It
Researchers traced the complete claim state transition, marked every external interaction, and asked what storage an adversarial recipient could still observe during the callback. The critical fact was not simply that ETH moved; it was that ownership and collected-reward state remained reusable at that exact point.
How Developers Can Prevent It
Apply checks-effects-interactions: invalidate the claim, update aggregate accounting, and burn or lock the position before transferring assets. Add a reentrancy guard as defense in depth, but do not use the guard as a substitute for correct state ordering.
Before deploying similar logic, verify
- Does any token, ETH, hook, or callback transfer occur before claim state is consumed?
- Can the recipient reenter through a different public function?
- Are shared pool balances exposed when one user's accounting is repeated?
- Do tests use malicious ERC777-style hooks and contract fallbacks?
#4 — Cross-Chain Signature Replay
The RWA borrowing and lending review found that a borrow authorization signed for one chain could be reused on another chain when both deployments accepted the same payload structure. The finding was rated High. Its remediation status is not stated in the published report.
What Was the Bug?
The signature committed to collateral, borrow assets, amounts, caller, and nonce, but not to the active chain. A nonce stored on Chain A does not consume the corresponding nonce on Chain B. Both contracts could therefore consider the same signed authorization fresh.
Vulnerable Logic
bytes32 digest = keccak256(abi.encode(
collateral,
borrowAsset,
collateralAmount,
borrowAmount,
msg.sender,
nonces[msg.sender]
// block.chainid and address(this) are missing
));
Exploit Scenario
Replay resistance has to exist across every domain where a signature might be accepted. A local nonce prevents repetition only within the state machine that stores it.
Potential Impact
A single authorization could enable more borrowing than the signer intended by being exercised on multiple deployments. The exact financial effect depends on liquidity and collateral behavior on each chain, so this article does not assign an unsupported loss amount.
How Kann Audits Found It
Researchers reconstructed the signed message field by field and compared it with the domains in which the verifier could be deployed. The missing chain identifier became visible only when the threat model included more than one deployment.
How Developers Can Prevent It
Use explicit domain separation such as EIP-712, bind authorizations to block.chainid and the verifying contract, and include an expiry plus purpose-specific operation type. Test the same signature against cloned and cross-chain deployments.
Before deploying similar logic, verify
- Is the chain identifier included in the signed domain?
- Is the verifying contract address bound to the authorization?
- Are nonce, expiry, signer, caller, and operation type included?
- Can the same signature be valid in a bridge, fork, testnet, or clone?
#5 — Burned Position Without a Recorded Withdrawal Claim
A Mystic Finance unstaking path burned the user's frxETH but did not save withdrawalRequests[msg.sender] after requesting validator unstaking. The later withdraw function depended on that mapping. The report rated the issue High and marked it Fixed.
What Was the Bug?
The protocol implemented unstaking as a multi-step process. When immediately available ETH was insufficient, it initiated a validator withdrawal and returned the amount unstaked, but omitted the state assignment that connected the user to the later claim.
The token burn represented an irreversible effect. Without a matching claim record, the protocol had consumed the user's position without preserving the right needed to receive ETH.
Vulnerable Logic
function unstake(uint256 amount) external {
_burn(msg.sender, amount);
uint256 amountUnstaked = validator.requestWithdraw(amount);
// Missing:
// withdrawalRequests[msg.sender] += amountUnstaked;
}
Exploit or Failure Scenario
This finding did not require an attacker to steal funds. A normal user following the intended flow could reach a state where assets were burned but the application could not complete payment through its documented path.
Potential Impact
Users on the affected fallback path could receive no ETH after their tokenized position was burned. The report describes loss of user funds with no recovery through the intended withdrawal flow.
How Kann Audits Found It
Researchers traced the user's entitlement across both functions rather than reviewing unstake and withdraw in isolation. The missing write was apparent when every irreversible state change was paired with the durable claim expected by the next stage.
How Developers Can Prevent It
Model asynchronous withdrawals as an explicit state machine. Record the claim before or atomically with any burn, assign a unique request identifier, and test every liquidity branch through final settlement. A transition should never destroy the user's current entitlement before the next entitlement exists.
Before deploying similar logic, verify
- Does every burn create or settle an equal user entitlement?
- Can a low-liquidity branch skip a required storage write?
- Is the later claim keyed to the correct user and request?
- Can failed external requests be retried, cancelled, or refunded?
Security Audits Challenge the Assumptions Code Does Not Enforce
These findings span different protocols and vulnerability classes, but they share one property: the implementation permitted a state the design did not intend. One asset's balance replaced an entire collateral portfolio. A claim stayed live during an external callback. A vault trusted assets and shares to move together. A signature trusted one deployment domain. A burn trusted a later claim record to exist.
Professional smart contract security review looks for those gaps before they become incidents. That means combining line-by-line code review with state-transition tracing, accounting invariants, privilege analysis, integration modeling, and adversarial tests at boundary conditions.
A security audit cannot guarantee that a Web3 protocol is vulnerability-free. It can produce concrete evidence about a defined scope, identify realistic exploit paths, and verify whether submitted fixes address the original issue without creating a new one.
Primary references