Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7d940c4
feat: V3Provider
raz-w-20230331 Mar 23, 2026
532a7b3
feat: add maxTickDeviation
raz-w-20230331 Mar 27, 2026
4b94d4a
feat: fork V3 contracts
raz-w-20230331 Mar 29, 2026
e1759f8
feat: make V3 UUPS upgradable
raz-w-20230331 Mar 29, 2026
de1ec38
fix: update SPDX license headers and consolidate license files for V3…
raz-w-20230331 Apr 2, 2026
36962b8
refactor: replace forked dex/v3 with lista-v3 submodule dependency
razww Jun 8, 2026
b146ad1
refactor: source V3 math libs from audited lista-dao-contracts submodule
razww Jun 8, 2026
9da502d
Merge origin/master into feature/v3-lp
razww Jun 8, 2026
a51586f
feat(provider): slisBNB/BNB v3 LP provider — ERC4626 shell + exchange…
razww Jun 9, 2026
f1ecce9
refactor(provider): split slisBNB/BNB V3 LP into vault + DEX adapter …
razww Jun 9, 2026
02ad2f2
chore(provider): prune stale v3 libs from foundry.lock + prettier for…
razww Jun 10, 2026
9c43f6c
refactor(provider): use AccessControlEnumerableUpgradeable for V3 con…
razww Jun 10, 2026
17d3d05
refactor(liquidator): use AccessControlEnumerableUpgradeable for V3Li…
razww Jun 10, 2026
389ac41
fix(provider): refund after mint+supply to close deposit reentrancy
qingyang-lista Jun 10, 2026
b48933f
Merge pull request #191 from lista-dao/fix/v3provider-deposit-refund-…
razww Jun 10, 2026
8e41af1
feat(provider): V3 LP collateral provider — generalize + Ethereum wst…
razww Jun 25, 2026
48dfeaa
feat(provider): bound slisBNB instantWithdraw slippage in rebalance c…
razww Jun 26, 2026
64e46de
Merge pull request #199 from lista-dao/feature/eth-v3-lp
razww Jun 26, 2026
9ba9766
refactor(provider): unify slisBNB rebalance with wstETH/wbETH (DEX-ag…
razww Jun 26, 2026
1105440
fix(provider): native-in/out swap settlement + non-18-decimal support
razww Jun 26, 2026
0f5c382
Merge pull request #201 from lista-dao/feature/eth-v3-lp
qingyang-lista Jun 26, 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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
[submodule "lib/murky"]
path = lib/murky
url = https://github.com/dmfxyz/murky
[submodule "lib/lista-v3"]
path = lib/lista-v3
url = https://github.com/lista-dao/lista-v3
[submodule "lib/lista-dao-contracts.git"]
path = lib/lista-dao-contracts.git
url = https://github.com/lista-dao/lista-dao-contracts.git
Expand Down
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
src = "src"
out = "out"
libs = ["lib"]
remappings = [
"@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/",
]
solc = "0.8.34"
optimizer = true
optimizer_runs = 20
Expand Down
1 change: 1 addition & 0 deletions lib/lista-v3
Submodule lista-v3 added at 54cc4b
4 changes: 3 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ forge-std=lib/forge-std/src
timelock=src/timelock
revenue=src/revenue
murky=lib/murky
lista-v3/=lib/lista-v3/src/
lista-dao-contracts/=lib/lista-dao-contracts.git/contracts/
@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol=lib/lista-dao-contracts.git/node_modules/@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol
@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol=lib/lista-dao-contracts.git/node_modules/@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol
@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol=lib/lista-dao-contracts.git/node_modules/@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol

lib/lista-dao-contracts.git/:@openzeppelin/contracts-upgradeable/=lib/lista-dao-contracts.git/node_modules/@openzeppelin/contracts-upgradeable/
lib/lista-dao-contracts.git/:@openzeppelin/contracts-upgradeable/=lib/lista-dao-contracts.git/node_modules/@openzeppelin/contracts-upgradeable/
503 changes: 503 additions & 0 deletions src/liquidator/V3Liquidator.sol

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/provider/SmartProvider.sol
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ contract SmartProvider is

/// @dev Sets the slisBNBxMinter address.
function setSlisBNBxMinter(address _slisBNBxMinter) external onlyRole(MANAGER) {
require(_slisBNBxMinter != address(0), "zero address provided");
require(_slisBNBxMinter != slisBNBxMinter, "same minter");
slisBNBxMinter = _slisBNBxMinter;

emit SlisBNBxMinterChanged(_slisBNBxMinter);
Expand Down
95 changes: 95 additions & 0 deletions src/provider/interfaces/INonfungiblePositionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.7.5;
pragma abicoder v2;

/// @title Minimal Lista V3 / Uniswap V3 NonfungiblePositionManager interface
/// @notice Self-contained subset consumed by {V3Provider}. The full interface in the
/// lista-v3 dependency (lib/lista-v3) is Solidity 0.7.6 and inherits OpenZeppelin
/// ERC721 upgradeable interfaces whose paths/names differ from this repo's OZ v5.2,
/// so it cannot be imported into a 0.8.34 contract. Only the methods V3Provider
/// actually calls are declared here.
interface INonfungiblePositionManager {
/// @notice The address of the factory that created the position manager's pools.
function factory() external view returns (address);

/// @notice Returns the position information associated with a given token ID.
function positions(
uint256 tokenId
)
external
view
returns (
uint96 nonce,
address operator,
address token0,
address token1,
uint24 fee,
int24 tickLower,
int24 tickUpper,
uint128 liquidity,
uint256 feeGrowthInside0LastX128,
uint256 feeGrowthInside1LastX128,
uint128 tokensOwed0,
uint128 tokensOwed1
);

struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}

/// @notice Creates a new position wrapped in a NFT.
function mint(
MintParams calldata params
) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);

struct IncreaseLiquidityParams {
uint256 tokenId;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}

/// @notice Increases the amount of liquidity in a position, with tokens paid by `msg.sender`.
function increaseLiquidity(
IncreaseLiquidityParams calldata params
) external payable returns (uint128 liquidity, uint256 amount0, uint256 amount1);

struct DecreaseLiquidityParams {
uint256 tokenId;
uint128 liquidity;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}

/// @notice Decreases the amount of liquidity in a position and accounts it to the position.
function decreaseLiquidity(
DecreaseLiquidityParams calldata params
) external payable returns (uint256 amount0, uint256 amount1);

struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}

/// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient.
function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);

/// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity
/// and all tokens must be collected first.
function burn(uint256 tokenId) external payable;
}
12 changes: 12 additions & 0 deletions src/provider/interfaces/ISlisBNBV3DexAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;

import { IV3DexAdapter } from "./IV3DexAdapter.sol";

/**
* @title ISlisBNBV3DexAdapter
* @notice slisBNB/BNB adapter surface consumed by SlisBNBV3Provider. The rate-centered `rebalance`
* and rate-drift config are now generic (promoted to {IV3DexAdapter}); this alias is retained
* so existing imports / casts keep compiling unchanged.
*/
interface ISlisBNBV3DexAdapter is IV3DexAdapter {}
8 changes: 8 additions & 0 deletions src/provider/interfaces/IStakeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ interface IStakeManager {
function convertBnbToSnBnb(uint256 _amount) external view returns (uint256);

function convertSnBnbToBnb(uint256 _amountInSlisBnb) external view returns (uint256);

/// @notice Stake native BNB and mint slisBNB to the caller.
function deposit() external payable;

/// @notice Instantly redeem slisBNB for native BNB (no unbonding cooldown), minus an
/// instant-withdraw fee. The caller must approve `_amountInSlisBnb` to this contract.
/// @return bnbAmount The native BNB sent to the caller.
function instantWithdraw(uint256 _amountInSlisBnb) external returns (uint256 bnbAmount);
}
141 changes: 141 additions & 0 deletions src/provider/interfaces/IV3DexAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;

