Skip to main content

Zero-Knowledge Circuit Deep Dive

Circuit Overview

The Zyfai Rebalancer Validation Circuit (rebalancer-validation.circom) is a Circom 2.x circuit that validates DeFi rebalancing opportunities against backend constraints using zero-knowledge proofs. The circuit implements the core validation logic from Zyfai's backend, ensuring opportunities meet all safety and performance criteria.

Circuit Properties

  • Language: Circom 2.0.0+
  • Proof System: Groth16 (efficient on-chain verification)
  • Curve: bn128 (bn254)
  • Constraints: ~100-200 (includes comparison circuits from circomlib)
  • Public Signals: 15 (all inputs are public)
  • Private Signals: None (transparency-focused implementation)

Public Inputs

The circuit has 15 public input signals organized into three categories:

New Opportunity Data (7 signals)

These signals describe the rebalancing opportunity being evaluated:

SignalDescriptionScalingExample
liquidityAvailable liquidity in the pool×100 (2 decimals)10000000 = $100,000.00
zyfiTvlCurrent Zyfai TVL in the pool×100 (2 decimals)500000 = $5,000.00
amountRebalancer deposit amountToken smallest unit1000000 = 1 USDC (6 decimals)
poolTvlTotal pool TVL×100 (2 decimals)10000000 = $100,000.00
newApyNew opportunity APY×10000 (4 decimals)54352 = 5.4352%
apyStable7DaysAPY stable over 7 daysBoolean (0 or 1)1 = stable
tvlStableTVL is stableBoolean (0 or 1)1 = stable

Old Opportunity Data (7 signals)

These signals describe the previous opportunity (if any) for comparison and edge case handling:

SignalDescriptionScalingDefault
oldApyPrevious opportunity APY×10000 (4 decimals)0 = no old opportunity
oldLiquidityOld opportunity liquidity×100 (2 decimals)0 = no old opportunity
oldZyfiTvlOld opportunity Zyfai TVL×100 (2 decimals)0 = no old opportunity
oldTvlStableOld opportunity TVL stableBoolean (0 or 1)1 = defaults to stable
oldUtilizationStableOld opportunity utilization stableBoolean (0 or 1)1 = defaults to stable
oldCollateralHealthOld opportunity collateral healthyBoolean (0 or 1)1 = defaults to healthy
oldZyfiTVLCheckOld opportunity passes Zyfai TVL checkBoolean (0 or 1)1 = defaults to pass

User Preferences (1 signal)

SignalDescriptionScalingExample
supportsCurrentPoolCurrent pool is in user's preferencesBoolean (0 or 1)1 = supported

Validation Constraints

The circuit enforces 5 core validation rules that all must pass for a valid proof:

1. Available Liquidity Check

Rule: liquidity * 0.85 > zyfiTvl

Purpose: Ensures sufficient liquidity to cover the rebalancing amount.

Implementation:

// Avoid floating point: liquidity * 85 > zyfiTvl * 100
signal liquidityLeft <== liquidity * 85;
signal liquidityRight <== zyfiTvl * 100;

component liquidityCheck = GreaterThan(252);
liquidityCheck.in[0] <== liquidityLeft;
liquidityCheck.in[1] <== liquidityRight;

Example:

liquidity = 10,000,000 (scaled: $100,000.00)
zyfiTvl = 500,000 (scaled: $5,000.00)

Check: 10,000,000 × 85 = 850,000,000 > 500,000 × 100 = 50,000,000 ✓

2. TVL Constraint

Rule: poolTvl * 1e6 > amount * 400

Purpose: Prevents excessive allocation. Ensures rebalancer is at most 25% of pool TVL.

Math: amount * (100 / 25) = amount * 4, scaled by 100: poolTvl * 1e6 > amount * 400

Implementation:

signal tvlLeft <== poolTvl * 1000000;
signal tvlRight <== amount * 400;

component tvlCheck = GreaterThan(252);
tvlCheck.in[0] <== tvlLeft;
tvlCheck.in[1] <== tvlRight;

3. APY Performance Check (with Edge Cases)

Rule: newApy > oldApy + 10 OR edge cases

Purpose: Requires meaningful yield improvement (0.1%+) unless bypassed by edge cases.

Edge Cases (APY check bypassed if ANY are true):

  1. No Old Opportunity: oldApy == 0 (first deposit or no previous opportunity)
  2. Rebalancing from Problematic Pool: shouldRebalanceFromOld == 1 (computed internally)
  3. Pool No Longer Supported: supportsCurrentPool == 0 (user preferences changed)

shouldRebalanceFromOld Computation:

shouldRebalanceFromOld = enoughLiquidity && (
!oldZyfiTVLCheck ||
!oldTvlStable ||
oldLiquidityIsLow ||
!oldUtilizationStable ||
!oldCollateralHealth
)

Where:
- enoughLiquidity = oldLiquidity * 1e6 >= amount
- oldLiquidityIsLow = oldLiquidity * 0.85 < oldZyfiTvl

Implementation:

// Base APY check: newApy > oldApy + 10 (0.1% improvement)
signal apyThreshold <== oldApy + 10;
component apyCheck = GreaterThan(252);
apyCheck.in[0] <== newApy;
apyCheck.in[1] <== apyThreshold;

// Edge case 1: No old opportunity
component oldApyIsZero = IsEqual();
oldApyIsZero.in[0] <== oldApy;
oldApyIsZero.in[1] <== 0;

// Edge case 2: shouldRebalanceFromOld (computed internally)
// ... (see circuit code for full implementation)

// Edge case 3: Pool no longer supported
signal notSupportsCurrentPool <== 1 - supportsCurrentPool;

// APY check passes if ANY are true (OR logic)
signal apyCheckOrSkip <== apyCheckValue + oldApyIsZeroValue +
shouldRebalanceFromOldValue + notSupportsCurrentPool;

component apyCheckFinal = GreaterThan(252);
apyCheckFinal.in[0] <== apyCheckOrSkip;
apyCheckFinal.in[1] <== 0;

Example:

newApy = 120,000 (12.0000%)
oldApy = 100,000 (10.0000%)
supportsCurrentPool = 1

Check: 120,000 > 100,000 + 10 = 100,010 ✓
Result: 2% improvement exceeds 0.1% threshold

4. APY Stability Check

Rule: apyStable7Days == 1

Purpose: Ensures APY has been stable over the past 7 days (prevents volatile pools).

Implementation:

component apyStabilityCheck = IsEqual();
apyStabilityCheck.in[0] <== apyStable7Days;
apyStabilityCheck.in[1] <== 1;

Example:

apyStable7Days = 1

Check: 1 == 1 ✓
Result: APY is stable

5. TVL Stability Check

Rule: tvlStable == 1

Purpose: Verifies pool TVL is stable (prevents pools with unstable liquidity).

Implementation:

component tvlStabilityCheck = IsEqual();
tvlStabilityCheck.in[0] <== tvlStable;
tvlStabilityCheck.in[1] <== 1;

Example:

tvlStable = 1

Check: 1 == 1 ✓
Result: TVL is stable

Constraint Composition

All 5 checks are combined using AND logic (all must pass):

signal check12 <== liquidityCheckValue * tvlCheckValue;
signal check34 <== apyCheckFinalValue * apyStabilityCheckValue;
signal check5 <== tvlStabilityCheckValue;
signal check1234 <== check12 * check34;
signal finalCheck <== check1234 * check5;

// Enforce all checks must pass
finalCheck === 1;

If any constraint fails, witness generation fails and no proof can be generated.

Example Input

{
"liquidity": 100000000,
"zyfiTvl": 500000,
"amount": 1000000,
"poolTvl": 100000000,
"newApy": 120000,
"apyStable7Days": 1,
"tvlStable": 1,
"oldApy": 100000,
"oldLiquidity": 8000000,
"oldZyfiTvl": 400000,
"oldTvlStable": 1,
"oldUtilizationStable": 1,
"oldCollateralHealth": 1,
"oldZyfiTVLCheck": 1,
"supportsCurrentPool": 1
}

Example Output

{
"isValid": 1 (or 0)
}

Off-chain Checks

More checks are in done off-chain which are not a part of the zk circuit but the above covers all the basic checks that're being proven on-chain.

Security Considerations

Current Implementation

  • ✅ All inputs are public signals (transparency-focused)
  • ✅ Boolean validation prevents malformed inputs
  • ✅ Circom 2.x syntax with explicit public input declaration
  • ✅ Comparison circuits from audited circomlib
  • ✅ Edge case handling for APY checks

Performance Metrics

OperationTimeGas Cost
Circuit Compilation~30s-
Witness Generation~100ms-
Proof Generation~500ms-
Off-Chain Verification~50ms-
On-Chain Verification~50ms-

References