Skip to content

Commit 5532f96

Browse files
razwwclaude
andcommitted
feat(market-factory): support bStock markets and wire the liquidation suite
Add a stockCollateral flag to createMarket / batchCreateMarkets that registers the collateral in StockOracleSwitch via setStock. The factory never touches the switch's global market-hours flag, so a new bStock is registered but stays gated by ops. An already-registered stock is skipped, since setStock reverts on a no-op and creating a market must not reopen a deliberately closed stock. Whitelist market tokens uniformly across all three liquidation contracts (Liquidator, BrokerLiquidator, LiquidationVault) for common, fixed-term and smart-collateral markets, and register smart providers on BrokerLiquidator too. A smart-collateral LP is the one exception: it is excluded from the vault whitelist and reflow-blacklisted on both liquidators instead, because the vault cannot sell it. This matches the live BSC posture, which was previously only half-reproduced by the factory. liquidationVault and stockOracleSwitch are storage variables with admin setters, following the rateCalculator / brokerLiquidator pattern, so the constructor immutables and the existing deploy scripts are unchanged and ETH can leave both unset. Deploying this requires granting MarketFactory the MANAGER role on LiquidationVault and StockOracleSwitch, then calling the two setters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9736182 commit 5532f96

6 files changed

Lines changed: 418 additions & 37 deletions

File tree

