Skip to content

v8.0.0 "Alpha" — emergency security upgrade - #534

Merged
Michael-Ixo merged 16 commits into
mainfrom
develop
Jun 24, 2026
Merged

Michael-Ixo merged 16 commits into
mainfrom
develop

Conversation

@Michael-Ixo

Copy link
Copy Markdown
Member

Promotes the v8.0.0 ("Alpha") emergency security release from develop to main.

This upgrade is already enacted on mainnet — gov proposal #482 passed and the Alpha upgrade applied at height 17871000.

Security

Addresses the disclosed vulnerability in x/bonds — see advisory GHSA-w3rp-4cm2-4wgc. The x/bonds module is disabled in this release.

What's included

  • x/bonds — module disabled (ante + msg-server + module guards), with e2e coverage
  • Authorization hardening — x/entity (signer↔DID binding, decorator recursion), x/token (batch), x/iid ante (MsgExec recursion handling), x/claims (IID-ante scoping)
  • ICA host — AllowMessages restricted to an ante-safe allow-list
  • Upgrade handler — app/upgrades/v8 (on-chain name Alpha)
  • Tests — unit + e2e no-ante harnesses, token batch, claims IID-ante regression guard

Scope

29 files, +1599 / -38.

Authored by: Michael Pretorius michael@ixo.world

Michael-Ixo and others added 16 commits April 7, 2025 16:36
* feat(claims): team member budgets for shared subscription pools

  Add per-member periodic spend budgets on claim collections so multiple
  members can share one deed's credit pool with individually enforced limits.

  - New MemberBudget state (prefix 0x05) keyed by collection_id/member_address.
    Independent KV entries keep per-member ops O(1) regardless of team size.
  - member_address threaded through SubmitClaimConstraints, CCAA constraints,
    MsgClaimIntent, MsgSubmitClaim, Intent and Claim with strict-equality
    matching at every step (empty == empty for individual subscriptions).
  - Anti-spoofing: admin locks member_address into the CCAA constraint at
    grant time; Accept() enforces equality, blocking grantees from minting
    authorizations tagged for other members.
  - Budget enforced at intent time (real spend gate, funds to escrow).
    Deducted on intent, restored on rejection / dispute / invalidation /
    intent expiration via EndBlocker.
  - Lazy period reset (feegrant PeriodicAllowance pattern, no cron).
  - New msgs: MsgSetCollectionMembers / MsgRemoveCollectionMembers (batch).
    New events: MemberBudget{Created,Updated,Removed}Event carrying full
    state for indexers. New queries: CollectionMember(List).

  All new fields are optional with empty defaults — backward compatible.

  Spec docs updated (concepts, state, messages, events, README).

  Authored by: Michael Pretorius <michael@ixo.world>

* feat(liquidstake): v7 multi-pool refactor + cross-module denom collision defences

    Reshape liquidstake from a single global ZERO/uzero pool into N independent
    pools, each with its own LST denom, validator whitelist, admin, fees, paused
    flag, and proxy account. Pool creation is gov-only; per-pool config edits are
    admin-or-gov. Mint stays 1:1 (admin-only LiquidStake preserves the pre-v7
    invariant); unstake rate captures all value accrual.

    - Proto: split Params into ModuleParams (global: min_liquid_stake_amount,
      module_paused) + Pool (per-pool); new MsgCreatePool / MsgUpdatePool /
      MsgUpdateModuleParams / MsgSetPoolPaused / MsgSetModulePaused; pool_id
      threaded through every per-pool message and event. Per-tx/per-epoch event
      fields upgraded from formatted strings to typed Coin / Timestamp / uint32
      while preserving v6 field tag numbers (pool_id appended at the end).
    - Keeper: per-pool LiquidValidator storage (prefix 0x11 + lp(poolID) + valAddr),
      Pool CRUD + registerPool, per-pool NetAmountState, per-pool LiquidStake /
      LiquidUnstake / autocompound / rebalance, BeginBlock and epoch hooks iterate
      pools, ModuleParams.module_paused as global kill switch.
    - Denom collision defence (3 layers): registerPool refuses IBC-shape denoms,
      bond denom, denoms with non-zero bank supply OR existing bank metadata;
      RegisterLSTDenomMetadata claims the denom in bank with the standard
      u-prefix two-unit shape (uzero@0 + zero@6); MintCoinsRestriction installed
      in app/keepers/keepers.go rejects any non-liquidstake module trying to mint
      a pool's LST denom. Bonds.CreateBond gains the same supply/metadata guard
      so collisions are caught at the front door instead of halting the chain
      in EndBlocker.
    - v7 upgrade migration: legacy Params -> ModuleParams + "zero" Pool with
      proxy_account_address pinned to the pre-v7 LiquidStakeProxyAcc bech32
      (preserves all existing delegations untouched), re-keys legacy 0x02
      LiquidValidator records under 0x11+lp("zero"), claims uzero metadata.
      Skips pool construction entirely if no legacy denom was set.
    - Local-test mode: x/epochs/types/genesis.go, x/liquidstake/types/keys.go,
      and x/mint/types/params.go each carry a commented "2min" alternative the
      operator can flip on for fast local autocompound/rebalance testing. All
      three files default to mainnet values; comments at each call site
      document that the three files must be flipped together.
    - Spec docs: x/liquidstake/spec/{README,01_concepts,02_state,03_messages,
      04_events,05_params}.md cover every type, message, event, parameter,
      and the v7 migration mechanics.

    Authored by: Michael Pretorius <michael@ixo.world>

