Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
de23dbc
feat(spoke): fork Comptroller into SpokeComptroller
Debugger022 Aug 17, 2026
3dfa39e
refactor(spoke): remove Prime integration
Debugger022 Aug 17, 2026
6f8a7de
feat(spoke): add allowlists and per-market liquidation incentive
Debugger022 Aug 17, 2026
4e08dde
refactor(spoke): replace revert strings with custom errors
Debugger022 Aug 17, 2026
ffb5ca1
refactor(spoke): move events and errors to interface
Debugger022 Aug 17, 2026
a17ff13
feat(spoke): bound collateral pricing on the borrow capacity paths
Debugger022 Aug 17, 2026
0cfc21b
chore: ignore the scratch and editor directories
Debugger022 Aug 17, 2026
cfe7364
feat(deploy): add spoke pool deployment script
Debugger022 Aug 18, 2026
759505a
docs(spoke): correct spoke comptroller doc comments
Debugger022 Aug 18, 2026
333a548
fix(spoke): resolve liquidation incentive per market
Debugger022 Aug 19, 2026
f45d072
test(spoke): add comptroller unit tests
Debugger022 Aug 19, 2026
1d20e88
feat(spoke): expand the comptroller error and getter surface
Debugger022 Aug 20, 2026
a5f3fac
test(spoke): add hub funded pool fork suite
Debugger022 Aug 20, 2026
fd91634
fix(deploy): update address comparison to use checksummed format
Debugger022 Aug 20, 2026
b527b13
fix(spoke): check ACM before validating the pool liquidation incentive
Debugger022 Aug 21, 2026
358f34b
fix(spoke): raise the pool-wide liquidation incentive floor
Debugger022 Aug 21, 2026
cbf92ac
chore(spoke): relicense adapter files to BSD-3-Clause
Debugger022 Aug 25, 2026
03baa75
test(spoke): cover the adapter guards a live market cannot trip
Debugger022 Aug 25, 2026
2300cc5
chore(spoke): drop the redundant liquidation allowlist check
Debugger022 Sep 2, 2026
ce9aa07
refactor(spoke): clarify names of the comptroller internals
Debugger022 Sep 2, 2026
395e6a6
fix(spoke): refresh markets before the liquidateAccount snapshot
Debugger022 Sep 2, 2026
89cb3b7
feat(spoke): give the spoke pool its own pool registry
Debugger022 Sep 2, 2026
b261a7b
feat(lens): report spoke-only pool and market state
Debugger022 Sep 2, 2026
e67dd5c
feat(spoke): let an approved router enter markets for a supplier
Debugger022 Sep 4, 2026
3058dbf
feat(lens): add a dedicated lens for spoke pools
Debugger022 Sep 8, 2026
6989643
feat(spoke): add a deploy script for the spoke vToken beacon
Debugger022 Sep 8, 2026
dd114d2
feat(spoke): add a deploy script for spoke pool markets
Debugger022 Sep 8, 2026
82d5cec
fix(spoke): harden the deploy scripts for a live network
Debugger022 Sep 8, 2026
cab9306
feat: deploy the spoke pool stack on bsctestnet
Debugger022 Sep 8, 2026
7b02a60
feat: updating deployment files
Debugger022 Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ dist
# ignoring scenario dir because it contains deprecated saddle code and will be removed
scenario
typechain
tmp
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ deployments/localhost

# OSX
.DS_Store

tmp/*
.vscode
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ scenario
typechain
contracts/oracle
CHANGELOG.md
tmp
1,916 changes: 1,916 additions & 0 deletions contracts/Spoke/SpokeComptroller.sol
Comment thread
GitGuru7 marked this conversation as resolved.

Large diffs are not rendered by default.

336 changes: 336 additions & 0 deletions contracts/Spoke/SpokeComptrollerInterface.sol

Large diffs are not rendered by default.

179 changes: 179 additions & 0 deletions contracts/Spoke/SpokeComptrollerStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.25;

import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interfaces/OracleInterface.sol";
import { IDeviationBoundedOracle } from "@venusprotocol/oracle/contracts/interfaces/IDeviationBoundedOracle.sol";

import { VToken } from "../VToken.sol";
import { RewardsDistributor } from "../Rewards/RewardsDistributor.sol";
import { Action } from "../ComptrollerInterface.sol";

/**
* @title SpokeComptrollerStorage
* @author Venus
* @notice Storage layout for the `SpokeComptroller` contract.
* @dev Fork of `ComptrollerStorage` (`contracts/ComptrollerStorage.sol`), kept separate so the spoke layout and the
* `AccountLiquiditySnapshot` struct can change without touching the shared implementation. Re-synced by hand when
* `ComptrollerStorage` changes, like the implementation it accompanies.
*/
contract SpokeComptrollerStorage {
struct LiquidationOrder {
VToken vTokenCollateral;
VToken vTokenBorrowed;
uint256 repayAmount;
}

/// @dev `totalCollateral` and `maxClearableDebt` are only meaningful under
/// `WeightFunction.USE_LIQUIDATION_THRESHOLD`, which is the weighting every caller that reads them passes. Under
/// the collateral factor `totalCollateral` is derived from the deviation-bounded collateral price rather than spot,
/// and `maxClearableDebt` is not accumulated at all, so it stays at zero. A new caller on that weighting must not
/// start reading either one without deciding what it wants first: a zero `maxClearableDebt` puts `healAccount` on a
/// repayment percentage of zero, which forgives the whole position as bad debt.
struct AccountLiquiditySnapshot {
uint256 totalCollateral;
uint256 weightedCollateral;
uint256 borrows;
uint256 effects;
uint256 liquidity;
uint256 shortfall;
// The largest borrow value the account's collateral can clear without leaving bad debt behind, computed as
// the sum over the account's collateral markets of `collateralValue / liquidationIncentive`, each market at
// its own incentive. Used to route an under-threshold account between `liquidateAccount` and `healAccount`.
uint256 maxClearableDebt;
}

struct RewardSpeeds {
address rewardToken;
uint256 supplySpeed;
uint256 borrowSpeed;
}

struct Market {
// Whether or not this market is listed
bool isListed;
// Multiplier representing the most one can borrow against their collateral in this market.
// For instance, 0.9 to allow borrowing 90% of collateral value.
// Must be between 0 and 1, and stored as a mantissa.
uint256 collateralFactorMantissa;
// Multiplier representing the collateralization after which the borrow is eligible
// for liquidation. For instance, 0.8 liquidate when the borrow is 80% of collateral
// value. Must be between 0 and collateral factor, stored as a mantissa.
uint256 liquidationThresholdMantissa;
// Per-market mapping of "accounts in this asset"
mapping(address => bool) accountMembership;
}

/**
* @notice Oracle which gives the price of any given asset
*/
ResilientOracleInterface public oracle;

/**
* @notice Multiplier used to calculate the maximum repayAmount when liquidating a borrow
*/
uint256 public closeFactorMantissa;

/**
* @notice Multiplier representing the discount on collateral that a liquidator receives, applied to every market
* that has no discount of its own
* @dev Internal rather than public, because the `liquidationIncentiveMantissa()` getter has to resolve the
* caller's market before answering: `VToken` reads that getter on itself and needs the discount that prices its
* own collateral, not the pool-wide default. See `SpokeComptroller.liquidationIncentiveMantissa`.
*/
uint256 internal _poolLiquidationIncentiveMantissa;

/**
* @notice Per-account mapping of "assets you are in"
*/
mapping(address => VToken[]) public accountAssets;

/**
* @notice Official mapping of vTokens -> Market metadata
* @dev Used e.g. to determine if a market is supported
*/
mapping(address => Market) public markets;

/// @notice A list of all markets
VToken[] public allMarkets;

/// @notice Borrow caps enforced by borrowAllowed for each vToken address. Defaults to zero which restricts borrowing.
mapping(address => uint256) public borrowCaps;

/// @notice Minimal collateral required for regular (non-batch) liquidations
uint256 public minLiquidatableCollateral;

/// @notice Supply caps enforced by mintAllowed for each vToken address. Defaults to zero which corresponds to minting not allowed
mapping(address => uint256) public supplyCaps;

/// @notice True if a certain action is paused on a certain market
mapping(address => mapping(Action => bool)) internal _actionPaused;

// List of Reward Distributors added
RewardsDistributor[] internal rewardsDistributors;

// Used to check if rewards distributor is added
mapping(address => bool) internal rewardsDistributorExists;

/// @notice Flag indicating whether forced liquidation enabled for a market
mapping(address => bool) public isForcedLiquidationEnabled;

uint256 internal constant NO_ERROR = 0;

// closeFactorMantissa must be strictly greater than this value
uint256 internal constant MIN_CLOSE_FACTOR_MANTISSA = 0.05e18; // 0.05

// closeFactorMantissa must not exceed this value
uint256 internal constant MAX_CLOSE_FACTOR_MANTISSA = 0.9e18; // 0.9

// No collateralFactorMantissa may exceed this value
uint256 internal constant MAX_COLLATERAL_FACTOR_MANTISSA = 0.95e18; // 0.95

/// @notice Whether the delegate is allowed to borrow or redeem on behalf of the user
//mapping(address user => mapping (address delegate => bool approved)) public approvedDelegates;
mapping(address => mapping(address => bool)) public approvedDelegates;

/// @notice Whether a market accepts supply only from the accounts on its supply allowlist. Keyed by market, and
/// disabled by default, so a newly listed market accepts supply from anyone.
mapping(address => bool) public isSupplyAllowlistEnabled;

/// @notice The accounts a market accepts supply from while its supply allowlist is enabled. Keyed by market,
/// then by account.
mapping(address => mapping(address => bool)) public isAllowedSupplier;

/// @notice Whether seizing collateral in this pool is restricted to the accounts on the liquidation allowlist.
/// Disabled by default.
/// @dev Pool-wide rather than per market, because `healAccount` seizes across every market the borrower is in and
/// so cannot attribute a seizure to a single one of them.
bool public isLiquidationAllowlistEnabled;

/// @notice The accounts allowed to seize collateral in this pool while the liquidation allowlist is enabled
mapping(address => bool) public isAllowedLiquidator;

/// @notice Per-market discount a liquidator receives on the collateral it seizes, scaled by 1e18. Keyed by the
/// collateral market, since that is what the discount prices.
/// @dev Zero means no market value has been set, in which case `_poolLiquidationIncentiveMantissa` applies. That
/// pool-wide value is always at least 1e18 for a listed market: `PoolRegistry.addMarket` refuses to add one to an
/// unregistered pool, and registering a pool goes through `setLiquidationIncentive`, which enforces the floor.
mapping(address => uint256) public liquidationIncentives;

/// @notice Oracle that bounds an asset's price against a recent window, so a deviating print cannot inflate
/// borrowing capacity. Read only where the collateral factor weights the position; the liquidation-threshold
/// paths stay on `oracle`, because they route liquidations.
IDeviationBoundedOracle public deviationBoundedOracle;

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
* The size is derived from the 47 slots `ComptrollerStorage` reserves: plus one for the Prime token slot, which
* is unused here and is returned to the gap rather than left as a hole, minus the six slots the allowlists, the
* per-market liquidation incentives and the deviation-bounded oracle above take. This contract therefore occupies
* the same number of slots as the one it was forked from.
*
* That is not layout compatibility. Reclaiming the Prime slot moved every variable declared after it up by one,
* so `approvedDelegates` here sits in the slot `ComptrollerStorage` gives to `prime`, and the two layouts cannot
* be swapped under a live pool in either direction. They do not have to be: a spoke pool upgrades through its own
* beacon. `tests/hardhat/Spoke/storageLayout.ts` pins the slot list, the divergence and this gap size.
*/
uint256[42] private __gap;
}
Loading
Loading