src/liquidator/IBrokerLiquidator.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ interface IBrokerLiquidator {
8080

8181
function reflowBlacklist(address token) external view returns (bool);
8282

83+
function batchSetSmartProviders(address[] calldata providers, bool status) external;
84+
85+
function smartProviders(address provider) external view returns (bool);
86+
8387
function marketIdToBroker(bytes32 id) external view returns (address);
8488

8589
function brokerToMarketId(address broker) external view returns (bytes32);

src/liquidator/ILiquidationVault.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ interface ILiquidationVault {
2525
/// @notice Whether an address is a registered liquidator (provideFund allow-list + collect* target).
2626
function liquidators(address liquidator) external view returns (bool);
2727

28+
/// @notice Whether a token is on the sell/collect allow-list (sell* legs + BOT collect*).
29+
function tokenWhitelist(address token) external view returns (bool);
30+
31+
/// @notice Add / remove a token from the sell/collect allow-list. Requires MANAGER on the vault.
32+
/// @dev Reverts (WhitelistSameStatus) when the token is already in the requested state.
33+
function setTokenWhitelist(address token, bool status) external;
34+
2835
event FundProvided(address indexed liquidator, address indexed token, uint256 amount);
2936
event FundCollected(address indexed liquidator, address indexed token, uint256 amount);
3037
event LiquidatorSet(address indexed liquidator, bool registered);

src/moolah/MarketFactory.sol

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { ISmartProvider } from "../provider/interfaces/IProvider.sol";
1414
import { IBroker } from "../broker/interfaces/IBroker.sol";
1515
import { IBrokerLiquidator } from "../liquidator/IBrokerLiquidator.sol";
1616
import { IRateCalculator } from "../broker/interfaces/IRateCalculator.sol";
17+
import { ILiquidationVault } from "../liquidator/ILiquidationVault.sol";
18+
import { IStockOracleSwitch } from "../oracle/interfaces/IStockOracleSwitch.sol";
1719

1820
contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, PausableUpgradeable {
1921
using MarketParamsLib for MarketParams;
@@ -40,6 +42,12 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
4042
address public immutable slisBNBProvider; // on ETH: not used (address(0))
4143
IRateCalculator public rateCalculator;
4244
IBrokerLiquidator public brokerLiquidator;
45+
/// @dev Shared liquidation fund pool. Tokens of every created market are added to its sell/collect
46+
/// allow-list. address(0) disables the wiring (chains where the vault is not deployed yet).
47+
ILiquidationVault public liquidationVault;
48+
/// @dev Market-hours switch for tokenized stocks. Required only to create a bStock market
49+
/// (`stockCollateral = true`); address(0) disables the wiring.
50+
IStockOracleSwitch public stockOracleSwitch;
4351

4452
bytes32 public constant OPERATOR = keccak256("OPERATOR");
4553
bytes32 public constant PAUSER = keccak256("PAUSER");
@@ -48,6 +56,8 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
4856
event CommonMarketDeployed(MarketParams marketParams, Id marketId);
4957
event RateCalculatorUpdated(address indexed oldAddress, address indexed newAddress);
5058
event BrokerLiquidatorUpdated(address indexed oldAddress, address indexed newAddress);
59+
event LiquidationVaultUpdated(address indexed oldAddress, address indexed newAddress);
60+
event StockOracleSwitchUpdated(address indexed oldAddress, address indexed newAddress);
5161

5262
/**
5363
* @dev constructor to set immutable variables
@@ -118,20 +128,23 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
118128
* @param supplyWhitelist An array of address arrays for the supply whitelist of each market
119129
* @param liquidatorMarketWhitelist An array of booleans indicating whether to whitelist the market in the liquidator for each market
120130
* @param liquidatorSmartProviders An array of booleans indicating whether the market is a smart collateral market that requires special provider configuration for each market
131+
* @param stockCollaterals An array of booleans indicating whether the collateral token is a tokenized stock (bStock) that must be registered in the StockOracleSwitch for each market
121132
*/
122133
function batchCreateMarkets(
123134
MarketParams[] calldata params,
124135
address[][] calldata liquidatorWhitelist,
125136
address[][] calldata supplyWhitelist,
126137
bool[] calldata liquidatorMarketWhitelist,
127-
bool[] calldata liquidatorSmartProviders
138+
bool[] calldata liquidatorSmartProviders,
139+
bool[] calldata stockCollaterals
128140
) external onlyRole(OPERATOR) {
129141
require(params.length > 0, "empty market params");
130142
require(
131143
params.length == liquidatorWhitelist.length &&
132144
params.length == supplyWhitelist.length &&
133145
params.length == liquidatorMarketWhitelist.length &&
134-
params.length == liquidatorSmartProviders.length,
146+
params.length == liquidatorSmartProviders.length &&
147+
params.length == stockCollaterals.length,
135148
"array length mismatch"
136149
);
137150

@@ -141,7 +154,8 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
141154
liquidatorWhitelist[i],
142155
supplyWhitelist[i],
143156
liquidatorMarketWhitelist[i],
144-
liquidatorSmartProviders[i]
157+
liquidatorSmartProviders[i],
158+
stockCollaterals[i]
145159
);
146160
}
147161
}
@@ -153,15 +167,24 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
153167
* @param supplyWhitelist An array of addresses for the supply whitelist of the market
154168
* @param liquidatorMarketWhitelist A boolean indicating whether to whitelist the market in the liquidator
155169
* @param liquidatorSmartProvider A boolean indicating whether the market is a smart collateral market that requires special provider configuration
170+
* @param stockCollateral A boolean indicating whether the collateral token is a tokenized stock (bStock) that must be registered in the StockOracleSwitch
156171
*/
157172
function createMarket(
158173
MarketParams calldata param,
159174
address[] calldata liquidatorWhitelist,
160175
address[] calldata supplyWhitelist,
161176
bool liquidatorMarketWhitelist,
162-
bool liquidatorSmartProvider
177+
bool liquidatorSmartProvider,
178+
bool stockCollateral
163179
) external onlyRole(OPERATOR) {
164-
_createMarket(param, liquidatorWhitelist, supplyWhitelist, liquidatorMarketWhitelist, liquidatorSmartProvider);
180+
_createMarket(
181+
param,
182+
liquidatorWhitelist,
183+
supplyWhitelist,
184+
liquidatorMarketWhitelist,
185+
liquidatorSmartProvider,
186+
stockCollateral
187+
);
165188
}
166189

167190
/**
@@ -193,7 +216,8 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
193216
address[] memory liquidatorWhitelist,
194217
address[] memory supplyWhitelist,
195218
bool liquidatorMarketWhitelist,
196-
bool liquidatorSmartProvider
219+
bool liquidatorSmartProvider,
220+
bool stockCollateral
197221
) private whenNotPaused {
198222
Id id = param.id();
199223
// moolah create market
@@ -210,13 +234,12 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
210234
if (liquidatorMarketWhitelist) {
211235
liquidator.setMarketWhitelist(Id.unwrap(id), true);
212236
}
213-
// liquidator set token whitelist
214-
if (!liquidator.tokenWhitelist(param.loanToken)) {
215-
liquidator.setTokenWhitelist(param.loanToken, true);
216-
}
217-
if (!liquidator.tokenWhitelist(param.collateralToken)) {
218-
liquidator.setTokenWhitelist(param.collateralToken, true);
219-
}
237+
// token whitelist across the liquidation suite (Liquidator / BrokerLiquidator / LiquidationVault).
238+
// A smart-collateral LP is excluded from the vault only: the vault cannot sell it, so it is
239+
// reflow-blacklisted in _configSmartProvider and only its underlying pair legs are vault-whitelisted.
240+
// Whitelisting the LP there would also open the BOT collect* path for it.
241+
_whitelistToken(param.loanToken, true);
242+
_whitelistToken(param.collateralToken, !liquidatorSmartProvider);
220243
// revenue distributor set token whitelist
221244
if (address(revenueDistributor) != address(0) && !revenueDistributor.tokenWhitelist(param.loanToken)) {
222245
address[] memory tokens = new address[](1);
@@ -251,6 +274,15 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
251274
_configSmartProvider(id, param.oracle, param.collateralToken);
252275
}
253276

277+
// if collateral is a tokenized stock, register it in the market-hours switch. setStock also opens
278+
// the stock; it stays gated by the switch's global market-hours flag, which MANAGER owns.
279+
if (stockCollateral) {
280+
require(address(stockOracleSwitch) != address(0), "StockOracleSwitch not set");
281+
if (!stockOracleSwitch.registered(param.collateralToken)) {
282+
stockOracleSwitch.setStock(param.collateralToken, true);
283+
}
284+
}
285+
254286
emit CommonMarketDeployed(param, id);
255287
}
256288

@@ -296,13 +328,9 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
296328
// rate calculator register broker
297329
rateCalculator.registerBroker(param.broker, param.ratePerSecond, param.maxRatePerSecond);
298330

299-
// broker liquidator set token whitelist
300-
if (!brokerLiquidator.tokenWhitelist(param.loanToken)) {
301-
brokerLiquidator.setTokenWhitelist(param.loanToken, true);
302-
}
303-
if (!brokerLiquidator.tokenWhitelist(param.collateralToken)) {
304-
brokerLiquidator.setTokenWhitelist(param.collateralToken, true);
305-
}
331+
// token whitelist across the liquidation suite (Liquidator / BrokerLiquidator / LiquidationVault)
332+
_whitelistToken(param.loanToken, true);
333+
_whitelistToken(param.collateralToken, true);
306334

307335
// broker liquidator set market whitelist
308336
brokerLiquidator.setMarketToBroker(Id.unwrap(id), param.broker, true);
@@ -327,20 +355,42 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
327355
if (!publicLiquidator.smartProviders(provider)) {
328356
publicLiquidator.batchSetSmartProviders(smartProviders, true);
329357
}
330-
// set token whitelist for liquidator if not set
358+
if (address(brokerLiquidator) != address(0) && !brokerLiquidator.smartProviders(provider)) {
359+
brokerLiquidator.batchSetSmartProviders(smartProviders, true);
360+
}
361+
// the underlying pair legs are what actually gets sold after a smart-collateral liquidation, so
362+
// they are whitelisted on the whole suite. A native leg surfaces here as BNB_ADDRESS and is
363+
// whitelisted like any other token, which is what collectETH / sellBNB require.
331364
address token0 = ISmartProvider(provider).token(0);
332365
address token1 = ISmartProvider(provider).token(1);
333-
if (!liquidator.tokenWhitelist(token0)) {
334-
liquidator.setTokenWhitelist(token0, true);
335-
}
336-
if (!liquidator.tokenWhitelist(token1)) {
337-
liquidator.setTokenWhitelist(token1, true);
338-
}
339-
// blacklist the LP collateral from reflow: a wrong-entry plain liquidate must not push un-sellable
340-
// LP into the vault. It stays in the liquidator, recoverable via redeemSmartCollateral.
366+
_whitelistToken(token0, true);
367+
_whitelistToken(token1, true);
368+
// blacklist the LP collateral from reflow on every liquidator that reflows into the vault: a
369+
// wrong-entry plain liquidate must not push un-sellable LP into the vault. It stays in the
370+
// liquidator, recoverable via redeemSmartCollateral. PublicLiquidator has no reflow path.
341371
if (!liquidator.reflowBlacklist(collateral)) {
342372
liquidator.setReflowBlacklist(collateral, true);
343373
}
374+
if (address(brokerLiquidator) != address(0) && !brokerLiquidator.reflowBlacklist(collateral)) {
375+
brokerLiquidator.setReflowBlacklist(collateral, true);
376+
}
377+
}
378+
379+
/// @dev Whitelist `token` on every contract that can hold or sell it during a liquidation: the
380+
/// Liquidator, the BrokerLiquidator and — when `includeVault` — the LiquidationVault. The
381+
/// latter two are skipped while unwired (address(0)). Every branch reads the current status
382+
/// first because these setters reject a no-op status change.
383+
/// @param includeVault false only for a smart-collateral LP, which the vault must never hold.
384+
function _whitelistToken(address token, bool includeVault) private {
385+
if (!liquidator.tokenWhitelist(token)) {
386+
liquidator.setTokenWhitelist(token, true);
387+
}
388+
if (address(brokerLiquidator) != address(0) && !brokerLiquidator.tokenWhitelist(token)) {
389+
brokerLiquidator.setTokenWhitelist(token, true);
390+
}
391+
if (includeVault && address(liquidationVault) != address(0) && !liquidationVault.tokenWhitelist(token)) {
392+
liquidationVault.setTokenWhitelist(token, true);
393+
}
344394
}
345395

346396
/**
@@ -363,6 +413,26 @@ contract MarketFactory is UUPSUpgradeable, AccessControlEnumerableUpgradeable, P
363413
brokerLiquidator = IBrokerLiquidator(_brokerLiquidator);
364414
}
365415

416+
/**
417+
* @dev Set the liquidation vault address
418+
* @param _liquidationVault The address of the liquidation vault contract
419+
*/
420+
function setLiquidationVault(address _liquidationVault) external onlyRole(DEFAULT_ADMIN_ROLE) {
421+
require(_liquidationVault != address(0), "ZeroAddress");
422+
emit LiquidationVaultUpdated(address(liquidationVault), _liquidationVault);
423+
liquidationVault = ILiquidationVault(_liquidationVault);
424+
}
425+
426+
/**
427+
* @dev Set the stock oracle switch address
428+
* @param _stockOracleSwitch The address of the stock oracle switch contract
429+
*/
430+
function setStockOracleSwitch(address _stockOracleSwitch) external onlyRole(DEFAULT_ADMIN_ROLE) {
431+
require(_stockOracleSwitch != address(0), "ZeroAddress");
432+
emit StockOracleSwitchUpdated(address(stockOracleSwitch), _stockOracleSwitch);
433+
stockOracleSwitch = IStockOracleSwitch(_stockOracleSwitch);
434+
}
435+
366436
/**
367437
* @dev pause contract
368438
*/

src/oracle/StockOracle.sol

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgr
55
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
66

77
import { IOracle, TokenConfig } from "../moolah/interfaces/IOracle.sol";
8-
9-
interface IStockOracleSwitch {
10-
function isEnabled(address token) external view returns (bool);
11-
}
8+
import { IStockOracleSwitch } from "./interfaces/IStockOracleSwitch.sol";
129

1310
/// @title StockOracle
1411
/// @notice Moolah {IOracle} for tokenized-stock (bStock) markets.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.34;
3+
4+
/// @title IStockOracleSwitch
5+
/// @notice View + config surface of {StockOracleSwitch} used by its consumers ({StockOracle} for
6+
/// market-hours gating, {MarketFactory} for registering a bStock collateral at market creation).
7+
interface IStockOracleSwitch {
8+
/// @notice Whether `token` is currently enabled (tradable). Unregistered tokens are passthrough (always true).
9+
function isEnabled(address token) external view returns (bool);
10+
11+
/// @notice Whether `token` is a managed stock, i.e. subject to market-hours gating.
12+
function registered(address token) external view returns (bool);
13+
14+
/// @notice Register / un-register a managed stock. Requires MANAGER on the switch.
15+
/// @dev Reverts (AlreadySet) when the token is already in the requested state.
16+
function setStock(address token, bool isStock) external;
17+
}

0 commit comments

Comments
 (0)