Smart Contract Security Checklist 2️⃣(Attacker's Mindset)
When Being "Trusted" Kills the Protocol: How Token Blacklists Can Hold Your Users Hostage

smart contract developer
What if the person you owe money to can no longer legally receive it, and that one fact breaks your entire protocol?
That’s not a hypothetical. It’s a live vulnerability class that has been found in real DeFi protocols, judged as medium severity, and quietly patched after audit findings. And if you’re building anything that moves tokens between addresses, especially stablecoins like USDC or USDT, this is something you genuinely cannot afford to skip.
Welcome back!!! If you’re just joining me, this is part of my ongoing series where I walk through the Solodit Smart Contract Security Checklist from an attacker’s mindset. Last time, we talked about how zero-value transactions can brick a protocol. Today, we’re going deeper into token behavior, specifically around blacklisting.
Let’s get into it.
The Question: “How does the protocol handle tokens with blacklisting functionality?”
Simple on the surface. Devastating when ignored.
This question forces you to think about a class of ERC-20 tokens that come with a special power: the ability to freeze specific addresses. Tokens like USDC (issued by Circle) and USDT (issued by Tether) are controlled by centralized issuers who can and do blacklist addresses associated with fraud, sanctions, regulatory violations, or court orders.
When an address gets blacklisted, it cannot send or receive that token. Full stop.
Now think about what that means for a DeFi protocol that assumes token transfers will always work.
The High-Level Idea: What Could Go Wrong?
Picture this: your protocol lets users borrow against liquidity positions. When a borrower repays their loan, the smart contract automatically sends fees to a creditor, the person who owns the liquidity that was borrowed against.
Everything looks clean. The logic is sound. The math checks out.
But what happens if that creditor’s wallet gets blacklisted by USDC?
When the code tries to send them their fees, the transfer reverts. And because this transfer is part of the repayment flow, the entire repayment transaction reverts. The borrower can no longer repay. No matter how much they want to, no matter how much money they have, the protocol refuses to process their repayment.
They are now being forced into default. Not because they did anything wrong. Not because they’re broke. But because someone else’s wallet got flagged.
That is not a hypothetical edge case. That is exactly what happened in the Real Wagmi audit on Sherlock.
Attacker Mindset
Let’s put on the attacker’s hat and look at this more carefully.
You’re reviewing a lending or leverage protocol. You notice the repayment logic does something like this for each active loan:
address creditor = underlyingPositionManager.ownerOf(loan.tokenId);
_increaseLiquidity(cache.saleToken, cache.holdToken, loan, amount0, amount1);
uint256 liquidityOwnerReward = FullMath.mulDiv(
params.totalfeesOwed,
cache.holdTokenDebt,
params.totalBorrowedAmount
) / Constants.COLLATERAL_BALANCE_PRECISION;
Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);
You see a direct token transfer from the vault to the creditor. Right in the middle of the repayment loop.
Now you ask yourself one question: what if that transfer fails?
In Solidity, if a token transfer reverts and there’s no error handling around it, the entire transaction reverts. Everything that came before it is undone. The borrower’s repayment attempt fails completely.
So as an attacker, or even as a careless LP, if you own liquidity positions and your wallet gets blacklisted for the hold token, you’ve accidentally (or intentionally) created a situation where the borrower is permanently stuck. They cannot repay. They will eventually be forced to default, and the LP can only recover their funds through emergency closure.
Now here’s where it gets interesting from a pure attack angle: an attacker could deliberately open a liquidity position with a wallet they know will eventually be blacklisted, or even manipulate the situation so their creditor wallet becomes restricted. The result? They can trap borrowers in defaults and extract value from the protocol’s emergency mechanisms.
Even without malicious intent, this is a landmine. Any real-world USDC blacklisting event, whether regulatory action, exchange hack, or sanctions, could trigger this silently.
Let’s See How This Plays Out with Alice and Bob
Suppose Alice runs a leverage protocol where borrowers take loans against collateral, and creditors provide liquidity. Fees accrue and get sent to creditors upon repayment.
Bob, a borrower, has a loan with a creditor whose address later gets blacklisted on the hold token (say, USDC). When Bob tries to repay:
Liquidity is restored to the pool.
Accumulated fees are calculated and transferred from the vault directly to the creditor.
Boom! The transfer reverts because of the blacklist.
Repayment fails, even though Bob has the funds.
The loan defaults, triggering penalties for Bob.
Liquidity providers can eventually pull out via emergency closure, but only after the default, leaving everyone worse off.
If an attacker like Charlie controls multiple blacklisted addresses, they could set up as creditors, lure borrowers, and systematically block repayments to force mass defaults. Chaos ensues, and Alice’s protocol reputation tanks.
Bottom line: What seems like a compliance feature in tokens becomes a weapon that makes your protocol brittle and unreliable.
Proof of Concept: The Real Wagmi Bug
Let’s walk through the actual vulnerability found in the Real Wagmi leverage protocol audit.
What the protocol does:
Real Wagmi is a leverage protocol where users borrow against Uniswap V3 liquidity positions. When a borrower repays, the contract:
Restores liquidity to the pool.
Calculates fees owed to the creditor (the liquidity position owner).
Transfers those fees directly from the vault to the creditor.
The vulnerable code:
// From LiquidityManager.sol#L306-L315
address creditor = underlyingPositionManager.ownerOf(loan.tokenId);
// Increase liquidity and transfer liquidity owner reward
_increaseLiquidity(cache.saleToken, cache.holdToken, loan, amount0, amount1);
uint256 liquidityOwnerReward = FullMath.mulDiv(
params.totalfeesOwed,
cache.holdTokenDebt,
params.totalBorrowedAmount
) / Constants.COLLATERAL_BALANCE_PRECISION;
Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);
This block runs for every loan during repayment. There is no try-catch. There is no fallback. If the transferToken call fails for any reason, the repayment fails completely.
The attack scenario:
A creditor’s wallet (the owner of a Uniswap LP position used as collateral) gets blacklisted by USDC, whether through regulatory action or intentional manipulation.
A borrower who used that LP position tries to repay their loan.
The contract computes fees, restores liquidity, then tries to transfer
liquidityOwnerRewardin USDC to the blacklisted creditor.USDC’s
transfer()reverts because the recipient is blacklisted.The entire repayment transaction reverts.
The borrower cannot repay. Ever. Through the normal flow.
The borrower eventually defaults.
LPs can recover funds, but only after default and only through emergency closure.
The borrower did nothing wrong. But they pay the price.
What the auditors found:
The issue was discovered by four independent researchers: 0x52, ArmedGoose, Bauer, and tsvetanovv. It was flagged as a medium-severity finding. The core of their report:
“If the creditor is blacklisted for the hold token then the fee transfer will always revert. This forces the borrower to default. LPs can recover their funds but only after the user has defaulted and they request emergency closure.”
The fix was merged into the protocol’s codebase at commit 3c17a39 on the RealWagmi GitHub.
Why This Is More Common Than You Think
Most protocols that integrate USDC or USDT never think about this. Why would they? Token transfers “just work” in 99.9% of cases.
But here’s what you need to internalize: USDC blacklisting is not theoretical. Circle has blacklisted thousands of addresses, many of them tied to sanctions compliance, exchange hacks (like the Tornado Cash incident), and court orders. USDT does the same. As DeFi becomes more intertwined with regulatory reality, blacklisting events will only become more frequent.
Any protocol that:
Directly transfers tokens to user-supplied addresses,
Relies on those transfers succeeding to complete a transaction,
And uses tokens with blacklisting functionality…
…is sitting on a time bomb.
It’s Not Just Lending Protocols
This vulnerability pattern shows up anywhere a protocol automatically pushes tokens to addresses it doesn’t control. Think about:
Yield distributors: protocols that iterate over a list of recipients and send rewards. One blacklisted address in the list breaks the entire distribution.
Fee routers: protocols that split fees between partners, treasuries, and LPs. If any recipient can’t receive tokens, the whole split fails.
Auction settlements: protocols that automatically transfer tokens to winners. A blacklisted winner blocks settlement.
Payment splitters: any contract that does for each recipient → transfer. Classic DoS through blacklisting.
The pattern is always the same: the protocol assumes a transfer will succeed, and when it doesn’t, the entire function breaks.
Impact
Forced Defaults: Borrowers can’t repay, leading to penalties, liquidations, and lost collateral.
Fund Lockups: LPs and users wait for emergency procedures, tying up capital unnecessarily.
Operational Disruption: Protocols become unreliable, scaring off users and reducing TVL.
Reputational Damage: Word spreads fast in DeFi — one blacklisting event could label your project as “unstable.”
Broader Ecosystem Risk: If multiple protocols ignore this, a mass blacklisting (e.g., due to regulations) could cascade failures across chains.
Recommended Mitigation
There are two clean approaches here, and the best protocols use a combination of both.
1. The Escrow / Pull-Over-Push Pattern
Instead of pushing tokens directly to the recipient, hold them in escrow and let the recipient claim them.
mapping(address => mapping(address => uint256)) public pendingClaims;
function _creditReward(address token, address recipient, uint256 amount) internal {
pendingClaims[token][recipient] += amount;
emit RewardCredited(token, recipient, amount);
}
function claimReward(address token) external {
uint256 amount = pendingClaims[token][msg.sender];
require(amount > 0, "Nothing to claim");
pendingClaims[token][msg.sender] = 0;
IERC20(token).transfer(msg.sender, amount);
}
With this pattern, if a recipient is blacklisted, it only affects them. They just can’t claim. The rest of the protocol continues working perfectly. Other users are unaffected.
2. Try-Catch with Escrow Fallback
If you need to attempt a direct transfer (for UX or gas efficiency reasons), wrap it in a try-catch and fall back to escrow if it fails.
try IERC20(token).transfer(creditor, amount) {
// transfer succeeded, carry on
} catch {
// transfer failed, could be blacklisted, paused, or other revert
pendingClaims[token][creditor] += amount;
emit TransferFailed(token, creditor, amount);
}
This is exactly what the Real Wagmi auditors recommended. The transaction no longer reverts just because one recipient can’t receive tokens. The funds are held safely in escrow, and the rest of the operation completes normally.
3. Consider Token Compatibility Upfront
If you’re building a protocol that will integrate any ERC-20, categorize your token risk:
Having this mental model before writing a single line of transfer logic saves a lot of pain later.
For Developers and Auditors
If you’re writing smart contracts:
Never assume a token transfer will succeed, especially with stablecoins.
Prefer the pull pattern (claimable balances) over the push pattern (direct transfers) whenever the recipient address is user-controlled.
If you must push, wrap it in a try-catch with a fallback.
Add a
blacklistedTokentest scenario to your test suite. Call USDC's blacklist function on a recipient, then try your repayment or distribution flow. If it reverts, you have a vulnerability.
If you’re auditing:
Search for any direct
transferortransferFromcalls where the recipient is a user-supplied address.Ask: “What happens if this transfer reverts?”
If the answer is “the whole transaction reverts,” that’s your finding.
Check if the token list includes USDC, USDT, or any other token with blacklisting or pausing capabilities.
Let’s Recap
Here’s the vulnerability in one paragraph:
Protocols that directly push tokens to user-supplied addresses assume the transfer will always succeed. Tokens like USDC and USDT can blacklist addresses, causing transfers to that address to revert. If such a transfer is embedded in a critical protocol flow, like loan repayment, one blacklisted address can block the entire operation. Borrowers get forced into default. Users lose access to funds. The protocol breaks, not from a hack, but from a compliance event happening somewhere else.
The fix is conceptually simple: don’t make critical operations dependent on the success of a single external transfer. Use escrow. Use try-catch. Let users pull their own funds.
As always, this is why the Solodit Security Checklist exists. It captures exactly these kinds of questions, the ones that seem obvious in hindsight but get missed completely in the heat of building.
Next question in the checklist coming soon. See you then.
Found this useful? Follow for more entries in the Smart Contract Security Checklist series.