/**
* @title IV3DexAdapter
* @author Lista DAO
* @notice Seam between the vault (V3Provider) / oracle (SlisBNBV3ProviderOracle) and the DEX custodian
* (V3DexAdapter). The adapter is the SOLE holder of the V3 NFT, the idle inventory and all
* NPM/pool interaction. The vault and oracle never touch NPM/pool directly — they call the
* adapter's `onlyProvider` write functions and read its raw-NAV/composition views (staticcall).
*
* @dev Conventions:
* - Raw NAV: all views return amounts WITHOUT any oracle haircut. The vault uses them for
* share accounting; the oracle applies its own haircut on top for Moolah pricing.
* - Token custody: before `addLiquidity` the provider transfers the input tokens to the adapter;
* the adapter refunds unused tokens to `refundTo` and, on `removeLiquidity`, sends the
* withdrawn underlying directly to `receiver` (avoids a provider→user double hop).
* - "amounts" are token0/token1 raw units; sqrt prices are X96.
*/
interface IV3DexAdapter {
/* ───────────────────── pool / position accessors ────────────── */

function TOKEN0() external view returns (address);

function TOKEN1() external view returns (address);

function DECIMALS0() external view returns (uint8);

function DECIMALS1() external view returns (uint8);

function POOL() external view returns (address);

/// @notice Wrapped-native token of the chain (WBNB on BSC, WETH on Ethereum).
function WRAPPED_NATIVE() external view returns (address);

/// @notice The vault (V3Provider) authorized to drive this adapter.
function provider() external view returns (address);

function tokenId() external view returns (uint256);

function tickLower() external view returns (int24);

function tickUpper() external view returns (int24);

function idleToken0() external view returns (uint256);

function idleToken1() external view returns (uint256);

/* ───────────────── raw-NAV / composition views ──────────────── */

/// @notice token0/token1 represented by the whole position at `sqrtPriceX96`, INCLUDING uncollected
/// fees (tokensOwed) and idle inventory. Raw (no haircut). Single source of truth that both
/// the vault (share accounting) and oracle (Moolah pricing) read.
function positionAmountsAt(uint160 sqrtPriceX96) external view returns (uint256 total0, uint256 total1);

/// @notice token0/token1 for a given `liquidity` at `sqrtPriceX96` (position math only, no fees/idle).
/// Used by the vault's value-based share mint to value freshly added liquidity at the fair price.
function amountsForLiquidity(
uint128 liquidity,
uint160 sqrtPriceX96
) external view returns (uint256 amount0, uint256 amount1);

/// @notice Current liquidity of the managed position (0 if none).
function totalLiquidity() external view returns (uint128);

/// @notice Fair valuation price: exchange-rate-implied for slisBNB/WBNB, pool TWAP otherwise.
function fairSqrtPriceX96() external view returns (uint160);

/// @notice Current pool spot price (slot0).
function spotSqrtPriceX96() external view returns (uint160);

/// @notice Simulate adding `amount0Desired/amount1Desired` at the current spot price.
function previewAddLiquidity(
uint256 amount0Desired,
uint256 amount1Desired
) external view returns (uint128 liquidity, uint256 amount0, uint256 amount1);

/// @notice Simulate removing `shares/totalShares` of the position (liquidity + idle) at spot.
function previewRemoveLiquidity(
uint256 shares,
uint256 totalShares
) external view returns (uint256 amount0, uint256 amount1);

/* ─────────────────────── writes (onlyProvider) ──────────────── */

/// @notice Add liquidity from tokens already transferred to the adapter; mint a fresh NFT if none
/// exists, otherwise increase. Unused input is refunded to `refundTo`.
/// @return liquidityAdded Liquidity units added.
/// @return amount0Used token0 actually consumed by the pool.
/// @return amount1Used token1 actually consumed by the pool.
function addLiquidity(
uint256 amount0Desired,
uint256 amount1Desired,
uint256 amount0Min,
uint256 amount1Min,
address refundTo
) external returns (uint128 liquidityAdded, uint256 amount0Used, uint256 amount1Used);

/// @notice Remove the `shares/totalShares` pro-rata slice of liquidity AND idle inventory, sending
/// the underlying directly to `receiver` (WBNB unwrapped to native BNB). Used by the vault's
/// withdraw / redeemShares. No protocol value floor — the caller's minAmount0/1 is the guard
/// (keeps liquidation live; see finding C4).
function removeLiquidity(
uint256 shares,
uint256 totalShares,
uint256 minAmount0,
uint256 minAmount1,
address receiver
) external returns (uint256 amount0, uint256 amount1);

/// @notice Collect accrued fees and re-add them plus idle inventory as liquidity (compound).
function collectAndCompound() external;

/* ─────────────────────── rebalance / rate config ────────────────── */

/// @notice Exchange rate at the last successful center/init (rate-implied pairs; 0 for TWAP pairs).
function lastCenterRate() external view returns (uint256);

/// @notice Min relative exchange-rate drift before rebalance is allowed (BPS; 0 = off).
function centerRateThresholdBps() external view returns (uint256);

/// @notice Set the rate-drift threshold required for rebalance (onlyRole MANAGER).
function setCenterRateThresholdBps(uint256 centerRateThresholdBps) external;

/// @notice Whether `swapPair` is a whitelisted venue for the rebalance inventory-conversion swap.
function swapPairWhitelist(address swapPair) external view returns (bool);

/// @notice Whitelist (or remove) a swap venue for the rebalance inventory conversion (onlyRole
/// MANAGER). A venue may never be TOKEN0 / TOKEN1 / POOL / POSITION_MANAGER.
function setSwapPairWhitelist(address swapPair, bool status) external;

/// @notice Recenter the position to its range and convert inventory to the optimal ratio.
/// onlyProvider — the provider gates the caller with the BOT role.
function rebalance(
uint256 minAmount0,
uint256 minAmount1,
uint256 minLiquidity,
uint256 deadline,
bytes calldata swapData
) external;
}
16 changes: 16 additions & 0 deletions src/provider/interfaces/IV3PoolMinimal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;

