PoW-gated public mode
Pool.liquidate(victim, partial_exp) is always callable by anyone — it's a permissionless external function. But the call only succeeds if governance has given the pool the right to internally invoke the otherwise permissioned square function. Concretely: liquidate() ends with this.square(msg.sender, victim, partial_exp), and msg.sender for that internal call is the pool address itself. The role check on square() therefore tests the pool, not the external caller. Granting POOL_SQUARE_ROLE to the pool enables the permissionless path; revoking it disables the permissionless path while leaving direct square() calls by other role-holders unaffected.
Layered on top of that on/off switch, governance controls how expensive the call is, via the POW_SQUARE parameter (per partial_exp). When POW_SQUARE is high, the public path becomes prohibitively expensive and only role-holders (calling square directly) liquidate in practice. When POW_SQUARE = 0, the PoW gate is disabled and anyone can liquidate freely — provided the pool still holds POOL_SQUARE_ROLE.
The PoW gate exists for cases where:
- The set of
POOL_SQUARE_ROLEholders is too small or too centralised. - Specific liquidations aren't being picked up by authorised keepers.
- Governance wants to widen liquidation access during stress.
How the gate works
Internally liquidate(victim, partial_exp) is annotated with powlimited(liquidateDifficultyOf(partial_exp)). The powlimited modifier:
- Looks up the per-
partial_expdifficultyD = parameterOf(POW_SQUARE_ID(partial_exp)). - If
D > 0, computeskey = keccak256(blockHash, tx.origin, msg.data)against a recently cached block hash. - Counts the leading zero bits of
key. If fewer thanD, reverts withPowLimited(key, D). - Otherwise the call proceeds and
liquidateinvokesthis.square(msg.sender, victim, partial_exp).
Notes:
- The PoW input includes
tx.originand the fullmsg.data, so the puzzle is bound to a specific caller and a specific (victim,partial_exp) target. - The block hash is cached and refreshed only after
cacheTime, so a successfully mined nonce is reusable for a window — the puzzle isn't tied to a single block, but it does become invalid after the cache rolls over. - The "difficulty" is a count of leading zero bits in the hash (so
D = 16means roughly 1 in 65 536 attempts).
Why PoW?
PoW's primary purpose is to introduce weak entropy (i.e. randomness) into the liquidation process, to reduce liquidator centralization (if governance chooses so). Without that randomness, the permissionless path collapses into a priority-gas auction — the fastest, best-tuned bot wins every race and a small oligopoly captures every liquidation, mirroring the MEV-centralization pattern already visible on existing lending markets. Because mining is probabilistic, no single bot can deterministically win on latency and gas-bid alone; governance tunes POW_SQUARE per partial_exp to widen the field (higher difficulty, more randomness) or favor speed (lower difficulty, faster reaction).
A secondary, incidental effect is that the puzzle makes spamming liquidate against still-healthy victims more expensive — but gas already discourages that (the caller pays for the supervisor pre-check before any revert), so spam-resistance is a side benefit rather than the motivation.
Computing the PoW
The puzzle is small — a few seconds of CPU at default difficulty. A keeper can mine off-chain and submit the result with the call. The banq-cli liquidate command bundles the mining step automatically (use -Y / --broadcast to actually submit). See CLI and tools.
Failure modes
Same as square(), plus:
- Insufficient PoW. The on-chain check sees fewer leading zeros than
POW_SQUARErequires and reverts withPowLimited(key, D). - Stale PoW. The cached block hash rolled over after your nonce was mined — re-mine against the new hash.
- Permissionless path disabled. If governance has not (or no longer has) granted
POOL_SQUARE_ROLEto the pool address, the innerthis.square(...)reverts on the role check and the wholeliquidate()call fails — regardless of the PoW.
Where to go next
- Running a liquidator — production setup
- Debt-assumption liquidation — the underlying mechanism
- CLI and tools —
banq-cli liquidate