Skip to main content
The Open Nile Protocol enforces a set of mathematical invariants at the contract level. These invariants hold at all times — no transaction can violate them. They collectively guarantee that the protocol remains solvent and accounting stays consistent, even under adversarial conditions.

Core Invariants

1. Zero-Sum PnL

TraderPnL + PoolPnL = 0    (excluding fees)
Every trader profit is a pool loss and vice versa. The protocol creates no value and destroys no value (fees aside). When a trader closes a position with +500 USDC profit, the pool’s total assets decrease by exactly 500 USDC. This is the foundational accounting identity of the system.
Fees (trading fees, liquidation penalties, oracle fees) are extracted separately and distributed to fee destinations. They are not part of the zero-sum equation.

2. Margin Lock

collateralBalance[account] >= imLockedTotal[account]    for all accounts
No operation can make locked margin exceed deposited collateral. Every time a position is opened or margin is added, the contract verifies that the account has sufficient free collateral. This prevents undercollateralized positions from being created.

3. IM Greater Than MM

imFactorBps > mmFactorBps
The initial margin factor is always strictly greater than the maintenance margin factor. This is enforced by Config.setMarginConfig() and cannot be violated at any time. The gap between IM and MM provides a buffer zone — traders must deposit more margin than the liquidation threshold, giving them room to absorb adverse price movements before liquidation.

4. Position Status Consistency

status == OPEN   =>  closeReason == NONE
status == CLOSED =>  closeReason in {MATURED, LIQUIDATED, EARLY_TERMINATION}
The transition is strictly one-way: OPEN to CLOSED only. An open position always has NONE as its close reason. A closed position always has exactly one of the three valid close reasons. Positions cannot be reopened once closed.

5. Snapshot Immutability

Snapshotted parameters are set at position open and never modified:
Snapshotted FieldPurpose
snapshotImBpsInitial margin factor at time of open
snapshotMmBpsMaintenance margin factor at time of open
snapshotTradingFeeBpsTrading fee rate at time of open
snapshotLiquidationPenaltyBpsLiquidation penalty at time of open
snapshotOracleFeeOracle fee at time of open
marginModeMargin mode (ISOLATED or CROSS) at time of open
Admin configuration changes only affect newly opened positions. Existing positions retain the parameters they were opened with, preventing retroactive parameter changes from affecting trader economics.
imLocked is not a snapshotted config parameter. It is mutable via addPositionMargin and removePositionMargin, allowing traders to adjust their margin on open positions.

6. Exposure Consistency

sum(pairGrossNotional[pair]) = grossNotional
The aggregate gross notional across the pool always equals the sum of gross notional across all individual pairs. This is maintained atomically as positions are opened, closed, increased, and reduced.

7. Margin Bounded by Notional

imLocked <= notional    for all open positions
A position’s locked margin can never exceed its notional value. This is enforced at position open and when adding margin via addPositionMargin. It prevents economically irrational positions where the collateral exceeds the exposure.

8. Global Margin Accounting

globalImLocked = sum(imLockedTotal[account])    for all accounts
The global sum of locked margin always equals the sum across all individual accounts. This is updated atomically whenever margin is locked or unlocked, ensuring the protocol has an accurate view of total collateral commitments.

9. Fee Destination Sum

sum(feeDestination.shareBps) = 10,000
Fee destination shares always sum to exactly 100% (10,000 basis points). This is enforced by Config.setFeeDestinations(). Every fee collected is fully distributed — no portion is lost or unaccounted for.

Solvency Mechanism

The protocol uses multiple layers of defense to maintain solvency:
Each position’s losses are capped at its locked margin (imLocked). A position going deeply underwater cannot drain the trader’s free collateral or affect their other positions. This containment prevents cascading losses within a single account.
When a position’s market PnL loss exceeds its locked margin (|marketPnl| > imLocked), the excess becomes bad debt. The trader’s realized loss is capped at imLocked, and the remaining loss is absorbed by pool equity. A BadDebt event is emitted for tracking and monitoring.
Pool equity is clamped to zero and cannot go negative. Even if cumulative bad debt exceeds pool equity, the pool’s total assets floor at zero rather than becoming negative. This prevents accounting underflows and maintains the ERC-4626 vault invariant.
The maxUtilizationBps parameter (default: 8,000 bps = 80%) prevents the pool from becoming over-leveraged. LP withdrawals are restricted when utilization is high, ensuring the pool retains sufficient liquidity to cover open position obligations. This acts as a circuit breaker against liquidity crises.

How Invariants Are Tested

The protocol’s invariant set is verified through a comprehensive Foundry test suite:

550+ Tests

The full test suite includes unit tests, integration tests, and edge-case coverage across all contract modules.

95%+ Line Coverage

Production code (excluding mocks, scripts, and test helpers) has over 95% line coverage and 89%+ branch coverage.

Fuzz Testing

Foundry fuzz tests exercise invariants with randomized inputs, verifying that no combination of parameters can violate the core accounting identities.

Edge Cases

Dedicated tests cover corner cases like bad debt scenarios, zero notional, fee exceeding available balance, and simultaneous maturity with liquidation eligibility.
Key invariant tests verify:
  • Zero-sum: After every settlement and liquidation, the sum of trader PnL and pool PnL impact equals zero (excluding fees)
  • Margin lock: Attempting to open a position with insufficient free collateral reverts with the appropriate error
  • IM > MM: Setting mmFactorBps >= imFactorBps in Config reverts
  • Status consistency: Closed positions cannot be settled, liquidated, or modified
  • Exposure tracking: Aggregate exposure counters match the sum of individual position notionals after arbitrary sequences of opens, closes, and increases
Run the full test suite with make test from the repository root, or forge test for contract tests only. Use forge test --match-test Invariant to run invariant-specific tests.