feat: governance execution verification, quadratic voting, oracle registry, insurance pool integration - #592
Merged
Levi-Ojukwu merged 4 commits intoJul 27, 2026
Conversation
execute_proposal fired invoke_contract for the cross-contract action call and unconditionally marked the proposal Executed and emitted ProposalExecuted, even if the callee panicked or returned an error. A failed cross-contract call (unauthorized, invalid params, paused target) left the protocol thinking a governance action had applied when nothing actually changed, with no way to retry. Switch to try_invoke_contract so the outcome is observable. On failure, the proposal is left at Passed (its eta_ledger is untouched) instead of being advanced to Executed, a ProposalExecutionFailed event is emitted, and execute_proposal returns the new ExecutionFailed error. Since the proposal stays Passed with its timelock already expired, calling execute_proposal again simply retries the same action - no separate retry mechanism or state field is needed. Also fixes tests_benchmarks::setup_benchmark_env(), which called initialize() with the pre-Invoice-Liquidity-Network#544 3-argument signature and hasn't compiled since the distribution_contract parameter was added - this was blocking the entire crate's test binary from building. Closes Invoice-Liquidity-Network#531
Linear vote weighting (weight = balance + delegated) lets a holder with 100x the tokens of another get exactly 100x the voting influence, so a handful of large holders can dominate every proposal outcome regardless of how many distinct holders disagree. Add a governance-controlled toggle (set_quadratic_voting_enabled / is_quadratic_voting_enabled, default off) that switches cast_vote to weight = isqrt(balance + delegated) instead - a whale with 100x the tokens ends up with only 10x the influence. isqrt is a floor integer square root via bounded binary search (no floating point in a #![no_std] contract). Combined balance is summed before the square root is taken, not square-rooted per-component then summed, so splitting delegation across multiple chains doesn't inflate total weight. The weight actually applied to each vote is now recorded as a receipt (AppliedVoteWeight, same temporary-storage TTL as HasVoted) and readable via get_applied_vote_weight, so the exact weight a cast vote carried is auditable independent of any balance changes afterward. Default-off keeps existing and in-flight proposals on the linear model they were built and tested against; toggling requires the same ILN-contract-authorized governance pattern as set_min_quorum_bps. Design tradeoffs (mid-window toggling, floor-sqrt quantization at small balances, alternatives considered) are written up in docs/adr/ADR-009-quadratic-voting.md. Adds SDK bindings (setQuadraticVotingEnabled, isQuadraticVotingEnabled, getAppliedVoteWeight) and the missing ExecutionFailed (Invoice-Liquidity-Network#531) error code to GovernanceContractError's mapper. Closes Invoice-Liquidity-Network#530
… registry Config.price_oracle was a single Option<Address> used only for payer identity/creditworthiness verification in fund_invoice. That doesn't scale to a protocol wanting multiple oracle kinds (price feeds, identity, credit scoring) or different providers per token (a USDC price feed and an XLM price feed are different contracts). Add an OracleFeedType registry (Price/Identity/Credit) resolved in priority order: per-token override, then feed-type-wide default, then (Identity only) the legacy price_oracle field - so existing set_price_oracle-only configurations keep working unmodified. fund_invoice now resolves through this registry for the Identity feed, keyed by the invoice's token, instead of reading price_oracle directly. All four registry mutators (register/remove, default and per-token) are require_admin-gated, matching the update_fee_rate/add_token pattern. Wire governance proposal actions (RegisterOracle, RemoveOracle) so these mutators are actually governance-controlled via execute_proposal, the same cross-contract-call pattern already used for fee rate/token/decay parameter changes. Health monitoring needed a dedicated entrypoint rather than piggybacking on fund_invoice: Soroban rolls back all storage writes for an invocation that returns Err, so a health snapshot written just before fund_invoice's existing OracleDataStale rejection never persists. check_oracle_health queries the resolved oracle and always records + returns the result without ever erroring, so keepers can poll staleness trends (consecutive_stale_count) independent of - and without risking - any funding side effect. Design tradeoffs are written up in docs/adr/ADR-010-oracle-registry.md. Also fixes tests_benchmarks.rs's initialize() call (still missing the distribution_contract arg from Invoice-Liquidity-Network#544) - discovered because it blocked the crate's test binary from compiling at all. Closes Invoice-Liquidity-Network#532
Currently, when an invoice defaults, an LP who opted into the insurance pool still had no way to actually get compensated - claim_default had no knowledge the pool existed. insurance_pool's own interface docs already described this exact follow-up: "the main invoice_liquidity contract, which (in a follow-up) invokes claim from its default-handling path via the generated InsurancePoolClient." Add insurance_pool as a regular (not just dev) dependency so claim_default can use the generated InsurancePoolInterfaceClient directly, matching the design already reviewed and written up in docs/insurance-pool-design.md. Store the deployed pool address as a new admin-gated DataKey::InsurancePool (set_insurance_pool/ get_insurance_pool, mirroring the set_price_oracle pattern). After the existing principal-refund loop in claim_default, check whether the claiming LP is enrolled and, if so, attempt pool.claim(invoice_id, lp). The pool credits the LP directly out of its own balance - invoice_liquidity never holds or forwards the payout, just triggers the claim and emits InsuranceClaimAttempted reporting the outcome. Use try_is_enrolled/try_claim (not the panicking variants) so a paused, empty, or unreachable pool degrades gracefully: claim_default still completes - the refund and status update already happened in the same atomic invocation - rather than reverting the whole default over an optional insurance top-up. Tests exercise the real insurance_pool contract (now a real dependency, not a mock) end-to-end: enrolled LP gets both the principal refund and the insurance payout, an unenrolled LP gets no attempt, and an empty pool still lets claim_default succeed. Closes Invoice-Liquidity-Network#529
|
@Asta-wizard Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Levi-Ojukwu
merged commit Jul 27, 2026
ddc5499
into
Invoice-Liquidity-Network:main
4 of 5 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #529
Closes #530
Closes #531
Closes #532
What changed
#531 — Proposal execution verification
execute_proposalfiredinvoke_contractfor the cross-contract action call and unconditionally marked the proposalExecuted, even if the callee panicked or returned an error.try_invoke_contractso the outcome is observable. On failure the proposal is left atPassed(timelock/eta untouched) instead of advancing toExecuted, aProposalExecutionFailedevent is emitted, andexecute_proposalreturns a newExecutionFailederror. Since the proposal staysPassedwith its timelock already expired, callingexecute_proposalagain simply retries the same action.tests_benchmarks::setup_benchmark_env(), which calledinitialize()with the pre-Implement on-chain mechanism to update distribution reward formulas #544 3-argument signature and hadn't compiled since thedistribution_contractparameter was added — this was blocking the entireiln_governancetest binary from building.#530 — Quadratic voting
weight = balance + delegated) lets a holder with 100x the tokens of another get exactly 100x the voting influence.set_quadratic_voting_enabled/is_quadratic_voting_enabled, default off) that switchescast_votetoweight = isqrt(balance + delegated)— a whale with 100x the tokens ends up with only 10x the influence.isqrtis a floor integer square root via bounded binary search (no floating point in a#![no_std]contract).AppliedVoteWeight, same TTL asHasVoted) and readable viaget_applied_vote_weight.docs/adr/ADR-009-quadratic-voting.md.#532 — Governance-controlled oracle registry
Config.price_oraclewas a single address used only for payer identity verification. Added anOracleFeedTyperegistry (Price/Identity/Credit) resolved per-token-override → feed-type-default → (Identity only) the legacyprice_oraclefield, so existingset_price_oracle-only configs keep working unmodified.fund_invoicenow resolves through the registry for theIdentityfeed, keyed by the invoice's token.RegisterOracle,RemoveOracle) so the registry is actually governance-controlled viaexecute_proposal.check_oracle_health) rather than piggybacking onfund_invoice, since Soroban rolls back all storage writes for an invocation that returnsErr— a health snapshot written just beforefund_invoice's existingOracleDataStalerejection would never persist. Design tradeoffs are written up indocs/adr/ADR-010-oracle-registry.md.#529 — Insurance pool integration
insurance_pool's own interface docs already described this follow-up: "the maininvoice_liquiditycontract, which (in a follow-up) invokesclaimfrom its default-handling path via the generatedInsurancePoolClient."insurance_poolas a regular (not just dev) dependency soclaim_defaultcan use the generatedInsurancePoolInterfaceClientdirectly. Deployed pool address is a new admin-gatedDataKey::InsurancePool(set_insurance_pool/get_insurance_pool).claim_default, checks whether the claiming LP is enrolled and, if so, attemptspool.claim(invoice_id, lp). The pool credits the LP directly from its own balance —invoice_liquiditynever holds or forwards the payout.try_is_enrolled/try_claim(not the panicking variants) so a paused, empty, or unreachable pool degrades gracefully:claim_defaultstill completes rather than reverting the whole default over an optional insurance top-up.Why
How to test
cargo test -p iln_governance— 97/98 passing (1 pre-existing failure,test_vote_receipt_available_within_ttl, unrelated — confirmed failing identically against unmodifiedmain).cargo test -p invoice_liquidity --lib— 93/97 passing (4 pre-existing failures —test_upgrade_*x3 andtest_admin_adds_token_with_different_decimals— confirmed failing identically against unmodifiedmainlogic; unrelated soroban-sdk API drift intests_new_features.rs/tests_storage_layout.rsthat predates this PR and isn't touched here).cargo build --workspaceandcargo clippy -p iln_governance --all-targets -- -D warnings— clean.cargo fmt --all -- --check— no new diffs introduced by this PR (pre-existing formatting drift onmainis left as-is, not touched).pnpm --filter @iln/sdk exec vitest run— new/touched SDK tests (admin.test.ts,governance.test.tsadditions) pass; pre-existing unrelated failures inerrors.test.ts,getTokenDecimals.test.ts,nft.test.ts, etc. are untouched by this PR.Notes for reviewers
invoice_liquidity's test suite (cargo test -p invoice_liquidity --lib) currently fails to compile onmaindue to pre-existing, unrelated soroban-sdk API drift intests_new_features.rsandtests_storage_layout.rs(Address::random()no longer exists; a function's return type changed toResult<Vec<u64>, ConversionError>without call sites being updated). This isn't touched here — fixing it would be a separate, unrelated PR — but it's worth flagging since it means that crate's test binary hasn't actually been exercised in a while (thetest/clippyCI jobs are also currently disabled viaif: ${{ false }}inci.yml, so this wasn't caught).tests_access_control.rs,tests_oracle_freshness.rs, plus a few others:tests_benchmarks.rs,tests_multi_token.rs,tests_lp_priority_queue.rs,tests_appeal.rs,tests_multisig_admin.rs) exist ininvoice_liquidity/srcbut aren't declared asmods inlib.rs, so they never actually compile/run as part ofcargo test. Not fixed here (out of scope for these 4 issues and would need its own review — some of those files have their own pre-existing issues once wired in), but flagging since it's a meaningful gap.