mirror of
https://github.com/zooai/proofs.git
synced 2026-10-30 14:49:47 +00:00
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.
125 lines
5.1 KiB
Solidity
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);
|
|
}
|
|
}
|
|
}
|