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.
112 lines
4.6 KiB
Solidity
112 lines
4.6 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.31;
|
|
|
|
/// @title AMM V2 Constant Product Invariant Symbolic Test
|
|
/// @notice Halmos symbolic execution test proving that the AMM V2 pair
|
|
/// maintains the constant product invariant: reserve0 * reserve1 >= k
|
|
///
|
|
/// The constant product invariant ensures that:
|
|
/// 1. After any swap, k does not decrease (it increases by fees)
|
|
/// 2. After mint/burn, the product adjusts proportionally
|
|
/// 3. No tokens can be extracted without providing the other side
|
|
///
|
|
/// Maps to: contracts/amm/AMMV2Pair.sol
|
|
/// Key state: reserve0 (uint112), reserve1 (uint112), kLast (uint256)
|
|
|
|
interface IAMMV2Pair {
|
|
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
|
|
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
|
|
function mint(address to) external returns (uint256 liquidity);
|
|
function burn(address to) external returns (uint256 amount0, uint256 amount1);
|
|
function totalSupply() external view returns (uint256);
|
|
function token0() external view returns (address);
|
|
function token1() external view returns (address);
|
|
}
|
|
|
|
interface IERC20 {
|
|
function balanceOf(address) external view returns (uint256);
|
|
function transfer(address, uint256) external returns (bool);
|
|
}
|
|
|
|
contract AMMInvariantTest {
|
|
|
|
IAMMV2Pair pair;
|
|
|
|
/// @notice Core invariant: reserve0 * reserve1 >= kLast after every swap
|
|
/// @dev The AMM V2 swap function enforces:
|
|
/// (reserve0_new - fee0) * (reserve1_new - fee1) >= reserve0_old * reserve1_old
|
|
/// where fee = 0.3% of input amount
|
|
function check_k_invariant_after_swap(
|
|
uint256 amount0Out,
|
|
uint256 amount1Out
|
|
) public {
|
|
(uint112 reserve0Before, uint112 reserve1Before,) = pair.getReserves();
|
|
|
|
// k before swap
|
|
uint256 kBefore = uint256(reserve0Before) * uint256(reserve1Before);
|
|
|
|
// Skip if reserves are zero (empty pool)
|
|
if (kBefore == 0) return;
|
|
|
|
// Bound outputs to prevent overflow
|
|
if (amount0Out >= reserve0Before || amount1Out >= reserve1Before) return;
|
|
if (amount0Out == 0 && amount1Out == 0) return;
|
|
|
|
// Execute swap (will revert internally if k violated)
|
|
try pair.swap(amount0Out, amount1Out, address(this), "") {
|
|
(uint112 reserve0After, uint112 reserve1After,) = pair.getReserves();
|
|
|
|
uint256 kAfter = uint256(reserve0After) * uint256(reserve1After);
|
|
|
|
// k must not decrease (it increases slightly due to fees)
|
|
assert(kAfter >= kBefore);
|
|
} catch {
|
|
// Swap reverted -- this is correct behavior for invalid inputs
|
|
}
|
|
}
|
|
|
|
/// @notice Invariant: LP shares represent proportional ownership
|
|
/// @dev After mint, new LP tokens = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY (first mint)
|
|
/// or proportional to existing supply
|
|
function check_mint_proportional(uint256 amount0, uint256 amount1) public {
|
|
// Bound inputs
|
|
if (amount0 == 0 || amount1 == 0) return;
|
|
if (amount0 > type(uint112).max || amount1 > type(uint112).max) return;
|
|
|
|
uint256 totalSupplyBefore = pair.totalSupply();
|
|
(uint112 r0Before, uint112 r1Before,) = pair.getReserves();
|
|
|
|
// After mint, totalSupply increases
|
|
// and reserve0 * reserve1 increases proportionally
|
|
if (totalSupplyBefore > 0 && r0Before > 0 && r1Before > 0) {
|
|
// Existing pool: new shares proportional to min(amount0/r0, amount1/r1)
|
|
uint256 expectedShares0 = (amount0 * totalSupplyBefore) / r0Before;
|
|
uint256 expectedShares1 = (amount1 * totalSupplyBefore) / r1Before;
|
|
uint256 expectedShares = expectedShares0 < expectedShares1
|
|
? expectedShares0
|
|
: expectedShares1;
|
|
|
|
// Minted shares should not exceed what the deposited tokens justify
|
|
assert(expectedShares <= totalSupplyBefore + amount0 + amount1);
|
|
}
|
|
}
|
|
|
|
/// @notice Invariant: burn returns proportional tokens without exceeding reserves
|
|
function check_burn_bounded(uint256 lpTokens) public {
|
|
if (lpTokens == 0) return;
|
|
|
|
uint256 totalSupplyBefore = pair.totalSupply();
|
|
if (lpTokens > totalSupplyBefore) return;
|
|
|
|
(uint112 r0, uint112 r1,) = pair.getReserves();
|
|
|
|
// Expected return: lpTokens / totalSupply * reserve
|
|
uint256 expected0 = (uint256(r0) * lpTokens) / totalSupplyBefore;
|
|
uint256 expected1 = (uint256(r1) * lpTokens) / totalSupplyBefore;
|
|
|
|
// Returns must not exceed reserves
|
|
assert(expected0 <= r0);
|
|
assert(expected1 <= r1);
|
|
}
|
|
}
|