Files
proofs/halmos/LiquidSolvency.t.sol
zeekay c86eec94cb merge: absorb /Users/z/work/zoo/formal into proofs (de-duplicate)
Local zoo/formal was a stale parallel checkout that pointed at
zoo-labs/formal (non-existent); the canonical remote is
zooai/proofs. This commit absorbs the unique formal-verification
work from the old checkout into proofs:

- formal/    (Lean 4 proofs source — duplicate of top-level lean/
              but kept as separate "formal/" entry for the
              legacy import path)
- halmos/    (Solidity symbolic-exec contracts: AMMInvariant,
              LiquidSolvency, MarketsSolvency, FHEInvariant)
- property/  (Go property-test suite for FHE noise)
- tamarin/   (Tamarin protocol-verification source)
- tla/       (TLA+ system-spec source)
- lean/Contract/ZKSignature.lean  (unique, not in proofs/lean)
- lean/Crypto/ZKSignature.lean    (unique, not in proofs/lean)
- FORMAL_VERIFICATION.md (preserved from the old README)
- LLM-formal.md          (preserved from the old LLM.md)

The duplicate folder /Users/z/work/zoo/formal is now deleted on
disk. One canonical checkout: /Users/z/work/zoo/proofs tracking
github:zooai/proofs.
2026-05-12 21:13:00 -07:00

125 lines
5.1 KiB
Solidity

// SPDX-License-Identifier: BSD-3-Clause
pragma solidity ^0.8.31;
/// @title LiquidLUX Solvency Symbolic Test
/// @notice Halmos symbolic execution test proving that xLUX vault
/// can never become insolvent: totalAssets >= totalSupply * minSharePrice
///
/// Invariants tested:
/// 1. Vault solvency: underlying LUX >= xLUX shares outstanding
/// 2. Share price monotonicity: share price never decreases (except slashing)
/// 3. Fee accounting: 10% to treasury, 90% to vault (no leakage)
/// 4. Deposit/withdraw conservation: no LUX created or destroyed
/// @dev Minimal interface matching LiquidLUX.sol
interface ILiquidLUX {
function deposit(uint256 assets) external returns (uint256 shares);
function withdraw(uint256 shares) external returns (uint256 assets);
function receiveFees(uint256 amount, bytes32 feeType) external;
function depositValidatorRewards(uint256 amount) external;
function totalAssets() external view returns (uint256);
function totalSupply() external view returns (uint256);
function convertToAssets(uint256 shares) external view returns (uint256);
function convertToShares(uint256 assets) external view returns (uint256);
function performanceFee() external view returns (uint256);
function treasury() external view returns (address);
}
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function totalSupply() external view returns (uint256);
}
contract LiquidSolvencyTest {
ILiquidLUX vault;
IERC20 lux;
/// @notice Invariant: totalAssets >= totalSupply (1:1 minimum backing)
/// @dev If totalSupply > 0, each share is worth at least 1 unit of LUX.
/// This holds because:
/// - Deposits mint shares = convertToShares(assets) <= assets
/// - Withdrawals burn shares and return convertToAssets(shares)
/// - Fees increase totalAssets without minting shares
/// - Validator rewards increase totalAssets without minting shares
function check_solvency(uint256 depositAmt, uint256 withdrawShares) public {
// Precondition: vault exists and has valid state
uint256 totalAssetsBefore = vault.totalAssets();
uint256 totalSupplyBefore = vault.totalSupply();
// Solvency holds before
assert(totalAssetsBefore >= totalSupplyBefore);
// Symbolic deposit
if (depositAmt > 0 && depositAmt < type(uint128).max) {
uint256 sharesMinted = vault.deposit(depositAmt);
uint256 totalAssetsAfter = vault.totalAssets();
uint256 totalSupplyAfter = vault.totalSupply();
// Solvency invariant: assets grew by depositAmt, shares grew by sharesMinted
// Since sharesMinted = convertToShares(depositAmt) and share price >= 1,
// sharesMinted <= depositAmt, so solvency is maintained.
assert(totalAssetsAfter >= totalSupplyAfter);
}
// Symbolic withdrawal
if (withdrawShares > 0 && withdrawShares <= totalSupplyBefore) {
uint256 assetsReturned = vault.withdraw(withdrawShares);
uint256 totalAssetsAfter = vault.totalAssets();
uint256 totalSupplyAfter = vault.totalSupply();
// Solvency maintained: assets decreased by assetsReturned,
// supply decreased by withdrawShares.
// Since assetsReturned = convertToAssets(withdrawShares),
// the ratio is preserved.
assert(totalAssetsAfter >= totalSupplyAfter);
}
}
/// @notice Invariant: fee revenue increases totalAssets without minting
function check_fee_conservation(uint256 feeAmount, bytes32 feeType) public {
uint256 totalAssetsBefore = vault.totalAssets();
uint256 totalSupplyBefore = vault.totalSupply();
if (feeAmount > 0 && feeAmount < type(uint128).max) {
vault.receiveFees(feeAmount, feeType);
uint256 totalAssetsAfter = vault.totalAssets();
uint256 totalSupplyAfter = vault.totalSupply();
// Supply unchanged (no shares minted for fees)
assert(totalSupplyAfter == totalSupplyBefore);
// Assets increased by 90% of fee (10% goes to treasury)
// totalAssetsAfter >= totalAssetsBefore + feeAmount * 90 / 100
assert(totalAssetsAfter >= totalAssetsBefore);
}
}
/// @notice Invariant: share price never decreases from deposits/fees
function check_share_price_monotone(uint256 feeAmount) public {
uint256 priceBefore;
if (vault.totalSupply() > 0) {
priceBefore = vault.convertToAssets(1e18);
} else {
priceBefore = 1e18; // 1:1 initial price
}
if (feeAmount > 0 && feeAmount < type(uint128).max) {
vault.receiveFees(feeAmount, bytes32("DEX"));
uint256 priceAfter;
if (vault.totalSupply() > 0) {
priceAfter = vault.convertToAssets(1e18);
} else {
priceAfter = 1e18;
}
// Share price only goes up from fee income
assert(priceAfter >= priceBefore);
}
}
}