Automated Rebalancing
How AutoRebalancer proves all three pillars: ZK Privacy, Verifiable AI, and DAO Constraints working together in production.
The Complete Flow
The AutoRebalancer is where everything comes together. It demonstrates how AI-driven decisions can be:
- ‣Verifiable — Every recommendation is committed on-chain before execution
- ‣DAO-Constrained — All allocations must pass governance rules
- ‣Privacy-Preserving — Pool-level rebalancing doesn't expose individual positions
The 4-Step Verification Flow
Trigger Detection (On-Chain)
AutoRebalancer monitors on-chain conditions every block:
function checkUpkeep(bytes calldata)
external view returns (bool upkeepNeeded, bytes memory performData)
{
// 1. Check cooldown (5 blocks for testnet, 100+ for production)
if (block.number < lastRebalanceBlock + MIN_BLOCKS_BETWEEN_REBALANCE) {
return (false, "Cooldown active");
}
// 2. Check allocation drift (>5% from target)
for (protocol in protocols) {
uint256 drift = calculateDrift(lastAllocation, currentAllocation);
if (drift > 500) return (true, "Allocation drift detected");
}
// 3. Check risk score changes (>10 points)
for (protocol in protocols) {
uint256 riskChange = abs(currentRisk - lastRisk);
if (riskChange > 10) return (true, "Risk score changed");
}
// 4. Check APY differentials (>2% spread)
if (maxAPY - minAPY > 200) return (true, "APY differential detected");
return (false, "No rebalance needed");
}Rebalancing is triggered by objective on-chain conditions, not arbitrary decisions. Anyone can verify the trigger was legitimate by checking the same on-chain data.
AI Analysis & Commitment (Verifiable AI)
When conditions are met, AutoRebalancer generates an AI recommendation and commits it on-chain before execution:
function performUpkeep(bytes calldata performData) external {
// Re-verify conditions (security critical)
(bool upkeepNeeded, ) = this.checkUpkeep("");
require(upkeepNeeded, "Upkeep not needed");
// Generate AI recommendation based on risk scores
(uint256 aaveAlloc, uint256 lidoAlloc, uint256 compAlloc) =
_generateRecommendation(protocols, percentages);
// COMMIT FIRST (before execution)
bytes32 commitmentHash = keccak256(abi.encode(
aaveAlloc, lidoAlloc, compAlloc, reason, block.timestamp
));
uint256 recId = aiCommitment.commitRecommendation(commitmentHash);
// Then execute...
}The AI commits to its recommendation before execution. The commitment hash is stored on-chain and can't be changed later. This creates an immutable audit trail of what the AI decided and when.
DAO Governance Check (DAO Constraints)
Before execution, the allocation must pass DAO-defined constraints:
function updateAllocationWeightsWithAI(...) external onlyOwner {
require(_aaveWeight + _lidoWeight + _compoundWeight == 10000, "Must total 100%");
// Verify AI recommendation matches commitment
bool verified = aiCommitment.verifyRecommendation(
recommendationId, _aaveWeight, _lidoWeight, _compoundWeight, reason
);
require(verified, "Allocation does not match AI recommendation");
// Check DAO constraints (happens in verifyRecommendation)
// - Max single protocol: 50%
// - Min diversification: 3 protocols
// - Risk ceiling: Score < 70
// Only if all checks pass, update weights
aaveWeight = _aaveWeight;
lidoWeight = _lidoWeight;
compoundWeight = _compoundWeight;
emit AIVerifiedAllocation(recommendationId, ...);
}The AI cannot bypass DAO governance rules. Even if the AI generates a recommendation, it will be rejected on-chain if it violates constraints like "no single protocol > 50%". The DAO stays in control.
Execution & Privacy (ZK Privacy Pools)
Once verified, the rebalance executes at the pool level, preserving individual privacy:
// Pool-level rebalancing (aggregate capital)
strategyRouter.rebalance(
["aave"], // Withdraw from Aave
["lido"], // Deposit to Lido
[amountToMove] // Move X wETH
);
// What's visible on-chain:
✅ Pool moved 10 wETH from Aave to Lido
✅ New allocation: 35% Aave, 40% Lido, 25% Compound
✅ AI Recommendation #17 was verified and executed
// What's NOT visible:
❌ Individual user positions (hidden via ZK commitments)
❌ Which users deposited how much
❌ Who will withdraw next (unlinkable addresses)The rebalance operates on aggregate pool capital, not individual positions. Users maintain privacy through ZK commitments while benefiting from AI-driven optimization. The AI sees the forest, not the trees.
Why This Flow Matters
- • AI is a black box
- • No audit trail
- • DAO has no control
- • Everyone sees your moves
- • Manual multisig (too slow)
- • Trusted AI service (opaque)
- • Privacy pools (no AI)
- • Public AI agents (no privacy)
- • AI commits before execution
- • DAO rules enforced on-chain
- • Full audit trail
- • Individual privacy preserved
Technical Implementation
Commit-Reveal Pattern
The AI uses a cryptographic commit-reveal pattern to ensure recommendations can't be changed after the fact:
// Step 1: Commit (before execution)
bytes32 commitmentHash = keccak256(abi.encode(
aaveAllocation, // e.g., 4500 (45%)
lidoAllocation, // e.g., 3000 (30%)
compoundAllocation, // e.g., 2500 (25%)
reason, // "Increased Aave for higher yields"
timestamp // Block timestamp
));
uint256 recId = aiCommitment.commitRecommendation(commitmentHash);
// Step 2: Reveal (during execution)
bool verified = aiCommitment.verifyRecommendation(
recId,
aaveAllocation,
lidoAllocation,
compoundAllocation,
reason
);
// If hash doesn't match, transaction revertsDAO Constraint Enforcement
Every allocation (AI or manual) must pass DAO-defined rules:
// In PoolController.updateAllocationWeights()
if (address(daoConstraintManager) != address(0)) {
require(
daoConstraintManager.checkAllocationConstraints(
_aaveWeight,
_lidoWeight,
_compoundWeight
),
"Violates DAO constraints"
);
}
// DAOConstraintManager checks:
// 1. No single protocol > maxSingleProtocolAllocation (default: 50%)
// 2. At least minDiversification protocols with >10% (default: 3)
// 3. Total allocation = 100% (10000 basis points)Privacy Preservation
Rebalancing operates on aggregate pool capital, not individual commitments:
// What the AI sees:
Pool State {
totalTVL: 100 wETH,
allocations: { aave: 40%, lido: 35%, compound: 25% },
riskScores: { aave: 45, lido: 38, compound: 52 }
}
// What the AI DOESN'T see:
❌ Individual commitments (ZK-hidden)
❌ User deposit amounts
❌ Withdrawal addresses
❌ Transaction history per user
// Result: AI optimizes the pool, users keep privacyLive Example: Testnet Rebalance
Here's what happens when you trigger AutoRebalancer on the demo:
true because Aave risk score changed from 45 → 58 (exceeds 10-point threshold)✅ Diversification: 3 protocols with >10%
✅ Total: 30% + 42% + 28% = 100%
✅ Event: AllocationWeightsUpdated(3000, 4200, 2800)
Anyone can verify this rebalance by:
- Checking the trigger condition was met (on-chain data)
- Verifying the commitment hash matches the executed allocation
- Confirming DAO constraints were satisfied
- Seeing the full transaction history in the block explorer
Demo vs Production
🧪 Testnet (Current)
- Trigger: Manual via
cast send - AI Logic: Simple risk-based allocation (on-chain)
- Cooldown: 5 blocks (~5 transactions)
- Purpose: Demonstrate the verification flow
🚀 Production (Future)
- Trigger: Chainlink Automation (automatic)
- AI Logic: Off-chain ML models via Chainlink Functions
- Cooldown: 100+ blocks (~20 minutes)
- Purpose: Real treasury management at scale
Key Point: The verification logic is identical in both environments. Only the trigger mechanism changes (manual script vs Chainlink Keepers). This ensures the testnet accurately demonstrates production behavior.
Try It Yourself
Trigger an Automated Rebalance
- Deposit funds to the pool (at least 10 wETH)
- Wait 5 blocks (make a few deposits/withdrawals to advance blocks)
- Check if upkeep is needed:
cast call $AUTO_REBALANCER "checkUpkeep(bytes)(bool,bytes)" 0x \ --rpc-url http://localhost:8545
- If true, trigger rebalance:
cast send $AUTO_REBALANCER "performUpkeep(bytes)" 0x \ --rpc-url http://localhost:8545 --private-key $PRIVATE_KEY
- Check the History tab to see the new AI-verified rebalance with full audit trail
Related Documentation: