3 Critical Smart Contract Vulnerabilities Explained
Quick answer. Three critical smart contract vulnerabilities from Guardian reviews keep recurring. They are zero-amount withdrawals that reset another user's cooldown, parallel withdrawal state that traps funds between contracts, and partial liquidations that leave a position less healthy.
These three critical smart contract vulnerabilities come from Guardian security reviews, and none of them is specific to one protocol.
We see them come up again and again, in different forms, across many kinds of systems.
For each one you will learn what the bug is, what makes it severe, and how to find and prevent it.
If you write DeFi contracts, avoid all three. If you review them, do not let them slip past you again.
Every finding below is from a public Guardian report. The full set is in the Guardian audits portfolio on GitHub.
1. Zero-Amount Withdrawals
The first finding is titled "Withdrawals can be permanently blocked." It sits in an ERC-4626 vault whose redeem function does more than vanilla ERC-4626 logic.
How the cooldown works
Liquidity providers must call a cooldown function before they can withdraw. It records a timestamp for msg.sender in a mapping of LP cooldowns.
Until the cooldown period ends, a validation function caps what the user can take out at zero. Both maxWithdraw and maxRedeem use it, so nothing comes out until enough time passes.
When a user redeems, the vault deletes their cooldown entry, since the redemption is done.
It then calculates a fee and sends that portion of the assets to a fee manager instead of the receiver.
The missing check
Nothing stops a caller from redeeming zero shares on behalf of another owner.
Redeeming for someone else requires an allowance, but OpenZeppelin's _spendAllowance only reverts when the allowance is less than the amount.
With an amount of zero, the check passes with no approval at all. You may have seen the same effect on portfolio trackers, where scam transactions show zero transfers "from" your wallet.
So an attacker calls redeem or withdraw with zero for any owner. Execution succeeds and the owner's cooldown entry is deleted.
function redeem(uint256 shares, address receiver, address owner)
public override returns (uint256 assets)
{
require(shares != 0, "zero shares"); // the missing check
require(shares <= maxRedeem(owner), "exceeds max redeem");
delete lpCooldowns[owner]; // resets the owner's cooldown
assets = previewRedeem(shares);
// spends allowance, which passes for a zero amount
_withdraw(msg.sender, receiver, owner, assets, shares);
}
Why it is critical
The attacker can repeat the call for every account, forever. No one ever reaches the end of a cooldown, so no one can withdraw or redeem.
That is a denial of service that locks every user's funds in the vault.
The fix
Revert when shares is zero in redeem, and do the same in withdraw. That single validation is the full remediation.
2. Parallel Withdrawal State
The second finding, GLOBAL-1, is titled "Issuance withdrawals can be trapped." It comes from Guardian's review of Rest Finance, and the pattern shows up in many protocols in other forms.
The system
Rest Finance deposits users' funds into EigenLayer through a vault. A user deposits a liquid staking token, such as Lido staked ETH, and the vault routes it into EigenLayer's strategy manager.
The user receives a receipt token for the deposit. Later they return it to the vault, which burns it and gives back the staked ETH plus any rewards.
Withdrawing from an EigenLayer strategy has a delay of about seven days between queuing a withdrawal and completing it.
On top of this, Rest Finance built an issuance contract that lets users sell a pending withdrawal. It works in three steps.
- Queue. A user calls
queueWithdrawalon the issuance contract with an amount of receipt tokens. It takes the tokens, queues a withdrawal through the vault and records the user as the owner. - Complete early. Partway through the wait, anyone can call
completeWithdrawEarly. They become the new owner and pay the original owner the underlying tokens immediately. - Complete. If no one buys the withdrawal, the owner calls
completeWithdrawalonce the seven days pass, and it completes through EigenLayer.
One smaller issue first: that last function needs access control, or at least must send funds to the withdrawal's owner rather than msg.sender. Always check who receives the funds.
The red flag: parallel state
Two contracts track the same withdrawal. The issuance contract records the user as its owner. The vault records the issuance contract as the owner, because the issuance contract queued it.
The vault's completeWithdraw can be called by anyone. On its own that is safe, because it pays whoever owns the withdrawal.
How funds get trapped
Now look at both contracts as one system. A third party calls completeWithdraw on the vault directly for the user's withdrawal.
EigenLayer confirms the seven days have passed, deletes the pending withdrawal and releases the funds. They go to the recorded owner, which is the issuance contract.
When the user later calls completeWithdrawal on the issuance contract, it tries to complete the withdrawal through the vault again. That withdrawal no longer exists, so the call reverts.
The funds sit in the issuance contract with no way out, and the true owner's pending withdrawals in the issuance contract are never updated.
Anyone can do this to any withdrawal queued through issuance.
The lesson and the fix
When a system has several contracts with external functions anyone can call, ask how one contract's reliance on another can be abused.
A malicious user, or even a normal one, may cause catastrophic outcomes.
The remediation is to remove the parallel tracking of state and refactor the logic, so calling the vault directly cannot confuse the system.
3. Worse Health After Liquidation
The third finding, ML-1, is titled "Health score decreases after liquidation." Its title doubles as a great invariant: a position's health should get better after a liquidation.
An account is liquidated because it is too unhealthy. If it ends up less healthy, the protocol's risk of bad debt only grows.
This applies to lending and borrowing, and also to perpetuals and options. Those usually close the whole position, but after any partial liquidation, health should improve or at least stay the same.
A basket of collateral
In this protocol, collateral is a basket of tokens rather than one asset. Each token has its own loan-to-value (LTV) ratio, set by its volatility.
LTV is the share of a token's value you can borrow against. Take a basket worth $50,000 in each of four tokens:
| Collateral | Value | LTV |
|---|---|---|
| USDT | $50,000 | 0.80 |
| Wrapped BTC | $50,000 | 0.70 |
| Wrapped ETH | $50,000 | 0.70 |
| SOL | $50,000 | 0.50 |
The basket is worth $200,000 and the average LTV is 0.675. The most you can borrow is $200,000 × 0.675, or $135,000.
The health score is the LTV-weighted collateral divided by the liabilities, and the account is liquidatable below 1. With $125,000 borrowed, the score is a little above 1.
With $145,000 borrowed, it is $135,000 / $145,000, or about 0.931. The position should be liquidated.
The liquidation
The protocol performs a partial liquidation and always picks the largest allocation in the basket. Say USDT is slightly the largest, so it is removed: 25% of the collateral value.
The protocol then repays 25% of the debt. That leaves $108,750 borrowed against $150,000 of collateral.
If the average LTV stayed at 0.675, health would be $101,250 / $108,750, still 0.931. That would be a fine liquidation, since the position shrank and its bad debt risk fell.
But the average LTV changes. Without USDT it is (0.70 + 0.70 + 0.50) / 3, about 0.633.
The effective collateral is now about $95,000, and health is $95,000 / $108,750, or 0.873. The liquidation made the position less healthy.
It is also more exposed, because the assets left are the more volatile ones. The liquidation leaves the position more prone to insolvency.
The fix and what to look for
Partial liquidations should start with the lower-LTV, riskier assets. Take the long-tail tokens out of the basket first, since they are not the best collateral.
Removing all the SOL and a little wrapped ETH could leave the position healthy again, because it raises the basket's average LTV.
When you review a protocol with basket collateral, per-token LTVs and partial liquidations, check the order in which tokens are liquidated.
Can it be forced to take only the blue-chip assets and leave the position unhealthier?
Guardian ranked this one High, so the set is two Criticals and a High. For more on how liquidations go wrong, read liquidation vulnerabilities.
Checklist
- Can any withdraw, redeem or transfer path run with a zero amount, and does it change state for another user when it does?
- Does a zero-value allowance check let a caller act for an owner without approval?
- Do two contracts track the same object, and can an external function on one desync the other?
- Does every completion function send funds to the true owner, with access control where needed?
- Does a liquidation always leave the position at least as healthy as before?
- With basket collateral, which tokens are liquidated first, and can a liquidation remove only high-LTV assets?
For three more findings broken down the same way, see smart contract audit findings explained. For a wider view, read critical DeFi vulnerability patterns.
How Guardian Helps
Guardian's Vanguard audit puts two independent teams on one scope: expert manual review backed by an invariant fuzzing suite, and Helix, Guardian's AI auditor.
Invariants like "health never decreases after liquidation" become executable properties tested against every reachable state, and each team cross-checks the other's findings.
Frequently Asked Questions
Why is a zero-amount withdrawal dangerous?
Allowance checks often pass for a zero amount. If the call still changes the owner's state, such as deleting a cooldown, an attacker can repeat it for every account and block withdrawals.
What is parallel state in a smart contract system?
Two contracts tracking the same object separately. If anyone can act on one copy directly, the other goes stale, and funds can end up trapped in a contract with no way out.
Can a liquidation make a position less healthy?
Yes. With basket collateral and per-token LTVs, removing a high-LTV asset lowers the average LTV. Borrowing power falls faster than the debt, so the health score drops.
How should partial liquidations choose collateral?
Start with the lower-LTV, riskier assets, such as long-tail tokens. Removing them first raises the basket's average LTV, so the position's health improves.
Sources
Catch critical bugs before launch
Guardian's two-team audit model and invariant suites are built to find vulnerabilities like these before they reach production.