Root Cause
Lien Finance's exchangeEquivalentBonds function allows users to exchange one group of bonds for an equivalent set — a structured-product operation requiring that the output bonds are genuinely backed by the input bonds. The function verified compliance by checking the count of "exception" bond IDs in the output list. It did not verify completeness: that every distinct required bond ID appeared in the input group.
By listing the same exception bond ID multiple times, an attacker satisfied the count check while omitting other required collateral bonds from the input — effectively minting synthetic bond tokens backed by nothing.
// Vulnerable completeness check (reconstructed from SlowMist analysis)
function exchangeEquivalentBonds(
uint256[] calldata inputBondIds,
uint256[] calldata outputBondIds
) external {
uint256 exceptionCount = 0;
for (uint256 i = 0; i < outputBondIds.length; i++) {
if (isException(outputBondIds[i])) {
exceptionCount++; // BUG: counts total entries, not unique IDs
}
}
// Passes if attacker repeats the same exception bondId N times
require(exceptionCount == requiredExceptions, "exception count mismatch");
// MISSING: verify each distinct required bondId appears in inputBondIds
// for (uint i = 0; i < requiredBondIds.length; i++) {
// require(inputContains(inputBondIds, requiredBondIds[i]), "missing bond");
// }
_mintBonds(outputBondIds); // Attacker receives bonds with no real collateral
}
Attack Steps
| Step | Action | Detail |
|---|---|---|
| 1 | Register group | Permissionlessly registered a fraudulent bond group with the target exception bond ID |
| 2 | Craft call | Constructed outputBondIds with the same exception bond ID repeated N times |
| 3 | Count check passes | exceptionCount == requiredExceptions satisfied by repetition |
| 4 | Mint synthetic bonds | Protocol mints output bond tokens with no real collateral input |
| 5 | Sell to OTC pool | Swapped synthetic bonds into Lien Finance OTC pools for ~$542,000 USDC |
| 6 | Exit | Withdrew USDC from the protocol |
Impact
- Loss: ~$542,000 USDC from Lien Finance OTC pools
- Protocol: Lien Finance on Ethereum (structured bond products)
- Chain: Ethereum
- Detection: SlowMist and Verichains published post-mortems post-drain
- Historical note: Lien Finance had a prior bond issuance bug rescued by whitehats in September 2020, highlighting the bond-exchange logic as a recurring high-risk surface
Lessons for Auditors
- Check completeness, not just count. When a function verifies a list contains every element of a required set,
length == requiredallows duplicates to satisfy the count while omitting other required elements. Use amapping(id => bool) seenand iterate the required set. - Permissionless group registration is a high-risk entry point. If users can freely register bond groups, the registration logic must tightly constrain what may be registered to prevent use in downstream exploits.
- Structured-product token accounting must be invariant-tested. Fuzz with Foundry/Echidna targeting the invariant:
totalCollateralDeposited >= sum(outstandingBonds * minCollateralPerBond). - OTC pools accepting protocol-minted tokens need independent collateral checks. A minting flaw should not directly translate to fund drainage — pools should verify token provenance or enforce collateral ratios independently.