/**
* @title IV3PoolMinimal
* @author Lista DAO
* @notice Minimal Uniswap/PancakeSwap V3 pool reader that decodes only the slot0 fields the adapter
* actually consumes (sqrtPriceX96, tick). The full slot0 tuple ends with a `feeProtocol` field
* whose width differs across forks — Uniswap V3 / lista-v3 pack it as uint8, PancakeSwap V3 as
* uint32. Decoding the whole tuple through a uint8-typed interface reverts against a Pancake
* pool (dirty high bits). Stopping the decode at `tick` makes the read width-agnostic, so the
* adapter works against any V3 flavor (and the integration tests can fork a live Pancake pool).
*/
interface IV3PoolMinimal {
function slot0() external view returns (uint160 sqrtPriceX96, int24 tick);
}
68 changes: 68 additions & 0 deletions src/provider/interfaces/IV3Provider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.34;

import { MarketParams, Id } from "moolah/interfaces/IMoolah.sol";
import { IProvider } from "./IProvider.sol";

/**
* @title IV3Provider
* @notice Vault surface for the V3 LP collateral provider. Position internals (tokenId, ticks, pool,
* TWAP) live on the DEX adapter (IV3DexAdapter); share pricing lives on the oracle
* (IV3ProviderOracle). The vault is no longer an IOracle.
*/
interface IV3Provider is IProvider {
function TOKEN0() external view returns (address);

function TOKEN1() external view returns (address);

/// @notice Wrapped-native token of the pool's chain (WBNB on BSC, WETH on Ethereum). On exit the
/// provider unwraps whichever leg equals this to the native coin, so consumers (e.g. the
/// liquidator) must treat that leg as native rather than ERC-20.
function WRAPPED_NATIVE() external view returns (address);

/// @notice The DEX adapter holding the V3 NFT / idle inventory.
function ADAPTER() external view returns (address);

/// @notice Total token0/token1 backing the vault at the current pool spot (display/bots).
function getTotalAmounts() external view returns (uint256 total0, uint256 total1);

/// @notice Deposit token0/token1 into the V3 position and supply resulting shares as Moolah
/// collateral on behalf of `onBehalf`.
function deposit(
MarketParams calldata marketParams,
uint256 amount0Desired,
uint256 amount1Desired,
uint256 amount0Min,
uint256 amount1Min,
address onBehalf
) external payable returns (uint256 shares, uint256 amount0Used, uint256 amount1Used);

/// @notice Withdraw shares from Moolah, remove liquidity, and return token0/token1 to `receiver`.
function withdraw(
MarketParams calldata marketParams,
uint256 shares,
uint256 minAmount0,
uint256 minAmount1,
address onBehalf,
address receiver
) external returns (uint256 amount0, uint256 amount1);

/// @notice Withdraw provider shares from Moolah collateral without redeeming the underlying position.
function withdrawShares(
MarketParams calldata marketParams,
uint256 shares,
address onBehalf,
address receiver
) external;

/// @notice Supply wallet-held provider shares as Moolah collateral.
function supplyShares(MarketParams calldata marketParams, uint256 shares, address onBehalf) external;

/// @notice Redeem shares already held by the caller (e.g. a liquidator) for the underlying token0/token1.
function redeemShares(
uint256 shares,
uint256 minAmount0,
uint256 minAmount1,
address receiver
) external returns (uint256 amount0, uint256 amount1);
}
Loading
Loading