SpokeComptroller
SpokeComptroller is the Comptroller of a hub-funded spoke pool. It is a fork of the shared isolated-pools Comptroller and behaves identically except where this page says otherwise: the same hooks, the same account-liquidity model, the same market listing and pause machinery, the same rewards flywheel.
Read Hub-Funded Spoke Pools first for why the fork exists and how a spoke pool is shaped.
What the fork changes
Section titled “What the fork changes”| Area | Shared Comptroller | SpokeComptroller |
|---|---|---|
| Supplying to a market | anyone, subject to the supply cap | optionally restricted to a per-market allowlist, checked in preMintHook |
| Seizing collateral | anyone | optionally restricted to a pool-wide liquidation allowlist |
| Liquidation incentive | one pool-wide value | per collateral market, falling back to the pool-wide value |
| Risk-parameter safety | no cross-check between threshold and incentive | setCollateralFactor and setMarketLiquidationIncentive both reject a liquidation threshold times incentive that reaches 1 |
| Batch-liquidation routing | totalCollateral vs borrows × incentive | borrows vs maxClearableDebt = Σ Cᵢ / incentiveᵢ |
| Borrow-power pricing | ResilientOracle spot | DeviationBoundedOracle bounded prices |
| Entering a market | enterMarkets only, always for msg.sender | adds ACM-gated enterMarketForAccount, so a gateway contract can enable collateral for the supplier it minted to |
| Prime | prime + setPrimeToken, seven *Verify hooks update Prime scores | removed; the seven hooks are no-ops |
healAccount pause checks | SEIZE (through preSeizeHook) and REPAY (through healBorrow) | adds LIQUIDATE in every market the account owes a nonzero balance in |
| Revert reasons | mixture of custom errors and require strings | custom errors throughout, except inherited checks: setMaxLoopsLimit and initialize still revert with MaxLoopsLimitHelper’s "Comptroller: Invalid maxLoopsLimit", and the onlyOwner setters (setPriceOracle, setDeviationBoundedOracle, addRewardsDistributor, setMaxLoopsLimit) with OpenZeppelin’s Ownable strings, and acceptOwnership with its Ownable2Step string |
Exit is never gated, in either implementation. Redeem, repay and transfer are not access-controlled, and an account removed from an allowlist keeps the position it already holds and can still leave. Governance retains the usual market-level pause.
Supply allowlist
Section titled “Supply allowlist”Per market, and off by default, so a newly listed market accepts supply from anyone.
mapping(address vToken => bool) public isSupplyAllowlistEnabled;mapping(address vToken => mapping(address account => bool)) public isAllowedSupplier;It is enforced in exactly one place, preMintHook:
if (isSupplyAllowlistEnabled[vToken] && !isAllowedSupplier[vToken][minter]) { revert SupplyNotAllowed(vToken, minter);}The account metered is the one credited with the vTokens, not the one paying for them. VToken.mintBehalf lets a third party fund a mint attributed to someone else, and only the recipient is checked. This is deliberate: metering the recipient is what bounds a market’s supply, and a third party funding a mint credited to the Hub’s YieldGroup is a donation to the Hub, not a way around the restriction.
Only minting is metered. Transfers and seizures do not check the list, so vTokens can still reach an account that is not on it: the allowlist bounds who can create supply, not who can hold it. Redeeming is never restricted. Enabling the allowlist on a market that is already serving supply cuts off every account that is not on the list, including whoever seeded it. That is the intended effect, but it is worth knowing before flipping it on a live market.
On the liquidity side of a hub-funded pool the single allowlisted account is the Liquidity Hub’s Spoke YieldGroup. The listing VIP must call setAllowedSupplier before the Hub registers the market as a resource: AdapterSpokeV1.validateRegistration rejects a market that will not accept the registering YieldGroup.
Entering a market for a supplier
Section titled “Entering a market for a supplier”enterMarkets reads msg.sender, so a contract that supplies through VToken.mintBehalf enters itself rather than the account it credited. On the collateral side of a spoke pool the user supplies and the Hub lends, so without help a first-time supplier needs two transactions: one to supply, one to enable the market as collateral.
enterMarketForAccount takes the account as an argument instead:
function enterMarketForAccount(address account, address vToken) external;It takes a single market rather than an array, which is what keeps the fork inside the EIP-170 limit; a caller that needs several markets loops over it. enterMarkets is untouched, so entering a market for yourself stays permissionless.
The function is gated by the AccessControlManager under the role string enterMarketForAccount(address,address). It takes the same arguments in the same order as the Core pool’s enterMarketForAccount, but returns nothing where the Core version returns a uint256 error code. A caller written for the Core version that decodes that return value reverts here. Grant it only to a contract that passes its own caller as account. A holder of this permission can enable a market as collateral for any address, and entering a market is what makes a balance eligible to be seized in a liquidation. The Collateral Gateway is the intended holder. Its spoke functions need this role on the pool; on BNB Chain it is granted for the bStock index pool.
Liquidation allowlist
Section titled “Liquidation allowlist”Pool-wide, and off by default.
bool public isLiquidationAllowlistEnabled;mapping(address account => bool) public isAllowedLiquidator;It is pool-wide rather than per market because healAccount seizes across every market the borrower is in and cannot attribute a seizure to a single one of them.
Two call sites enforce it:
preSeizeHook: every seizure reaches this hook carrying the account that receives the collateral, whichever entry point it came from, so this is the check that actually enforces the list.healAccount, at the entry: a borrower holding no vTokens at all takes a branch that only callshealBorrowand never reaches a hook carrying the caller. Without this check anyone could move the whole remaining principal into bad debt at no cost.
liquidateAccount has no entry check of its own. Every one of its orders ends in a seizure, so preSeizeHook rejects a caller that is not on the list while the first order is being processed.
Enabling the list also restricts healAccount, so any keeper relied on to record bad debt has to be allowlisted too.
Note that when the collateral is a tokenized stock liquidated through BStockLiquidator, it is that contract’s address that has to be on the list, not the operator’s. The liquidator contract is the account that receives the seized collateral.
Per-market liquidation incentive
Section titled “Per-market liquidation incentive”mapping(address vTokenCollateral => uint256) public liquidationIncentives;Keyed on the collateral market, because the incentive is the discount on the collateral being seized. 0 is the “unset” sentinel, in which case the pool-wide value applies:
effectiveLiquidationIncentive(vToken) = liquidationIncentives[vToken] != 0 ? liquidationIncentives[vToken] : poolLiquidationIncentiveThe motivation is risk steering: a higher incentive on high-volatility collateral pushes liquidators to seize the riskiest collateral first, which de-risks an account faster. A borrow-only market (collateral factor 0) is never seized in normal operation, so its incentive is not used to price a liquidation. It is still read by setCollateralFactor, and vTokens transferred into the market by an entered account can be seized, so it should still hold a valid value.
Two getters, deliberately different.
effectiveLiquidationIncentive(address vToken): takes the market as an argument. This is what a lens, a keeper or a liquidation bot should read.liquidationIncentiveMantissa(): takes no argument and answers formsg.sender. This is the getterComptrollerViewInterfacedeclares and the one aVTokencalls on its Comptroller:VToken._seizedivides the protocol seize share by it andVToken.setProtocolSeizeSharebounds that share against it, and both need the discount that prices the calling market’s own collateral. Any caller with no per-market value stored reads the pool-wide value from it, including any caller that is not a market of this pool. A market that was unlisted after setting its own value still reads that value.
Floors. An incentive is a multiplier on the debt repaid, and VToken._seize hands the protocol protocolSeizeShareMantissa of that debt out of the seized collateral. An incentive below 1e18 + protocolSeizeShareMantissa therefore pays the liquidator less collateral than it repaid, and nobody liquidates that market.
setMarketLiquidationIncentiveenforces≥ 1e18 + vToken.protocolSeizeShareMantissa()against the market’s live share.setLiquidationIncentive(the pool-wide value) enforces≥ MIN_POOL_LIQUIDATION_INCENTIVE_MANTISSA = 1.05e18, which is1e18plusVToken’s default seize share. That covers a newly listed market carrying the default share and no incentive of its own. This is stricter than the sharedComptroller, which stops at1e18and would let a pool be registered with a value that pays every default-share market’s liquidator less than it repaid.VToken.setProtocolSeizeShareholds the same bound from the other side, reading the incentive back throughliquidationIncentiveMantissa().
A market whose seize share is later raised above the default still needs an incentive of its own; the pool-wide floor is a backstop for the newly listed case, not a full guarantee.
There is no way back to “unset” once a per-market value is stored: setMarketLiquidationIncentive rejects 0, so a mistaken zero cannot silently move a market back onto the pool-wide value. Passing the current pool-wide value is not the same as unsetting either: the market is pinned at that number and stops following later setLiquidationIncentive changes, so a pool-wide change has to update those markets too.
setLiquidationIncentive does not recheck the markets that fall back to it, so give every market with a nonzero liquidation threshold an incentive of its own and leave the pool-wide value applying to none.
Batch-liquidation routing
Section titled “Batch-liquidation routing”The shared Comptroller routes an account that is in shortfall and whose collateral is at or below minLiquidatableCollateral between liquidateAccount and healAccount using one pool-wide incentive. With per-market incentives that no longer works, so the account snapshot carries a new field:
maxClearableDebt = Σ over the account's collateral markets of ( Cᵢ / incentiveᵢ )where Cᵢ is the account’s collateral value in market i and incentiveᵢ is that market’s own effective incentive. It is the largest borrow value the account’s collateral can clear without leaving bad debt behind.
The two batch operations are the two sides of that one benchmark, so every such account routes to exactly one of them, with no gap and no overlap:
| Applies when | Otherwise reverts with | |
|---|---|---|
liquidateAccount | borrows < maxClearableDebt | DebtExceedsClearableAmount(borrows, maxClearableDebt) |
healAccount | maxClearableDebt ≤ borrows | CollateralCoversDebt(borrows, maxClearableDebt) |
healAccount’s repayment share becomes:
percentage = maxClearableDebt / borrowsOne blended share applies to every borrow, so what the caller pays in total is the sum over the collateral markets of each market’s value at its own incentive. The discount is exact in aggregate; it is not attributed per piece of collateral.
Each market’s repayment is rounded up, so the rounding falls on the caller rather than on the market’s suppliers. It can never exceed the borrow balance, because the routing gate keeps maxClearableDebt at or below borrows.
liquidateAccount keeps its order loop unchanged: each order is a (debt, collateral) pair, and liquidateCalculateSeizeTokens prices it at the collateral market’s own incentive. What changed is the gate above and the market refresh described below.
maxClearableDebt is only accumulated on liquidation-threshold snapshots. A collateral-factor snapshot leaves it at zero, and reading it there would put healAccount on a repayment percentage of zero, forgiving the whole position as bad debt.
Both batch operations still require the account’s total collateral to be at or below minLiquidatableCollateral; above that threshold they revert CollateralExceedsThreshold and the position must go through the regular VToken.liquidateBorrow path.
Both also accrue interest and push an oracle update for every market the borrower is in before taking the routing snapshot. The orders inside liquidateAccount run with the per-order liquidity check skipped, so that snapshot is the only thing separating a solvent account from a full liquidation, and it has to be taken on the same state the orders will execute against.
Bounded collateral pricing
Section titled “Bounded collateral pricing”A spoke pool values borrowing capacity through the DeviationBoundedOracle rather than at spot. This is the same mechanism the Core pool uses; see the DeviationBoundedOracle article for the window, trigger and exit logic, and Protection Mode for the user-facing behavior.
Which oracle is read depends on which risk parameter weights the snapshot:
| Weighting | Used by | Collateral price | Debt price |
|---|---|---|---|
USE_COLLATERAL_FACTOR | preBorrowHook, preRedeemHook, preTransferHook, exitMarket, getBorrowingPower, getHypotheticalAccountLiquidity | deviationBoundedOracle | deviationBoundedOracle |
USE_LIQUIDATION_THRESHOLD | preLiquidateHook, healAccount, liquidateAccount, getAccountLiquidity | oracle (spot) | oracle (spot) |
While protection is active for an asset the bounded oracle values collateral at the low end of its recent price window and debt at the high end; otherwise it returns spot on both legs, including for an asset it holds no configuration for. A print that triggers protection can therefore only ever shrink an account’s borrowing capacity, never inflate it. While protection is off, a move too small to trigger it is priced at spot, exactly as the shared Comptroller prices every check, so capacity follows it up or down.
The liquidation paths stay on spot because they route liquidations and set how much of a position healing repays, which has to track the live price. liquidateCalculateSeizeTokens is likewise priced at spot.
Both preBorrowHook and _checkRedeemAllowed (which serves redeem, transfer and exit) call deviationBoundedOracle.updateProtectionState(vToken) for every market the account is in before pricing, so a deviating print latches protection and starts its cooldown instead of evaporating once the price returns to the window.
deviationBoundedOraclemust be set before the pool serves any borrow, redeem, transfer or market exit. Nothing falls back to spot when it is unset: theupdateProtectionStateandgetBoundedPricesViewcalls go to the zero address and revert, so all four fail closed rather than running unbounded. Minting and repaying are unaffected because neither calls the bounded oracle. So is any redeem, transfer or exit that moves zero tokens or is made by an account that is not a member of the market, since the check returns before pricing in both cases. A supply-only account such as the Liquidity Hub’s YieldGroup never becomes a market member, so it can still exit a market whose pool has no bounded oracle set.
Solidity API
Section titled “Solidity API”Only the surface this fork adds or changes is listed here. Everything else matches the shared Comptroller. Three changed functions are covered by sections above rather than entries of their own: setCollateralFactor (also throws UnsafeLiquidationParams, see Floors), preSeizeHook (also throws LiquidationNotAllowed, see Liquidation allowlist) and liquidateCalculateSeizeTokens (prices at the collateral market’s own incentive, see Batch-liquidation routing).
setMarketLiquidationIncentive
Section titled “setMarketLiquidationIncentive”Sets the discount a liquidator receives on the collateral it seizes from a single market.
function setMarketLiquidationIncentive(address vToken, uint256 newLiquidationIncentiveMantissa) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| vToken | address | The collateral market to set the incentive for |
| newLiquidationIncentiveMantissa | uint256 | New incentive for this market, scaled by 1e18, at least 1e18 + protocolSeizeShareMantissa of the market |
📅 Events
Section titled “📅 Events”- Emits
NewMarketLiquidationIncentiveon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
❌ Errors
Section titled “❌ Errors”MarketNotListedis thrown if the market is not listedInvalidLiquidationIncentiveis thrown if the new incentive would leave the liquidator with less collateral than the debt it repaid,0includedUnsafeLiquidationParamsis thrown if the market’s liquidation threshold times the new incentive would reach1
setSupplyAllowlistEnabled
Section titled “setSupplyAllowlistEnabled”Restricts supplying to a market to the accounts on its supply allowlist, or lifts the restriction.
function setSupplyAllowlistEnabled(address vToken, bool enabled) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| vToken | address | The market to change the setting for |
| enabled | bool | Whether the market should accept supply only from allowlisted accounts |
📅 Events
Section titled “📅 Events”- Emits
SupplyAllowlistEnabledUpdatedon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
❌ Errors
Section titled “❌ Errors”MarketNotListedis thrown if the market is not listed
setAllowedSupplier
Section titled “setAllowedSupplier”Adds an account to a market’s supply allowlist or removes it. Takes effect only while that market’s allowlist is enabled. Setting an account to the value it already holds is not an error, so a governance action that overlaps an earlier one still executes.
function setAllowedSupplier(address vToken, address supplier, bool allowed) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| vToken | address | The market whose allowlist to update |
| supplier | address | The account to add or remove |
| allowed | bool | Whether the account should be allowed to supply |
📅 Events
Section titled “📅 Events”- Emits
AllowedSupplierUpdatedon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
❌ Errors
Section titled “❌ Errors”MarketNotListedis thrown if the market is not listedZeroAddressNotAllowedis thrown if the account is the zero address
setLiquidationAllowlistEnabled
Section titled “setLiquidationAllowlistEnabled”Restricts seizing collateral in this pool to the accounts on the liquidation allowlist, or lifts the restriction. Also restricts healAccount.
function setLiquidationAllowlistEnabled(bool enabled) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| enabled | bool | Whether seizing collateral should be restricted to allowlisted accounts |
📅 Events
Section titled “📅 Events”- Emits
LiquidationAllowlistEnabledUpdatedon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
setAllowedLiquidator
Section titled “setAllowedLiquidator”Adds an account to the pool’s liquidation allowlist or removes it. Takes effect only while the allowlist is enabled.
function setAllowedLiquidator(address liquidator, bool allowed) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| liquidator | address | The account to add or remove |
| allowed | bool | Whether the account should be allowed to seize collateral |
📅 Events
Section titled “📅 Events”- Emits
AllowedLiquidatorUpdatedon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
❌ Errors
Section titled “❌ Errors”ZeroAddressNotAllowedis thrown if the account is the zero address
enterMarketForAccount
Section titled “enterMarketForAccount”Adds a market to another account’s liquidity calculation, enabling it as collateral for that account.
function enterMarketForAccount(address account, address vToken) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| account | address | The account to enable it for |
| vToken | address | The market to enable |
📅 Events
Section titled “📅 Events”- Emits
MarketEnteredwhen the account was not already a member; nothing is emitted otherwise
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
❌ Errors
Section titled “❌ Errors”ActionPausedis thrown if entering the market is pausedMarketNotListedis thrown if the market is not listedZeroAddressNotAllowedis thrown when the account address is zero
setDeviationBoundedOracle
Section titled “setDeviationBoundedOracle”Sets the deviation-bounded oracle the pool prices borrowing capacity through.
function setDeviationBoundedOracle(IDeviationBoundedOracle newBoundedOracle) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| newBoundedOracle | IDeviationBoundedOracle | Address of the new deviation-bounded oracle |
📅 Events
Section titled “📅 Events”- Emits
NewDeviationBoundedOracleon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Only the owner (governance)
❌ Errors
Section titled “❌ Errors”ZeroAddressNotAllowedis thrown when the new oracle address is zero
setLiquidationIncentive
Section titled “setLiquidationIncentive”Sets the liquidation incentive applied to any market that has no incentive of its own. Unchanged from the shared Comptroller except for the floor and the revert type.
function setLiquidationIncentive(uint256 newLiquidationIncentiveMantissa) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| newLiquidationIncentiveMantissa | uint256 | New pool-wide incentive, scaled by 1e18, at least 1.05e18 |
📅 Events
Section titled “📅 Events”- Emits
NewLiquidationIncentiveon success
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Controlled by AccessControlManager
❌ Errors
Section titled “❌ Errors”InvalidLiquidationIncentiveis thrown if the new incentive is belowMIN_POOL_LIQUIDATION_INCENTIVE_MANTISSA
effectiveLiquidationIncentive
Section titled “effectiveLiquidationIncentive”The discount that applies to a market: its own if it has one, otherwise the pool-wide value.
function effectiveLiquidationIncentive(address vToken) external view returns (uint256)Parameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| vToken | address | The collateral market to read the discount of |
Return Values
Section titled “Return Values”| Name | Type | Description |
|---|---|---|
| [0] | uint256 | The discount that prices this market’s collateral, scaled by 1e18 |
liquidationIncentiveMantissa
Section titled “liquidationIncentiveMantissa”The discount a liquidator receives on the collateral it seizes from the calling market. Answers for msg.sender rather than taking the market as an argument. See Per-market liquidation incentive.
function liquidationIncentiveMantissa() external view returns (uint256)Return Values
Section titled “Return Values”| Name | Type | Description |
|---|---|---|
| [0] | uint256 | The discount that applies to the caller, scaled by 1e18 |
healAccount
Section titled “healAccount”Seizes all of a given account’s collateral, requiring the caller to repay maxClearableDebt / borrows of its debt in every market. The unpaid debt is recorded as badDebt on each market. The Shortfall auction contract cannot auction it: it only finds pools in the pool registry it is configured with, and that registry does not hold spoke pools. Recovery is a governance action instead. badDebtRecovered can only be called by the address stored in the market’s shortfall variable, and on BNB Chain that address is the Normal Timelock, so a proposal sends the underlying to the market and calls it to write the recovered amount off badDebt.
The heal is all or nothing: every seizure and repayment runs in this one call, so a pause that blocks any of them reverts the whole heal.
function healAccount(address user) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| user | address | The account to heal |
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Not restricted while the liquidation allowlist is disabled, otherwise the caller has to be on it
❌ Errors
Section titled “❌ Errors”LiquidationNotAllowedis thrown if the liquidation allowlist is enabled and the caller is not on itCollateralExceedsThresholdis thrown when the account’s collateral is aboveminLiquidatableCollateralCollateralCoversDebtis thrown when the collateral can clear the whole debt at each market’s own incentive, which leaves nothing to heal; useliquidateAccountInsufficientShortfallis thrown when the account is not liquidatableActionPausedis thrown ifSEIZEis paused in a market the account holds collateral in, orLIQUIDATEorREPAYis paused in a market it owes a nonzero balance in (REPAYonly when the repay amount is not zero)SnapshotErroris thrown if a market fails to return the account’s snapshotPriceErroris thrown if the oracle returns a zero price for a market the account is in
liquidateAccount
Section titled “liquidateAccount”Liquidates all of a borrower’s positions in one call, executing the supplied orders. Skips the close-factor check.
function liquidateAccount(address borrower, struct LiquidationOrder[] orders) externalParameters
Section titled “Parameters”| Name | Type | Description |
|---|---|---|
| borrower | address | The account to liquidate |
| orders | LiquidationOrder[] | The (vTokenCollateral, vTokenBorrowed, repayAmount) orders to execute |
⛔️ Access Requirements
Section titled “⛔️ Access Requirements”- Not restricted while the liquidation allowlist is disabled, otherwise the caller has to be on it
❌ Errors
Section titled “❌ Errors”LiquidationNotAllowedis thrown if the liquidation allowlist is enabled and the caller is not on itCollateralExceedsThresholdis thrown when the account’s collateral is aboveminLiquidatableCollateralDebtExceedsClearableAmountis thrown when the debt is too large for the collateral to clear; usehealAccountMarketNotListedis thrown if either market in any order is not listedNonzeroBorrowBalanceAfterLiquidationis thrown if a debt remains in any of the borrower’s markets once every order has executedInsufficientShortfallis thrown when the account is not liquidatableMaxLoopsLimitExceededis thrown if half the number of orders exceeds the loop limit (the check is_ensureMaxLoops(ordersCount / 2))ActionPausedis thrown ifLIQUIDATEorREPAYis paused in a borrowed market, orSEIZEin a collateral market, of any orderTooMuchRepayis thrown if an order repays more than the borrow balanceMarketNotCollateralis thrown if the borrower has not entered the collateral market of an order
preMintHook
Section titled “preMintHook”Unchanged apart from the allowlist check.
function preMintHook(address vToken, address minter, uint256 mintAmount) external❌ Errors
Section titled “❌ Errors”SupplyNotAllowedis thrown if the market’s supply allowlist is enabled and the minter (the account credited with the vTokens, not necessarily the payer) is not on it- plus every error the shared
Comptrollerthrows here
Post-action hooks
Section titled “Post-action hooks”mintVerify, redeemVerify, borrowVerify, repayBorrowVerify, liquidateBorrowVerify, seizeVerify and transferVerify are all no-ops. They cannot be removed: ComptrollerInterface declares all seven, and the VToken calls each of them with a plain external call against a Comptroller that has no fallback, so a missing function would revert the operation it belongs to.
Events
Section titled “Events”Everything the shared Comptroller emits, plus:
| Event | Emitted when |
|---|---|
NewMarketLiquidationIncentive(address indexed vToken, uint256 oldLiquidationIncentiveMantissa, uint256 newLiquidationIncentiveMantissa) | a single market’s liquidation incentive changes |
NewDeviationBoundedOracle(IDeviationBoundedOracle oldBoundedOracle, IDeviationBoundedOracle newBoundedOracle) | the deviation-bounded oracle changes |
SupplyAllowlistEnabledUpdated(address indexed vToken, bool enabled) | a market’s supply allowlist is enabled or disabled |
AllowedSupplierUpdated(address indexed vToken, address indexed supplier, bool allowed) | an account is added to or removed from a market’s supply allowlist |
LiquidationAllowlistEnabledUpdated(bool enabled) | the pool’s liquidation allowlist is enabled or disabled |
AllowedLiquidatorUpdated(address indexed liquidator, bool allowed) | an account is added to or removed from the liquidation allowlist |
NewPrimeToken is not emitted. Prime is not part of a spoke pool.
Errors
Section titled “Errors”| Error | Meaning |
|---|---|
SupplyNotAllowed(address market, address supplier) | the market’s supply allowlist is enabled and the account being credited is not on it |
LiquidationNotAllowed(address liquidator) | the liquidation allowlist is enabled and the account seizing is not on it |
InvalidLiquidationIncentive() | an incentive below 1e18 + protocolSeizeShareMantissa for a market, or below MIN_POOL_LIQUIDATION_INCENTIVE_MANTISSA for the pool-wide value |
CollateralCoversDebt(uint256 borrows, uint256 maxClearableDebt) | healAccount on an account whose collateral can clear the whole debt |
DebtExceedsClearableAmount(uint256 borrows, uint256 maxClearableDebt) | liquidateAccount on an account whose debt the collateral cannot clear |
NonzeroBorrowBalanceAfterLiquidation() | the orders passed to liquidateAccount did not cover the whole position |
UnsafeLiquidationParams(uint256 liquidationThresholdMantissa, uint256 liquidationIncentiveMantissa) | a market’s liquidation threshold times its liquidation incentive would reach 1, the point from which a liquidation stops reducing the account’s shortfall |
Replacing require strings
Section titled “Replacing require strings”The fork converts every revert string of its own to a custom error. The inherited MaxLoopsLimitHelper and OpenZeppelin Ownable / Ownable2Step strings are left as they are.
| Error | Replaces |
|---|---|
InvalidCloseFactor() | "Close factor greater than maximum close factor" / "Close factor smaller than minimum close factor" |
InvalidVToken() | "Comptroller: Invalid vToken" |
InvalidArrayLength() | "invalid input" / "invalid number of markets" |
RewardsDistributorAlreadyExists() | "already exists" |
MarketNotListed(address) | "cannot pause a market that is not listed" |
NonzeroBorrowBalanceAfterLiquidation() | "Nonzero borrow balance after liquidation" |
InvalidLiquidationIncentive() | "liquidation incentive should be greater than 1e18" |
Changed meaning
Section titled “Changed meaning”CollateralExceedsThreshold(uint256 expectedLessThanOrEqualTo, uint256 actual)keeps the sharedComptroller’s name, arguments and selector, and is thrown by both batch operations when the account’s collateral is aboveminLiquidatableCollateral. In the sharedComptrollerhealAccountalso uses it for the “collateral covers the debt” case; here that case isCollateralCoversDebt.InsufficientCollateraldoes not exist in this fork.liquidateAccountthrowsDebtExceedsClearableAmountinstead. Both of its arguments are debt values rather than collateral values, and reusing the old name would leave two incompatible versions sharing one selector.