mirror of
https://github.com/luxfi/liquid.git
synced 2026-10-30 16:21:07 +00:00
The engine could mint synthetic that could never be burned, valued collateral from an unbounded admin-swappable oracle, priced unrelated assets against each other as if they were the same, could not liquidate anything, and compiled 1.3x past the EIP-170 limit. The invariant suite passed over all of it. Engine: - Debt tokens are reached through ISynthetic (mint / burn / burnFrom), matching what the token family now exposes. - A market is bound to its adapter at initialize and on every swap: the adapter must name this market's own yield and underlying token. Price is admitted through a per-block deviation cap, anchored on the last accepted value, so a swap or a compromised feed cannot teleport collateral value, and a same-block move cannot move borrowing power at all. A dead adapter leaves the last good price standing rather than marking every position worthless. - The three collateralization levels are checked as one ordering wherever any of them is set, so the global floor can no longer sit above the mint bar and route every liquidation through full seizure. The fee-vault bonus fails soft when no vault is set: liquidation clears the position either way. - _forceRepay derives debt relief from the clamped credit, so the transmuter can no longer be handed less than the debt retired. - claimRedemption returns a shortfall instead of burning through it, and the bad-debt ratio is taken in debt units, rounded up. - Deposits and new debt are refused while the protocol is short, and the state is observable through inBadDebt(). - getMaxBorrowable saturates at zero instead of underflowing; Liquidated is emitted from one place, so batch and single liquidations report alike. Markets are like-kind by deployment: bridged ETH backs LETH, bridged BTC backs LBTC, WLUX backs LLUX. The engine converts underlying to debt with a decimals scalar and holds no cross-asset price source, so the mainnet script's LETH debt against WLUX collateral was a drain bounded only by the deposit cap. DeployL2 was a byte-identical copy of DeployBrandL1 carrying the same market; DeployLux could not execute at all. Both are gone. Yul optimizer on: Liquid goes from 32,268 bytes to 22,589, under EIP-170 with room to spare. Tests read the block through the cheatcode rather than the opcode -- block.number is constant within a real transaction, so the optimizer is entitled to fold it, and vm.roll is not. Testing. Every audit finding is a regression test in src/test/Audit.t.sol, kept in the shape of the exploit it began as, including an 8-decimal BTC market for the 10^10 conversion factor. The invariant suite gains price movement, strategy loss and both liquidation paths -- and, more to the point, a borrow path: _randomMinter discarded the borrow ceiling it queried, so no account in that suite had ever carried debt, liquidate reverted on all 4096 calls of every campaign, and the invariants described a protocol nobody had borrowed from. Stakers now stake synthetic they borrowed rather than minting it, which is what the transmuter's claim on the protocol rests on. Co-authored-by: Hanzo Dev <[email protected]>
4.4 KiB
4.4 KiB
Lux Liquid
Overview
Self-repaying lending protocol on Lux. Users deposit yield-bearing collateral, borrow synthetic debt tokens (e.g. LETH), and debt is automatically repaid via yield. Includes a Transmuter for converting synthetics back to yield tokens over time.
Tech Stack
- Language: Solidity 0.8.28
- Framework: Foundry (forge, cast)
- EVM: Cancun
- Dependencies: OpenZeppelin 5.x, forge-std, vault-v2, permit2, solmate
Build & Run
forge build # Compile all contracts
forge test -v # Run tests
forge test --match-path "src/test/Liquid.t.sol" -v # Run specific test
Key Contracts
Liquid.sol-- Core lending engine (Initializable proxy pattern, empty constructor + initialize)LiquidPosition.sol-- ERC721 NFT representing user positions (constructor takes liquid address)LiquidTransmuter.sol-- Converts synthetic tokens to yield tokens over time (Fenwick tree / StakingGraph)LiquidETHVault.sol-- ETH/WETH fee vault (constructor: weth, liquid, owner)LiquidTokenVault.sol-- ERC20 fee vaultLiquidCurator.sol-- Strategy cap management (constructor: admin, operator)LiquidAllocator.sol-- Capital allocation to strategies (constructor: vault-v2, admin, operator)LiquidStrategyClassifier.sol-- Risk class management (constructor: admin)LiquidStrategy.sol-- Base strategy adapter for VaultV2 flowsLiquidGauge.sol-- Governance gauge for strategy weight votingLiquidGate.sol/LiquidCompliance.sol-- Auth + KYC gating for redemptions
Constructor Signatures (critical for deployment)
Liquid()-- empty, then callinitialize(LiquidInitializationParams)LiquidPosition(address liquid_)LiquidTransmuter(ILiquidTransmuter.TransmuterInitializationParams memory params)LiquidETHVault(address _weth, address _liquid, address _owner)LiquidCurator(address _admin, address _operator)via PermissionedProxyLiquidAllocator(address _vault, address _admin, address _operator)via PermissionedProxyLiquidStrategyClassifier(address _admin)
Deployment Order
- Deploy Liquid (empty constructor)
- Deploy LiquidPosition (needs liquid address)
- Deploy LiquidTransmuter (needs debt token address)
- Deploy LiquidETHVault (needs WLUX + liquid address)
- Call liquid.initialize(params) with transmuter address
- Call liquid.setLiquidPositionNFT(position)
- Call transmuter.setLiquid(liquid)
- Whitelist liquid as minter on debt token (LETH)
Canonical Lux Addresses
- LETH:
0x60E0a8167FC13dE89348978860466C9ceC24B9ba - WLUX:
0x4888E4a2Ee0F03051c72D2BD3ACf755eD3498B3E - LBTC:
0x1E48D32a4F5e9f08DB9aE4959163300FaF8A6C8e
Test Failures (pre-existing)
- Strategy fork tests (SfrxETH, MorphoYearnOGWETH, PeapodsETH, TokeAutoETH) -- need MAINNET_RPC_URL
- IntegrationTest -- needs Ethereum mainnet fork
- LiquidGauge (2 tests) -- logic bugs in cap application and vote aggregation
- MockERC20 IERC20 compliance -- mock doesn't implement full interface
- LiquidETHVault testDepositWETH -- mock WETH incomplete
Scripts
script/DeployBrandL1.s.sol-- Any brand L1, one like-kind market per runscript/DeployMainnet.s.sol-- Lux mainnet ETH market, canonical LETH pinnedscript/DeployLocal.s.sol-- Full local stack (Anvil/luxd)script/TestFlow.s.sol-- E2E smoke flow against a deployed local stack
Structure
src/
Liquid.sol, LiquidPosition.sol, LiquidTransmuter.sol
LiquidETHVault.sol, LiquidTokenVault.sol
LiquidCurator.sol, LiquidAllocator.sol
LiquidStrategyClassifier.sol, LiquidStrategy.sol, LiquidGauge.sol
LiquidGate.sol, LiquidCompliance.sol
adapters/ -- AbstractFeeVault, EulerUSDCAdapter, SecurityTokenAdapter
base/ -- Errors, ErrorMessages, LiquidTransmuterErrors
external/ -- LETH (synthetic), interfaces
governance/ -- LiquidGovernor, LiquidToken
interfaces/ -- All interfaces
libraries/ -- FixedPointMath, SafeCast, SafeERC20, TokenUtils, StakingGraph, …
mocks/ -- ERC20Mock, Pool, Stake, StakingPoolMock
strategies/ -- EETH, SfrxETH, Lido, EigenLayer, Pendle, Morpho, Yearn, …
test/ -- All test files
utils/ -- PermissionedProxy, Whitelist, ZeroXSwapVerifier
script/
DeployBrandL1.s.sol, DeployMainnet.s.sol, DeployLocal.s.sol, TestFlow.s.sol
lib/
forge-std, openzeppelin-contracts, openzeppelin-contracts-upgradeable
vault-v2, permit2, solmate, chainlink-brownie-contracts, halmos-cheatcodes