* feat(names): add chain-level name service module

  x/names: governance-managed namespaces mapping human-readable handles
  to DIDs. Self-register and registrar-on-behalf flows, status lifecycle
  (no hard-delete), reverse-lookup index by owner DID, and dual-emit
  NameUpdatedEvent + action events for clean indexer integration.

  - Messages: gov Create/UpdateNamespace; user RegisterName/TransferName;
    registrar RegisterNameByRegistrar / UpdateNameByRegistrar /
    SetNameStatus.
  - Six query rpcs (Namespace, Namespaces, ResolveName, GetName,
    NamesByNamespace, NamesByOwner) — all wasm-whitelisted.
  - Defensive length caps on namespace text fields and registrar-supplied
    evidence_hash / source, dual-enforced at ValidateBasic and inside the
    keeper handler so Wasm sub-message dispatches can't bypass.
  - v7 upgrade adds the new store key.
  - Also fills wasm-whitelist gaps for claims (CollectionMember,
    CollectionMemberList) and entity (EntityMetaData, EntityIidDocument).
  - Spec docs under x/names/spec/.

  Authored by: Michael Pretorius <michael@ixo.world>

* feat(claims): add FLAGGED non-terminal evaluation status

  Add `EvaluationStatus.FLAGGED (5)` — an escape-hatch outcome for
  evaluators who want to defer a final call (e.g. AI oracles escalating
  ambiguous cases to a human reviewer). FLAGGED skips payment and the
  intent-escrow refund branch and keeps funds locked in escrow until
  terminal finalisation; it still consumes AgentQuota (only INVALIDATED
  skips).

  A FLAGGED claim may be re-evaluated by any authorized evaluator,
  including the original flagger if they later get more information. The
  same agent cannot re-flag a claim they've already flagged — checked
  across both the current evaluation and the full evaluation_history,
  so flag-bombing across an intervening flag from another agent is also
  rejected (ErrSelfReFlag).

  State additions: Claim.evaluation_history (prior evaluations in
  chronological order, latest stays in evaluation), Collection.flagged
  (cumulative event counter, never decremented), Collection.flagged_active
  (claims currently in FLAGGED state).

  ClaimEvaluatedEvent / ClaimUpdatedEvent are reused; indexers distinguish
  flag events by inspecting evaluation.status. Spec docs updated across
  concepts / state / messages / events / future improvements.

* feat(claims): v7 dispute resolution and performance deposits

  - Adds rolling AgentDepositBalance per (collection, agent) with a
    min_deposit_period lock on withdrawals to close the
    deposit-submit-withdraw exploit; slash path bypasses the lock.
  - Adds Dispute target_role (SUBMITTER/EVALUATOR), at-most-one-OPEN-per-
    pair semantics, AWARDED permanently blocks, DISMISSED supersedes.
  - Adds MsgAdjudicateDispute with DID-key auth (IID ante + keeper
    re-check), per-adjudicator AdjudicationDid.reward_percentage so each
    whitelisted DID self-sets their fee, configurable AWARDED/DISMISSED
    split, intended-vs-actual penalty captured on DisputeResolution.
  - DisputeResolution.data and MsgAdjudicateDispute.data replace the v6
    free-form reason string with a DisputeData payload (uri + proof +
    type + encrypted), symmetric with MsgDisputeClaim.data.
  - FLAGGED evaluations cannot be disputed as EVALUATOR; re-evaluating
    to a terminal status re-enables that path.
  - New events for ClaimDisputed / DisputeResolved and the
    AgentDepositBalance lifecycle; v3→v4 migration stamps legacy
    disputes as DISMISSED; spec docs (01/02/03/04/README) updated.

