Skip to content
Open
Show file tree
Hide file tree
Changes from 23 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
78 changes: 76 additions & 2 deletions contracts/Lens/PoolLens.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ExponentialNoError } from "../ExponentialNoError.sol";
import { VToken } from "../VToken.sol";
import { Action, ComptrollerInterface, ComptrollerViewInterface } from "../ComptrollerInterface.sol";
import { PoolRegistryInterface } from "../Pool/PoolRegistryInterface.sol";
import { SpokeComptrollerViewInterface } from "../Spoke/SpokeComptrollerInterface.sol";
import { PoolRegistry } from "../Pool/PoolRegistry.sol";
import { RewardsDistributor } from "../Rewards/RewardsDistributor.sol";
import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol";
Expand Down Expand Up @@ -44,6 +45,10 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 {
uint256 liquidationIncentive;
uint256 minLiquidatableCollateral;
VTokenMetadata[] vTokens;
/// @notice Oracle a spoke pool bounds collateral prices with. Zero for a pool that has none
address deviationBoundedOracle;
/// @notice True while only allowlisted accounts may liquidate in this pool. False for a pool with no allowlist
bool liquidationAllowlistEnabled;
}

/**
Expand All @@ -67,6 +72,11 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 {
uint256 vTokenDecimals;
uint256 underlyingDecimals;
uint256 pausedActions;
/// @notice Discount a liquidator receives on this market's collateral: the market's own value if it has one,
/// otherwise the pool-wide value
uint256 liquidationIncentiveMantissa;
/// @notice True while only allowlisted accounts may supply to this market. False for a market with no allowlist
bool supplyAllowlistEnabled;
}

/**
Expand Down Expand Up @@ -368,8 +378,18 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 {
vTokens: vTokenMetadataItems,
priceOracle: address(comptrollerViewInstance.oracle()),
closeFactor: comptrollerViewInstance.closeFactorMantissa(),
// The pool-wide value on either kind of pool: a spoke resolves this getter against `msg.sender`, and a
// lens is not one of its markets. Per market, read `VTokenMetadata.liquidationIncentiveMantissa`.
liquidationIncentive: comptrollerViewInstance.liquidationIncentiveMantissa(),
minLiquidatableCollateral: comptrollerViewInstance.minLiquidatableCollateral()
minLiquidatableCollateral: comptrollerViewInstance.minLiquidatableCollateral(),
deviationBoundedOracle: _probeAddress(

@fred-venus fred-venus Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to decide whether its isolated pools or spoke pools ?

i am thinking why dont we just redeploy a new PoolLens with clean implementation, right now we already have new poolRegistry, its natural to think we should have a poolLens as well.

So basically 2 sets of contracts in parallel so they dont confuse ppl

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also for VTokenBeacon lets deploy a new one, so that in the future if ever we do the change its not gonna impact existing isolated pools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lens isn't bound to a registry, it takes poolRegistryAddress as a call argument, and it's a plain non-proxy contract we version by redeploying (R1/R2 archived, this PR archives R3), so the separation already exists at the address level. But fair point on the confusion, I'll add a separate SpokePoolLens and leave PoolLens for isolated. That also drops the staticcall probing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, will deploy a separate VToken beacon for the spoke pool.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

venusPool.comptroller,
abi.encodeCall(SpokeComptrollerViewInterface.deviationBoundedOracle, ())
),
liquidationAllowlistEnabled: _probeBool(
venusPool.comptroller,
abi.encodeCall(SpokeComptrollerViewInterface.isLiquidationAllowlistEnabled, ())
)
});

return poolData;
Expand Down Expand Up @@ -419,7 +439,12 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 {
underlyingAssetAddress: underlyingAssetAddress,
vTokenDecimals: vToken.decimals(),
underlyingDecimals: underlyingDecimals,
pausedActions: pausedActions
pausedActions: pausedActions,
liquidationIncentiveMantissa: _marketLiquidationIncentive(comptrollerAddress, address(vToken)),
supplyAllowlistEnabled: _probeBool(
comptrollerAddress,
abi.encodeCall(SpokeComptrollerViewInterface.isSupplyAllowlistEnabled, (address(vToken)))
)
});
}

Expand Down Expand Up @@ -616,4 +641,53 @@ contract PoolLens is ExponentialNoError, TimeManagerV8 {
uint256 supplierDelta = mul_(supplierTokens, deltaIndex);
return supplierDelta;
}

/**
* @dev The discount a liquidator receives on this market's collateral. A spoke pool answers per market through
* `effectiveLiquidationIncentive`; any other comptroller has one incentive for the whole pool, and that is the
* value applying to each of its markets.
* @param comptroller The market's comptroller
* @param vToken The market to read the discount of
* @return The discount that applies to this market, scaled by 1e18
*/
function _marketLiquidationIncentive(address comptroller, address vToken) private view returns (uint256) {
(bool success, bytes memory data) = comptroller.staticcall(
abi.encodeCall(SpokeComptrollerViewInterface.effectiveLiquidationIncentive, (vToken))
);
if (success && data.length == 32) {
return abi.decode(data, (uint256));
}
return ComptrollerViewInterface(comptroller).liquidationIncentiveMantissa();
}

/**
* @dev Reads an address a comptroller only answers if it is a spoke pool. `staticcall` rather than a typed call,
* so a comptroller without the function reverts here instead of reverting the whole read, and that is taken to
* mean "this pool has none". Encode with `abi.encodeCall` so the selector stays checked against the interface.
* @param comptroller The comptroller to ask
* @param call The encoded call
* @return The address the comptroller answered, or zero if it does not answer this call
*/
function _probeAddress(address comptroller, bytes memory call) private view returns (address) {
(bool success, bytes memory data) = comptroller.staticcall(call);
if (!success || data.length != 32) {
return address(0);
}
return abi.decode(data, (address));
}

/**
* @dev Reads a flag a comptroller only answers if it is a spoke pool. See `_probeAddress`. A comptroller that does
* not answer has no such restriction, which is what false reports.
* @param comptroller The comptroller to ask
* @param call The encoded call
* @return True if the comptroller answered true, false if it answered false or does not answer this call
*/
function _probeBool(address comptroller, bytes memory call) private view returns (bool) {
(bool success, bytes memory data) = comptroller.staticcall(call);
if (!success || data.length != 32) {
return false;
}
return abi.decode(data, (bool));
}
}
Loading
Loading