* feat(claims): add MsgUpdateCollectionQuota

  Adds the missing admin-mutation path for collection.quota — the only
  user-facing Collection field without an update msg until now. Handler
  rejects new quota < current count with ErrCollectionQuotaBelowCount so
  already-submitted claims can't be retroactively invalidated; quota = 0
  keeps its unlimited semantics. Wired into msg_server, codec,
  ValidateBasic, and the cli (update-collection-quota); spec
  (03_messages.md) updated.

* feat(iid): block module-reserved DID namespaces on MsgCreateIidDocument

  Adds a guard rejecting user-submitted MsgCreateIidDocument for DIDs
  under a module-reserved prefix (currently did:ixo:entity:...). Those
  prefixes are minted deterministically by their owning module via
  IidKeeper.SetDidDocument, so without this guard a malicious user could
  squat a DID the entity module's CreateSequence will later try to mint,
  deadlocking the module. Enforced in ValidateBasic (catches at antehandler)
  and again at the msgServer handler (defense in depth for direct
  callsites). New ErrReservedDidNamespace, ReservedDidPrefixes registry
  in iid/types/keys.go, and spec note in iid/spec/03_messages.md.

  Authored by: Michael Pretorius <michael@ixo.world>

* test(app): comprehensive L1+L2+L3 suite + GH Actions wiring

  Adds a multi-layer test harness for ixo-blockchain:

    L1 (keeper unit tests) — every custom module
      - x/iid, x/bonds, x/entity, x/claims, x/token, x/epochs, x/mint,
        x/names, x/liquidstake, x/smart-account, app/ante
      - Testify suite + apptesting/ harness (app/apptesting/) for
        keeper-level tests against a real IxoApp instance.

    L2 (simulator) — tests/simulator/
      - sim_test, sim_genesis_test, sim_invariants_test,
        sim_module_accounts_test, sim_modules_test, sim_ordering_test,
        sim_gov_lifecycle_test, sim_bench_test.
      - AppModuleSimulation hooks (Decoder + empty WeightedOperations)
        wired on every custom module's module.go.
      - scripts/makefiles/tests.mk: test-sim-app, test-sim-determinism,
        test-sim-import-export targets.

    L3 (interchaintest E2E) — tests/interchaintest/
      - Per-module FullScenario tests (bank/staking/distribution/
        slashing/gov/wasm/ibc + iid/bonds/entity/claims/token/names/
        liquidstake/chaintime/tx-delegation/multi-message-atomicity).
      - Dedicated claims flows:
          TestIxoClaimsFlagged_FullScenario
          TestIxoClaimsDisputes_FullScenario
          TestIxoClaimsDisputesValidations_FullScenario
          TestIxoClaimsDisputesMultiAdjudicator_FullScenario
      - tests/interchaintest/ is a sub-module (its own go.mod) to keep
        docker-dependent deps off the main build.

    Silent-drop chain bug fixes uncovered while writing tests:
      - iid: MsgUpdateIidDocument dropped all fields (msg_server.go
        missing `*didDoc = did`)
      - iid: MsgDeactivateIID ignored `state` (Deactivate() always
        true)
      - iid: add-verification-method CLI used wrong args index
      - iid: nested message types rendered as {} via autocli; manual
        GetQueryCmd added (x/iid/client/cli/query.go)
      - bonds: --oracle-did CLI flag not registered + NewMsgCreateBond
        dropped OracleDid silently
      - liquidstake: autocli + dynamicpb Coin panic; manual GetTxCmd
        added (x/liquidstake/client/cli/tx.go)

    CI (.github/workflows/tests.yml):
      - push to main/master/develop, PRs into main/develop,
        workflow_dispatch.
      - Unit + race + simulator on every push / PR.
      - Full L3 Docker E2E only on PRs into main + manual dispatch.

    Lefthook (lefthook.yml): pre-push hook runs make test-unit so the
    L1 sweep can't accidentally regress.

  Authored by: Michael Pretorius <michael@ixo.world>

* feat(iid): restrict MsgCreateIidDocument DID forms to signer-account and wasm-contract

  IXO-2045. Adds ValidateMsgCreateDIDForm: only did:ixo:<bech32-account>
  (account must equal signer) and did:ixo:wasm:<bech32-contract> (anyone
  may create) are accepted. Reserved did:ixo:entity: stays rejected; any
  other form (did:cosmos:, did:x:, did:ixo:foo:bar, malformed wasm) is
  refused with ErrDIDFormNotAllowed / ErrDIDAccountSignerMismatch.
  Enforced in ValidateBasic + msg-server (defense in depth so Cosmwasm
  stargate cannot bypass). Unit + L3 coverage added.

* fix: add legacy proto for indexing services

* chore(upgrade): name v7 upgrade "Opus"

Authored by: Michael Pretorius <michael@ixo.world>

* docs(liquidstake): regenerate proto-docs for legacy MsgUpdateParams

Authored by: Michael Pretorius <michael@ixo.world>

* chore(app): bump go module path v6 -> v7 for the Opus upgrade

Sweep github.com/ixofoundation/ixo-blockchain/v6 -> /v7 across go.mod,
all Go sources, proto go_package options, the interchaintest submodule
(module + local replace), CLAUDE.md and scripts/protoc-gen.sh. go mod
tidy. Root build, vet and the interchaintest test compile all pass.

Authored by: Michael Pretorius <michael@ixo.world>

* chore(docker): default image build version to v7.0.0

GIT_VERSION baked into ixod via ldflags now defaults to v7.0.0 (was the
stale v6.0.0); GIT_COMMIT defaults to zeros and should be overridden with
--build-arg GIT_COMMIT=$(git rev-parse HEAD) at build time.

Authored by: Michael Pretorius <michael@ixo.world>
…zation

Emergency v8 upgrade following the 2026-06-20 x/bonds reserve drain, in which
bonds handlers moved funds from a DID-resolved address without checking it
against the transaction signer. Disables bonds and closes the broader class of
DID/authz authorization gaps with keeper-level, route-independent checks.

x/bonds — disabled:
- Register a disabled msg server: every bonds message returns
  ErrBondsModuleDisabled, covering all routes (top-level, authz, CosmWasm, ICA).
- No-op the batch EndBlocker so existing bonds can no longer move reserves.
- Add an ante guard that rejects any bonds message, recursing authz.MsgExec.

x/iid — ante MsgExec recursion:
- VerifyIidControllersAgainstSignature now unwraps nested authz.MsgExec so
  IID-controlled messages cannot skip the controller check.

x/entity — keeper authorization:
- Add VerifyDidSignerAuthentication; UpdateEntity / TransferEntity /
  UpdateEntityVerified now bind the signer to the acting DID in the keeper, so
  authorization holds on routes that bypass the ante (CosmWasm, ICA, authz).
- BlockNftContractTransferForEntityDecorator now recurses authz.MsgExec.

x/claims — scope IidTxMsg to signer-controlled DIDs:
- Drop IidTxMsg from MsgSubmitClaim / MsgEvaluateClaim /
  MsgCreateClaimAuthorization (proto signer is admin_address; the *_did field is
  agent/creator attribution, authorized in-keeper via collection.Admin). Keep
  DisputeClaim / AdjudicateDispute, whose signer is the DID's own party.

x/token — batch hardening:
- ValidateTokenBatch enforces a non-empty batch, per-element id/amount, and a
  single contract; CancelToken uses checked subtraction (no underflow panic).

app — v8 upgrade:
- Add app/upgrades/v8 and register it. The handler tightens ICA-host
  AllowMessages from ["*"] to an ante-safe allow-list (standard Cosmos messages
  only; ixo identity/asset modules excluded).

tests:
- Unit, decorator, no-ante router e2e, full signed-tx ante e2e, ICA allow-list
  guard, and a claims IidTxMsg-membership regression across the changed modules.

Authored by: Michael Pretorius <michael@ixo.world>
The MsgSoftwareUpgrade plan.name must equal UpgradeName; binary stays v8.0.0.

Authored by: Michael Pretorius <michael@ixo.world>
Their proto signer agent_address may be a delegated agent or an entity
module account (e.g. the SUPA onboarding fee account) that is decoupled
from AgentDid, which is attribution only. Authorization is enforced in
the keeper (SubmitClaimAuthorization grant / dispute deposit) on every
route, so the IID ante's signer-controls-DID check wrongly broke these
on-behalf flows. MsgAdjudicateDispute stays IidTxMsg: the keeper's
AuthorizeAdjudicator itself requires signer to control AdjudicatorDid.

Authored by: Michael Pretorius <michael@ixo.world>
Sweep github.com/ixofoundation/ixo-blockchain/v7 -> /v8 across go.mod
(module + interchaintest replace directive + its own module path), all
.go imports, proto go_package options, CLAUDE.md, CHANGELOG.md and
scripts/protoc-gen.sh. The app/upgrades/v7 handler package is left
intact — only the module-path prefix changes. go build ./... and all
test-package compilation pass.

Authored by: Michael Pretorius <michael@ixo.world>
@Michael-Ixo
Michael-Ixo merged commit 572c58c into main Jun 24, 2026
15 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant