package keeper // msg_server.go implements the cover module's MsgServer (REQ-046, REQ-047, // REQ-049, REQ-050, REQ-052, REQ-055, REQ-056, REQ-062, REQ-048, D-077, // D-079, D-086, D-088, D-089, D-090). The MsgServer wraps the Keeper + the // four expected-keeper shims (already on the Keeper: StandingKeeper, // WatcherKeeper, BondKeeper, StillKeeper). // // Each method returns a (*Response, error). Handler state-machine ordering // is enforced: ValidateBasic -> handler authz/gate -> state mutation -> // ctx.EventManager().EmitEvent. // // P1 handler set: // - LaunchCoverPool: D-086 category phase check + D-077 Standing gate + // reserve floor + Watcher attestation; persists the CoverPool. // - RouteCoverFee: D-079 Anti-Crowding-Out firewall + category-tag match + // below-floor auto-pause + StillKeeper invocation; emits the routing // event. // - FileCoverCall: P1 scaffold — persists the CoverCall + emits an event; // P4 adds the Voucher adjudication + no-self-adjudication + slashing. // // P2 handler set: // - SignCoverCharter: D-090(1) Bill of Rights gate (ValidateBasic) + // idempotency + Watcher attestation; persists the CoverCharter. // - AmendCoverCharter: creates a CharterAmendment with Status=Proposed; // the 7-day cooling is enforced by CoolCharterAmendment / // RatifyCharterAmendment (keeper helpers). // - ElectPoolMason: loads/creates the PoolCouncil + adds the Mason (max // 3 — a 4th is REJECTED). // - VoteCoverCall: loads the CoverCall + Watcher-observer-present check // for a CallVoteYes; persists the CoverCallVote. // - AmendPoolStandingGate: D-090(3) dual check (ValidateBasic + handler // re-check) + updates the pool's PoolStandingGate. // - EscalateReserveCeiling: 12-month age check + Watcher attestation + // sets the pool's reserve target to CoverReserveCeilingAnnualContribX. // // Nil-shim behavior (simtest wiring): a nil StandingKeeper skips the D-077 // gate (the handler still mutates state — the simtest documents the wiring // contract); a nil WatcherKeeper skips the launch/charter/escalation // attestation; a nil StillKeeper skips the auto-Still recording (the pool's // PoolPaused flag is still set, just the Still event is not recorded in a // still store); a nil BondKeeper is the P1 default (the P4 handler will // reject a nil shim as a wiring error when the P4 MAB check is wired). import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/oy/openyield/x/cover/firewall" "github.com/oy/openyield/x/cover/types" ) // msgServer is the concrete MsgServer implementation wrapping the Keeper. type msgServer struct { Keeper } // NewMsgServerImpl returns the cover MsgServer for the provided Keeper. func NewMsgServerImpl(k Keeper) types.MsgServer { return &msgServer{Keeper: k} } var _ types.MsgServer = msgServer{} // unwrapCtx extracts the sdk.Context from the interface-typed ctx. func unwrapCtx(ctx interface{}) sdk.Context { if c, ok := ctx.(sdk.Context); ok { return c } panic(fmt.Sprintf("cover: expected sdk.Context, got %T", ctx)) } // gateForCategory returns the locked Standing gate floor for a Cover // category (D-077). HealthMCS demands the Preferred gate (4.5); Travel + // IncomePause use the Trusted gate (4.0) as the default. Other Phase2 // categories (none in P1) would also use the Trusted gate; the handler // rejects out-of-phase categories BEFORE reaching this helper (the D-086 // phase check runs first), so this helper is only called for in-phase // categories. func gateForCategory(cat types.CoverCategory) float64 { if cat == types.CatHealthMCS { return types.CoverStandingGatePreferred } return types.CoverStandingGateTrusted } // bucketMeetsGate reports whether a Standing bucket string + score meet the // locked gate floor (D-077). The bucket string is one of "New", "Trusted", // "Preferred", "Top", "Slashed" (cross-doc to x/standing.StandingBucket). // "Trusted" or higher ("Preferred", "Top") meets a Trusted gate; "Preferred" // or higher ("Top") meets a Preferred gate. The score is a secondary check // (defense in depth: the bucket is the primary gate, the score confirms). // "New" or "Slashed" never meets either gate. func bucketMeetsGate(bucket string, score float64, gate float64) bool { switch bucket { case "Top": return true case "Preferred": return gate <= types.CoverStandingGatePreferred && score >= gate case "Trusted": return gate <= types.CoverStandingGateTrusted && score >= gate } return false } // --- LaunchCoverPool ---------------------------------------------------------- // LaunchCoverPool launches a Cover Pool (REQ-046, REQ-047, REQ-049, D-077, // D-086). The handler enforces: // 1. ValidateBasic (stateless — floor check on ReserveAnnualContribRatio). // 2. Idempotency: pool-id must not already exist. // 3. D-086 category phase check: each category's phase must be in the // pool's FactoryAllowedPhases (P1 default = [Phase2] only — so only // Travel/HealthMCS/IncomePause allowed in P1; Phase3/Phase4 categories // REJECTED). // 4. D-090(3) dual gate check: the Params.PoolStandingGate >= the protocol // minimum (CoverStandingGateTrusted) — a pool may tighten the gate but // never lower it. // 5. D-077 Standing gate: for each category, query // StandingKeeper.GetStandingBucket(hostReachID, category). Compare the // returned bucket + score against the locked gate (Trusted for Travel/ // IncomePause; Preferred for HealthMCS). A nil StandingKeeper skips // the gate check (simtest wiring). // 6. Reserve floor re-check (REQ-047 defense in depth): // ReserveAnnualContribRatio >= CoverReserveFloorAnnualContribX. // 7. Watcher attestation (REQ-046): WatcherKeeper.Attest(poolID, payload). // A nil WatcherKeeper skips (simtest). // 8. Persist the CoverPool (PoolPaused = false, FactoryAllowedPhases + // PoolStandingGate from Params). // // On success an event is emitted. func (s msgServer) LaunchCoverPool(ctx interface{}, msg *types.MsgLaunchCoverPool) (*types.MsgLaunchCoverPoolResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: pool-id must not already exist. if _, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID); ok { return nil, fmt.Errorf("cover: pool %q already exists", msg.PoolID) } // Load the Params (the effective Params: the override if set, else // DefaultParams). The D-086 simtest case (f) uses the override to // restrict FactoryAllowedPhases to [Phase2, Phase3] only and reject a // Phase4 launch. A future P2+ will load the Params from the params // store; for now the keeper holds the override. params := s.Keeper.Params() if err := params.Validate(); err != nil { return nil, fmt.Errorf("cover: params invalid: %w", err) } // D-086 category phase check: each category's phase must be in the // FactoryAllowedPhases (P1 default = [Phase2] only). allowed := make(map[types.CoverCategoryPhase]bool, len(params.FactoryAllowedPhases)) for _, ph := range params.FactoryAllowedPhases { allowed[ph] = true } for _, cat := range msg.Categories { ph := types.CoverCategoryPhaseFor(cat) if ph == "" { return nil, fmt.Errorf("cover: unknown category %q (D-086 phase check)", cat) } if !allowed[ph] { return nil, fmt.Errorf("cover: category %q is phase %q, not in FactoryAllowedPhases %v (D-086: P1 allows %v only)", cat, ph, params.FactoryAllowedPhases, params.FactoryAllowedPhases) } } // D-077 Standing gate: for each category, query the host's Standing // bucket + score and compare against the locked gate. A nil // StandingKeeper skips the gate check (simtest wiring — documented). if s.Keeper.standingKeeper != nil { for _, cat := range msg.Categories { gate := gateForCategory(cat) bucket, score, err := s.Keeper.standingKeeper.GetStandingBucket(msg.HostReachID, string(cat)) if err != nil { return nil, fmt.Errorf("cover: Standing lookup for host %q category %q: %w (D-077 gate)", msg.HostReachID, cat, err) } if !bucketMeetsGate(bucket, score, gate) { return nil, fmt.Errorf("cover: host %q Standing bucket %q score %.2f for category %q does not meet the locked gate %.2f (D-077)", msg.HostReachID, bucket, score, cat, gate) } } } // Reserve floor re-check (defense in depth — ValidateBasic already // checked this statelessly). if msg.ReserveAnnualContribRatio < types.CoverReserveFloorAnnualContribX { return nil, fmt.Errorf("cover: ReserveAnnualContribRatio %.2f < floor %.2f (REQ-047 handler re-check)", msg.ReserveAnnualContribRatio, types.CoverReserveFloorAnnualContribX) } // Watcher attestation (REQ-046). A nil WatcherKeeper skips (simtest). if s.Keeper.watcherKeeper != nil { payload := []byte(fmt.Sprintf("cover.launch:%s:%s:%v:%.2f", msg.PoolID, msg.HostReachID, msg.Categories, msg.ReserveAnnualContribRatio)) if _, err := s.Keeper.watcherKeeper.Attest(msg.PoolID, payload); err != nil { return nil, fmt.Errorf("cover: Watcher attestation for pool %q: %w (REQ-046)", msg.PoolID, err) } } pool := types.CoverPool{ PoolID: msg.PoolID, HostReachID: msg.HostReachID, Categories: msg.Categories, ReserveAnnualContribRatio: msg.ReserveAnnualContribRatio, ReserveAccount: msg.ReserveAccount, PoolPaused: false, CharterHash: msg.CharterHash, FactoryAllowedPhases: params.FactoryAllowedPhases, PoolStandingGate: params.PoolStandingGate, CreatedAt: sdkCtx.BlockTime().Unix(), } s.Keeper.SetCoverPool(sdkCtx, pool) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pool_launched", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("host_reach_id", msg.HostReachID), sdk.NewAttribute("reserve_annual_contrib_ratio", fmt.Sprintf("%.2f", msg.ReserveAnnualContribRatio)), )) return &types.MsgLaunchCoverPoolResponse{}, nil } // --- RouteCoverFee ------------------------------------------------------------ // RouteCoverFee routes a Cover-Fee into a pool's reserve (REQ-050, D-079 // firewall, REQ-047 below-floor auto-pause). The handler enforces: // 1. ValidateBasic (stateless). // 2. Load the CoverPool. If not found, REJECT. // 3. Below-floor pause check (REQ-047): if pool.PoolPaused == true, REJECT // with "pool paused (below reserve floor)". // 4. D-079 Anti-Crowding-Out firewall: call // firewall.CheckCoverFeeRouting(pool.ReserveAccount). If the firewall // rejects (the destination is NOT permitted — e.g. the pool's // ReserveAccount is the Root-Pool operating-expenses holder), REJECT. // 5. Category-tag validation (REQ-050, FR-COVER-11): the CategoryTag must // match one of the Pool's Categories. Mismatch -> REJECT. // 6. Reserve floor check (REQ-047): if pool.ReserveAnnualContribRatio < // floor, REJECT the routing AND set pool.PoolPaused = true (auto-pause) // AND invoke StillKeeper.Still(poolID, "below reserve floor") (D-089(1) // — nil StillKeeper skips). Persist the paused pool. Emit // cover.pool_below_floor. // 7. Otherwise: emit cover.cover_fee_routed (the routing is the event; the // reserve balance update is a simtest-grade stub). func (s msgServer) RouteCoverFee(ctx interface{}, msg *types.MsgRouteCoverFee) (*types.MsgRouteCoverFeeResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (RouteCoverFee rejected)", msg.PoolID) } // Below-floor pause check: a paused pool rejects all routing. if pool.PoolPaused { return nil, fmt.Errorf("cover: pool %q paused (below reserve floor) — routing rejected", msg.PoolID) } // D-079 Anti-Crowding-Out firewall: the destination (the pool's // ReserveAccount) must be a permitted routing destination. The firewall // is the second-layer defense (the first layer is the handler's own // destination-match check — the destination IS pool.ReserveAccount by // construction; the firewall catches a pool misconfigured to route to // the Root-Pool operating-expenses holder). if err := firewall.CheckCoverFeeRouting(pool.ReserveAccount); err != nil { return nil, fmt.Errorf("cover: %w (pool %q ReserveAccount %q)", err, msg.PoolID, pool.ReserveAccount) } // Category-tag validation (REQ-050, FR-COVER-11): the CategoryTag must // match one of the Pool's Categories. tagMatched := false for _, cat := range pool.Categories { if string(cat) == msg.CategoryTag { tagMatched = true break } } if !tagMatched { return nil, fmt.Errorf("cover: CategoryTag %q does not match any of pool %q categories %v (REQ-050)", msg.CategoryTag, msg.PoolID, pool.Categories) } // Reserve floor check (REQ-047): if the pool's ReserveAnnualContribRatio // is below the floor, REJECT the routing AND auto-pause the pool AND // invoke StillKeeper.Still (D-089(1)). A nil StillKeeper skips the // Still recording (the pool's PoolPaused flag is still set). if pool.ReserveAnnualContribRatio < types.CoverReserveFloorAnnualContribX { pool.PoolPaused = true s.Keeper.SetCoverPool(sdkCtx, pool) if s.Keeper.stillKeeper != nil { if err := s.Keeper.stillKeeper.Still(msg.PoolID, "below reserve floor"); err != nil { return nil, fmt.Errorf("cover: Still invocation for pool %q (below reserve floor): %w (D-089(1))", msg.PoolID, err) } } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pool_below_floor", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("reserve_annual_contrib_ratio", fmt.Sprintf("%.2f", pool.ReserveAnnualContribRatio)), sdk.NewAttribute("floor", fmt.Sprintf("%.2f", types.CoverReserveFloorAnnualContribX)), )) return nil, fmt.Errorf("cover: pool %q below reserve floor (%.2f < %.2f) — routing rejected, pool auto-paused (REQ-047)", msg.PoolID, pool.ReserveAnnualContribRatio, types.CoverReserveFloorAnnualContribX) } // Success: the routing is the event (the reserve balance update is a // simtest-grade stub — P2 may add a CoverFeeRouting record). sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.cover_fee_routed", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("category_tag", msg.CategoryTag), sdk.NewAttribute("grain_amount", fmt.Sprintf("%d", msg.GrainAmount)), sdk.NewAttribute("reserve_account", pool.ReserveAccount), )) return &types.MsgRouteCoverFeeResponse{}, nil } // --- FileCoverCall ------------------------------------------------------------ // FileCoverCall files a Cover Call against a pool's category (REQ-055 P1 // scaffold — the Voucher adjudication lands in P4). The handler enforces: // 1. ValidateBasic (stateless). // 2. Load the CoverPool. If not found, REJECT. // 3. The category must match one of the Pool's Categories. // 4. Persist the CoverCall. Emit cover.cover_call_filed. // // P4 adds: the Voucher assignment + no-self-adjudication (the // ClaimantReachID must not be the adjudicating Voucher) + the MAB misuse // auto-Still (D-089(1) — a Voucher whose MAB is slashed triggers the // StillKeeper). func (s msgServer) FileCoverCall(ctx interface{}, msg *types.MsgFileCoverCall) (*types.MsgFileCoverCallResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (FileCoverCall rejected)", msg.PoolID) } // The category must match one of the Pool's Categories. catMatched := false for _, cat := range pool.Categories { if cat == msg.Category { catMatched = true break } } if !catMatched { return nil, fmt.Errorf("cover: category %q does not match any of pool %q categories %v", msg.Category, msg.PoolID, pool.Categories) } call := types.CoverCall{ CallID: msg.CallID, PoolID: msg.PoolID, ClaimantReachID: msg.ClaimantReachID, Category: msg.Category, AmountGrain: msg.AmountGrain, FiledAt: sdkCtx.BlockHeight(), } s.Keeper.SetCoverCall(sdkCtx, call) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.cover_call_filed", sdk.NewAttribute("call_id", msg.CallID), sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("claimant_reach_id", msg.ClaimantReachID), sdk.NewAttribute("category", string(msg.Category)), sdk.NewAttribute("amount_grain", fmt.Sprintf("%d", msg.AmountGrain)), )) return &types.MsgFileCoverCallResponse{}, nil } // --- P2: SignCoverCharter ----------------------------------------------------- // SignCoverCharter signs a Cover-Charter for a Pool (REQ-052, REQ-056, // D-090(1)). The handler enforces: // 1. ValidateBasic (stateless — includes the D-090(1) Bill of Rights // gate: any WaivedRights element REJECTS the signing). // 2. Idempotency: CharterID must not already exist. // 3. The referenced Pool must exist (the charter binds to a pool). // 4. WatcherKeeper.Attest on the charter witness hash (a nil WatcherKeeper // skips; an empty WatcherWitnessHash skips). // 5. Persist the CoverCharter + link the pool's CharterRef. // 6. Emit cover.charter_signed. func (s msgServer) SignCoverCharter(ctx interface{}, msg *types.MsgSignCoverCharter) (*types.MsgSignCoverCharterResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: charter-id must not already exist. if _, ok := s.Keeper.GetCoverCharter(sdkCtx, msg.CharterID); ok { return nil, fmt.Errorf("cover: charter %q already exists", msg.CharterID) } // The referenced pool must exist (the charter binds to a pool). pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (SignCoverCharter rejected)", msg.PoolID) } // Watcher attestation over the witness hash (REQ-052). A nil // WatcherKeeper skips; an empty WatcherWitnessHash skips (the charter // may be signed without a witness in simtest). if s.Keeper.watcherKeeper != nil && len(msg.WatcherWitnessHash) > 0 { if _, err := s.Keeper.watcherKeeper.Attest(msg.PoolID, msg.WatcherWitnessHash); err != nil { return nil, fmt.Errorf("cover: Watcher attestation for charter %q: %w (REQ-052)", msg.CharterID, err) } } charter := types.CoverCharter{ CharterID: msg.CharterID, PoolID: msg.PoolID, StatementOfBeliefsHash: msg.StatementOfBeliefsHash, DisputePath: msg.DisputePath, Gate: msg.Gate, HoldingPeriodDays: msg.HoldingPeriodDays, HostReachID: msg.HostReachID, WatcherWitnessHash: msg.WatcherWitnessHash, Amendments: []types.CharterAmendment{}, WaivedRights: msg.WaivedRights, } s.Keeper.SetCoverCharter(sdkCtx, charter) // Link the pool's CharterRef. pool.CharterRef = msg.CharterID s.Keeper.SetCoverPool(sdkCtx, pool) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.charter_signed", sdk.NewAttribute("charter_id", msg.CharterID), sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("host_reach_id", msg.HostReachID), )) return &types.MsgSignCoverCharterResponse{}, nil } // --- P2: AmendCoverCharter ---------------------------------------------------- // AmendCoverCharter files a Charter amendment (REQ-052). The handler // enforces: // 1. ValidateBasic (stateless). // 2. The referenced charter must exist. // 3. Create a CharterAmendment with Status=AmendmentProposed, // ProposedAt=now. Persist the amendment + append to the charter's // Amendments slice. // 4. Emit cover.charter_amend_proposed. // // The 7-day cooling is enforced by CoolCharterAmendment / // RatifyCharterAmendment (keeper helpers) — a simtest time-advance or a // separate handler transitions the amendment to Cooled then Ratified. func (s msgServer) AmendCoverCharter(ctx interface{}, msg *types.MsgAmendCoverCharter) (*types.MsgAmendCoverCharterResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) charter, ok := s.Keeper.GetCoverCharter(sdkCtx, msg.CharterID) if !ok { return nil, fmt.Errorf("cover: charter %q not found (AmendCoverCharter rejected)", msg.CharterID) } // Idempotency: amendment-id must not already exist. if _, ok := s.Keeper.GetCharterAmendment(sdkCtx, msg.AmendmentID); ok { return nil, fmt.Errorf("cover: amendment %q already exists", msg.AmendmentID) } amendment := types.CharterAmendment{ AmendmentID: msg.AmendmentID, Description: msg.Description, Status: types.AmendmentProposed, ProposedAt: sdkCtx.BlockTime().Unix(), } s.Keeper.SetCharterAmendment(sdkCtx, amendment) // Append the amendment to the charter's Amendments slice + persist. charter.Amendments = append(charter.Amendments, amendment) s.Keeper.SetCoverCharter(sdkCtx, charter) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.charter_amend_proposed", sdk.NewAttribute("charter_id", msg.CharterID), sdk.NewAttribute("amendment_id", msg.AmendmentID), )) return &types.MsgAmendCoverCharterResponse{}, nil } // --- P2: ElectPoolMason ------------------------------------------------------- // ElectPoolMason elects a Mason to the Pool Council (REQ-062). The // handler enforces: // 1. ValidateBasic (stateless). // 2. The referenced pool must exist. // 3. Load or create the PoolCouncil. Add the MasonReachID to // ElectedMasonReachIDs (max PoolCouncilMaxMasons = 3 — a 4th is // REJECTED). Reject a duplicate MasonReachID (already elected). // 4. Persist the PoolCouncil + link the pool's CouncilRef. // 5. Emit cover.pool_mason_elected. func (s msgServer) ElectPoolMason(ctx interface{}, msg *types.MsgElectPoolMason) (*types.MsgElectPoolMasonResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (ElectPoolMason rejected)", msg.PoolID) } council, exists := s.Keeper.GetPoolCouncil(sdkCtx, msg.PoolID) if !exists { council = types.PoolCouncil{ PoolID: msg.PoolID, HostReachID: pool.HostReachID, ElectedMasonReachIDs: [3]string{}, } } // Reject a duplicate MasonReachID (already elected). for _, m := range council.ElectedMasonReachIDs { if m == msg.MasonReachID { return nil, fmt.Errorf("cover: mason %q already elected to pool %q council (REQ-062)", msg.MasonReachID, msg.PoolID) } } // Find the first empty slot; if all 3 are filled, REJECT (max // PoolCouncilMaxMasons). slotIdx := -1 for i, m := range council.ElectedMasonReachIDs { if m == "" { slotIdx = i break } } if slotIdx == -1 { return nil, fmt.Errorf("cover: pool %q council already has %d masons (REQ-062 max %d)", msg.PoolID, types.PoolCouncilMaxMasons, types.PoolCouncilMaxMasons) } council.ElectedMasonReachIDs[slotIdx] = msg.MasonReachID s.Keeper.SetPoolCouncil(sdkCtx, council) // Link the pool's CouncilRef (the council is keyed by pool-id, so the // ref is the pool-id itself). pool.CouncilRef = msg.PoolID s.Keeper.SetCoverPool(sdkCtx, pool) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pool_mason_elected", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("mason_reach_id", msg.MasonReachID), sdk.NewAttribute("slot", fmt.Sprintf("%d", slotIdx)), )) return &types.MsgElectPoolMasonResponse{}, nil } // --- P2: VoteCoverCall -------------------------------------------------------- // VoteCoverCall votes on a Cover Call (REQ-062). The handler enforces: // 1. ValidateBasic (stateless — includes the valid VoteOption check). // 2. The referenced CoverCall must exist. // 3. The Watcher-observer-present check: if VoteOption == CallVoteYes and // WatcherObserverPresent == false, REJECT (majority requires observer // present — REQ-062). // 4. Idempotency: VoteID must not already exist. // 5. Persist the CoverCallVote. Emit cover.cover_call_voted. func (s msgServer) VoteCoverCall(ctx interface{}, msg *types.MsgVoteCoverCall) (*types.MsgVoteCoverCallResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // The referenced CoverCall must exist. if _, ok := s.Keeper.GetCoverCall(sdkCtx, msg.CallID); !ok { return nil, fmt.Errorf("cover: call %q not found (VoteCoverCall rejected)", msg.CallID) } // The Watcher-observer-present check (REQ-062): a CallVoteYes requires // the Watcher observer to be present. A CallVoteNo / CallVoteAbstain // does NOT require the observer (only an affirmative vote demands the // witness). if msg.VoteOption == types.CallVoteYes && !msg.WatcherObserverPresent { return nil, fmt.Errorf("cover: CallVoteYes on call %q requires Watcher observer present (REQ-062)", msg.CallID) } // Idempotency: vote-id must not already exist. if _, ok := s.Keeper.GetCoverCallVote(sdkCtx, msg.VoteID); ok { return nil, fmt.Errorf("cover: vote %q already exists", msg.VoteID) } vote := types.CoverCallVote{ VoteID: msg.VoteID, CallID: msg.CallID, PoolID: msg.PoolID, VoterReachID: msg.VoterReachID, VoteOption: msg.VoteOption, WatcherObserverPresent: msg.WatcherObserverPresent, VotedAt: sdkCtx.BlockTime().Unix(), } s.Keeper.SetCoverCallVote(sdkCtx, vote) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.cover_call_voted", sdk.NewAttribute("vote_id", msg.VoteID), sdk.NewAttribute("call_id", msg.CallID), sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("voter_reach_id", msg.VoterReachID), sdk.NewAttribute("vote_option", string(msg.VoteOption)), )) return &types.MsgVoteCoverCallResponse{}, nil } // --- P2: AmendPoolStandingGate ------------------------------------------------ // AmendPoolStandingGate amends a Pool's Standing gate (D-090(3)). The // handler enforces: // 1. ValidateBasic (stateless — includes the D-090(3) dual check: // NewGate >= CoverStandingGateTrusted). // 2. The referenced pool must exist. // 3. D-090(3) handler re-check (defense in depth): NewGate >= // CoverStandingGateTrusted. ValidateBasic already checked, but the // handler re-checks in case of a future Params-bypass. // 4. Update the pool's PoolStandingGate. Persist. // 5. Emit cover.pool_standing_gate_amended. func (s msgServer) AmendPoolStandingGate(ctx interface{}, msg *types.MsgAmendPoolStandingGate) (*types.MsgAmendPoolStandingGateResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (AmendPoolStandingGate rejected)", msg.PoolID) } // D-090(3) handler re-check (defense in depth — ValidateBasic already // checked, but the handler re-checks in case of a future Params-bypass). if msg.NewGate < types.CoverStandingGateTrusted { return nil, fmt.Errorf("cover: NewGate %.2f < CoverStandingGateTrusted %.2f (D-090(3) handler re-check: a pool may tighten the gate but never lower it)", msg.NewGate, types.CoverStandingGateTrusted) } pool.PoolStandingGate = msg.NewGate s.Keeper.SetCoverPool(sdkCtx, pool) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pool_standing_gate_amended", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("new_gate", fmt.Sprintf("%.2f", msg.NewGate)), )) return &types.MsgAmendPoolStandingGateResponse{}, nil } // --- P2: EscalateReserveCeiling ----------------------------------------------- // EscalateReserveCeiling escalates a Pool's reserve target to the // CoverReserveCeilingAnnualContribX (REQ-048). The handler enforces: // 1. ValidateBasic (stateless). // 2. The referenced pool must exist. // 3. 12-month age check: now - pool.CreatedAt >= ReserveCeilingAgeSeconds // (365 days). A fresh pool is REJECTED. NOTE: pool.CreatedAt is set to // sdkCtx.BlockHeight() at launch in P1; for the age check we use // BlockTime().Unix() - pool.CreatedAt where pool.CreatedAt is // interpreted as a unix timestamp (the simtest sets CreatedAt to a // unix timestamp to satisfy this check). // 4. Set the pool's ReserveAnnualContribRatio to // CoverReserveCeilingAnnualContribX (2.5). // 5. WatcherKeeper.Attest (a nil WatcherKeeper skips). // 6. Persist the updated pool. Emit cover.reserve_ceiling_escalated. func (s msgServer) EscalateReserveCeiling(ctx interface{}, msg *types.MsgEscalateReserveCeiling) (*types.MsgEscalateReserveCeilingResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (EscalateReserveCeiling rejected)", msg.PoolID) } // 12-month age check (REQ-048): the pool must have >= 365 days of // operating history before the reserve target can be escalated to the // ceiling. pool.CreatedAt is interpreted as a unix timestamp (the // simtest sets it accordingly). now := sdkCtx.BlockTime().Unix() if now-pool.CreatedAt < types.ReserveCeilingAgeSeconds { return nil, fmt.Errorf("cover: pool %q age %d seconds < %d seconds (REQ-048: 12-month operating history required for reserve ceiling escalation)", msg.PoolID, now-pool.CreatedAt, types.ReserveCeilingAgeSeconds) } // Set the pool's reserve target to the ceiling. pool.ReserveAnnualContribRatio = types.CoverReserveCeilingAnnualContribX // Watcher attestation (REQ-048). A nil WatcherKeeper skips. if s.Keeper.watcherKeeper != nil { payload := []byte(fmt.Sprintf("cover.escalate:%s:%.2f", msg.PoolID, types.CoverReserveCeilingAnnualContribX)) if _, err := s.Keeper.watcherKeeper.Attest(msg.PoolID, payload); err != nil { return nil, fmt.Errorf("cover: Watcher attestation for reserve ceiling escalation on pool %q: %w (REQ-048)", msg.PoolID, err) } } s.Keeper.SetCoverPool(sdkCtx, pool) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.reserve_ceiling_escalated", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("reserve_annual_contrib_ratio", fmt.Sprintf("%.2f", types.CoverReserveCeilingAnnualContribX)), )) return &types.MsgEscalateReserveCeilingResponse{}, nil } // --- v0.7 P4: Voucher + Dissolution handlers (REQ-055, REQ-063, D-090(2)) ------ // // (Cover Claims Voucher registration + Cover Call adjudication + Voucher // slash + Pool dissolution waterfall). The four handlers exercise the // D-090(2) cold-start bond fallback, the FR-CPCV-2 no-self-adjudication // gate, the cross-Pool slash via StandingKeeper.RecordSlash, and the // FR-MAB-4 seniority chain (Cover-Fee contributors > MAB > Bread holders). // RegisterCoverClaimsVoucher registers a Cover Claims Voucher for a Pool // (REQ-055, D-090(2)). The handler enforces: // 1. ValidateBasic (stateless). // 2. The referenced Pool must exist. // 3. Idempotency: no duplicate Voucher for the same VoucherReachID + // PoolID (a Voucher is registered per-Pool; a second registration for // the same composite key is REJECTED). // 4. Compute bond: max(CoverClaimsVoucherBondMultipleAvgCall × // GetAvgCallSize(poolID), Params.MinimumVoucherBond). D-090(2) cold- // start: when no Calls exist, GetAvgCallSize returns 0 -> bond = // MinimumVoucherBond (NOT zero). // 5. Persist the Voucher + emit cover.voucher_registered. func (s msgServer) RegisterCoverClaimsVoucher(ctx interface{}, msg *types.MsgRegisterCoverClaimsVoucher) (*types.MsgRegisterCoverClaimsVoucherResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // The referenced Pool must exist. if _, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID); !ok { return nil, fmt.Errorf("cover: pool %q not found (RegisterCoverClaimsVoucher rejected)", msg.PoolID) } // Idempotency: no duplicate Voucher for the same VoucherReachID + PoolID. if _, ok := s.Keeper.GetCoverClaimsVoucher(sdkCtx, msg.VoucherReachID, msg.PoolID); ok { return nil, fmt.Errorf("cover: voucher %q already registered for pool %q (RegisterCoverClaimsVoucher rejected)", msg.VoucherReachID, msg.PoolID) } // D-090(2) bond computation: max(multiple × avgCallSize, // MinimumVoucherBond). When no Calls exist, avgCallSize = 0 -> bond = // MinimumVoucherBond (NOT zero — the cold-start fix). avgCallSize := s.Keeper.GetAvgCallSize(sdkCtx, msg.PoolID) multipleBond := int64(types.CoverClaimsVoucherBondMultipleAvgCall) * avgCallSize minBond := s.Keeper.Params().MinimumVoucherBond bond := multipleBond if bond < minBond { bond = minBond } v := types.CoverClaimsVoucher{ VoucherReachID: msg.VoucherReachID, PoolID: msg.PoolID, BondAmount: bond, BondMultipleAvgCall: types.CoverClaimsVoucherBondMultipleAvgCall, RegisteredAt: sdkCtx.BlockTime().Unix(), } s.Keeper.SetCoverClaimsVoucher(sdkCtx, v) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.voucher_registered", sdk.NewAttribute("voucher_reach_id", msg.VoucherReachID), sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("bond_amount", fmt.Sprintf("%d", bond)), sdk.NewAttribute("avg_call_size", fmt.Sprintf("%d", avgCallSize)), )) return &types.MsgRegisterCoverClaimsVoucherResponse{BondAmount: bond}, nil } // AdjudicateCoverCall adjudicates a Cover Call (REQ-055, FR-CPCV-2). The // handler enforces: // 1. ValidateBasic (stateless). // 2. The CoverCall must exist. // 3. FR-CPCV-2 no self-adjudication: reject if VoucherReachID == // CoverCall.ClaimantReachID (the Voucher cannot adjudicate their own // Call). // 4. The Voucher must be registered for the Call's Pool. // 5. Record the adjudication result on the CoverCall (AdjudicationResult + // AdjudicatedBy + AdjudicatedAt). Persist. Emit // cover.cover_call_adjudicated. func (s msgServer) AdjudicateCoverCall(ctx interface{}, msg *types.MsgAdjudicateCoverCall) (*types.MsgAdjudicateCoverCallResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) call, ok := s.Keeper.GetCoverCall(sdkCtx, msg.CallID) if !ok { return nil, fmt.Errorf("cover: call %q not found (AdjudicateCoverCall rejected)", msg.CallID) } // FR-CPCV-2 no self-adjudication: the Voucher cannot adjudicate their // own Call. if msg.VoucherReachID == call.ClaimantReachID { return nil, fmt.Errorf("cover: FR-CPCV-2 no self-adjudication — voucher %q == call %q claimant %q (AdjudicateCoverCall rejected)", msg.VoucherReachID, msg.CallID, call.ClaimantReachID) } // The Voucher must be registered for the Call's Pool. if _, ok := s.Keeper.GetCoverClaimsVoucher(sdkCtx, msg.VoucherReachID, call.PoolID); !ok { return nil, fmt.Errorf("cover: voucher %q not registered for pool %q (AdjudicateCoverCall rejected)", msg.VoucherReachID, call.PoolID) } // Record the adjudication result on the CoverCall (additive fields). call.AdjudicationResult = msg.AdjudicationResult call.AdjudicatedBy = msg.VoucherReachID call.AdjudicatedAt = sdkCtx.BlockTime().Unix() s.Keeper.SetCoverCall(sdkCtx, call) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.cover_call_adjudicated", sdk.NewAttribute("call_id", msg.CallID), sdk.NewAttribute("pool_id", call.PoolID), sdk.NewAttribute("voucher_reach_id", msg.VoucherReachID), sdk.NewAttribute("adjudication_result", msg.AdjudicationResult), )) return &types.MsgAdjudicateCoverCallResponse{}, nil } // SlashCoverClaimsVoucher slashes a Cover Claims Voucher for a fraudulent // Cover Call adjudication (REQ-055). The handler enforces: // 1. ValidateBasic (stateless — Reason must == SlashReasonFraudulentCoverCall). // 2. The Voucher must exist (look up by VoucherReachID across all Pools — // a Voucher may be registered for multiple Pools; the slash drops the // Standing bucket, which is cross-Pool). // 3. Invoke StandingKeeper.RecordSlash(voucherReachID, amount, reason, // attester) — the slash drops the Voucher's Standing bucket (cross-Pool // applicability — the bucket drop disqualifies them from other Pools' // Standing gates). A nil StandingKeeper is a wiring error -> REJECT. // 4. Emit cover.voucher_slashed. func (s msgServer) SlashCoverClaimsVoucher(ctx interface{}, msg *types.MsgSlashCoverClaimsVoucher) (*types.MsgSlashCoverClaimsVoucherResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // The Voucher must exist (look up by VoucherReachID across all Pools). vouchers := s.Keeper.AllCoverClaimsVouchers(sdkCtx) var found *types.CoverClaimsVoucher for i := range vouchers { if vouchers[i].VoucherReachID == msg.VoucherReachID { found = &vouchers[i] break } } if found == nil { return nil, fmt.Errorf("cover: voucher %q not found (SlashCoverClaimsVoucher rejected)", msg.VoucherReachID) } // StandingKeeper.RecordSlash — the slash drops the Voucher's Standing // bucket (cross-Pool applicability). A nil StandingKeeper is a wiring // error -> REJECT (the slash cannot be recorded). if s.Keeper.standingKeeper == nil { return nil, fmt.Errorf("cover: StandingKeeper shim not wired (SlashCoverClaimsVoucher cannot record the slash — REQ-055 cross-Pool applicability)") } if err := s.Keeper.standingKeeper.RecordSlash(msg.VoucherReachID, float64(found.BondAmount), msg.Reason, msg.Signer); err != nil { return nil, fmt.Errorf("cover: StandingKeeper.RecordSlash for voucher %q: %w (REQ-055)", msg.VoucherReachID, err) } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.voucher_slashed", sdk.NewAttribute("voucher_reach_id", msg.VoucherReachID), sdk.NewAttribute("call_id", msg.CallID), sdk.NewAttribute("reason", msg.Reason), sdk.NewAttribute("bond_amount", fmt.Sprintf("%d", found.BondAmount)), )) return &types.MsgSlashCoverClaimsVoucherResponse{}, nil } // DissolveCoverPool dissolves a Cover Pool (REQ-063, FR-MAB-4). The handler // enforces: // 1. ValidateBasic (stateless). // 2. The Pool must exist. // 3. Compute the PoolDissolutionWaterfall (FR-MAB-4 seniority chain): // Tier 1 = Cover-Fee contributors (the Pool's reserve — a simtest-grade // placeholder amount; the real reserve balance is a v0.8+ concern), // Tier 2 = MAB holders (query BondKeeper.GetMABsForPool for the Pool's // outstanding MABs; sum the PrincipalGrain), Tier 3 = Bread holders // (the remainder — simtest-grade placeholder). MAB holders have NO // Voice in the dissolution decision (REQ-063 — the PoolCouncil from P2 // already excludes them; the waterfall only determines the payout // order). // 4. Emit cover.pool_dissolved with the waterfall tiers. func (s msgServer) DissolveCoverPool(ctx interface{}, msg *types.MsgDissolveCoverPool) (*types.MsgDissolveCoverPoolResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID) if !ok { return nil, fmt.Errorf("cover: pool %q not found (DissolveCoverPool rejected)", msg.PoolID) } // FR-MAB-4 waterfall. Tier 1 = Cover-Fee contributors (the Pool's // reserve — simtest-grade placeholder; the real reserve balance is a // v0.8+ concern, so we use a deterministic placeholder derived from // the pool's ReserveAnnualContribRatio for the simtest assertion). coverFeeContributors := int64(pool.ReserveAnnualContribRatio * 1_000_000) // Tier 2 = MAB holders (sum the outstanding MAB principal via // BondKeeper.GetMABsForPool). A nil BondKeeper returns an empty slice // -> Tier 2 amount = 0. mabHolders := int64(0) if s.Keeper.bondKeeper != nil { for _, m := range s.Keeper.bondKeeper.GetMABsForPool(msg.PoolID) { mabHolders += m.PrincipalGrain } } // Tier 3 = Bread holders (the remainder — simtest-grade placeholder; // the real Bread-holder balance is a v0.8+ concern, so we use a // deterministic placeholder for the simtest assertion). breadHolders := coverFeeContributors / 4 waterfall := []types.PoolDissolutionWaterfall{ {Tier: types.PoolDissolutionWaterfallTierCoverFeeContributors, AmountGrain: coverFeeContributors}, {Tier: types.PoolDissolutionWaterfallTierMABHolders, AmountGrain: mabHolders}, {Tier: types.PoolDissolutionWaterfallTierBreadHolders, AmountGrain: breadHolders}, } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pool_dissolved", sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("tier_1_cover_fee_contributors", fmt.Sprintf("%d", coverFeeContributors)), sdk.NewAttribute("tier_2_mab_holders", fmt.Sprintf("%d", mabHolders)), sdk.NewAttribute("tier_3_bread_holders", fmt.Sprintf("%d", breadHolders)), )) return &types.MsgDissolveCoverPoolResponse{Waterfall: waterfall}, nil } // --- v0.7 P5: Bill of Rights ceremony + Pier Selection handlers (REQ-056, REQ-066) -- // // (REQ-056 §7 acceptance ceremony, REQ-066 Pier Selection.) The three // handlers exercise the bonded-Counsel review gate, the Pier selection // persistence + index accumulation, and the Cover-Pool-supermajority + // Counsel-witness revocation gate. // CounselReviewBillOfRights records a bonded Counsel's review of the // Anti-Capture Bill of Rights (REQ-056, vision §7, §8.2). The handler // enforces: // 1. ValidateBasic (stateless — includes the §7 "bonded Counsel" gate: // Staked must be true). // 2. Idempotency: ReviewID must not already exist. // 3. Handler re-check of Staked (defense in depth — the §7 acceptance is // load-bearing; the handler re-checks in case of a future // ValidateBasic-bypass). // 4. Persist the BillOfRightsReview (the bill_review/ store: ReviewID -> // review record). Emit cover.bill_of_rights_reviewed. func (s msgServer) CounselReviewBillOfRights(ctx interface{}, msg *types.MsgCounselReviewBillOfRights) (*types.MsgCounselReviewBillOfRightsResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: review-id must not already exist. if _, ok := s.Keeper.GetBillOfRightsReview(sdkCtx, msg.ReviewID); ok { return nil, fmt.Errorf("cover: bill-of-rights review %q already exists", msg.ReviewID) } // Handler re-check of Staked (defense in depth — the §7 "bonded Counsel" // acceptance is load-bearing; re-check in case of a future // ValidateBasic-bypass). if !msg.Staked { return nil, fmt.Errorf("cover: REQ-056 §7 acceptance: handler re-check — Staked must be true (bonded Counsel)") } review := BillOfRightsReview{ ReviewID: msg.ReviewID, CounselReachID: msg.CounselReachID, Staked: msg.Staked, ReviewResult: msg.ReviewResult, ReviewedAt: sdkCtx.BlockTime().Unix(), } s.Keeper.SetBillOfRightsReview(sdkCtx, review) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.bill_of_rights_reviewed", sdk.NewAttribute("review_id", msg.ReviewID), sdk.NewAttribute("counsel_reach_id", msg.CounselReachID), sdk.NewAttribute("staked", fmt.Sprintf("%v", msg.Staked)), sdk.NewAttribute("review_result", msg.ReviewResult), )) return &types.MsgCounselReviewBillOfRightsResponse{}, nil } // SelectPier records a Guild Council's selection of a Pier at formation // (REQ-066). The handler enforces: // 1. ValidateBasic (stateless). // 2. Idempotency: a PierSelectionRecord for the same GuildID must not // already exist (a Guild selects exactly one Pier; a second selection // is REJECTED — use MsgRevokePierSelection first). // 3. Guild existence check via the GuildKeeper shim (a nil shim skips — // simtest wiring; a non-nil shim with exists=false REJECTS). // 4. Persist the PierSelectionRecord (pier_selection/ store: GuildID -> // record). Create or update the PierSelectionIndex entry for the // PierID (pier_index/ store: PierID -> index — accumulate scores from // successive selections). Emit cover.pier_selected. func (s msgServer) SelectPier(ctx interface{}, msg *types.MsgSelectPier) (*types.MsgSelectPierResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: a Guild selects exactly one Pier; a second selection is // REJECTED (use MsgRevokePierSelection first to re-select). if _, ok := s.Keeper.GetPierSelectionRecord(sdkCtx, msg.GuildID); ok { return nil, fmt.Errorf("cover: guild %q already has a Pier selection (use RevokePierSelection first to re-select — REQ-066)", msg.GuildID) } // Guild existence check via the GuildKeeper shim (a nil shim skips — // simtest wiring; a non-nil shim with exists=false REJECTS). if s.Keeper.guildKeeper != nil { if !s.Keeper.guildKeeper.GetGuild(msg.GuildID) { return nil, fmt.Errorf("cover: guild %q not found (SelectPier rejected — REQ-066)", msg.GuildID) } } record := types.PierSelectionRecord{ GuildID: msg.GuildID, PierID: msg.PierID, SelectedAt: sdkCtx.BlockTime().Unix(), SelectedBy: msg.Signer, } s.Keeper.SetPierSelectionRecord(sdkCtx, record) // Create or update the PierSelectionIndex entry for the PierID // (accumulate scores from successive selections — a fresh entry gets // the default scores; an existing entry keeps its scores but the // OverallScore is recomputed for non-decreasing assertion). The live // mesh oracle is a v0.8+ concern; the simtest uses deterministic // defaults so the index is non-empty on first selection. idx, ok := s.Keeper.GetPierSelectionIndex(sdkCtx, msg.PierID) if !ok { idx = types.PierSelectionIndex{ PierID: msg.PierID, JurisdictionalReliabilityScore: DefaultPierJurisdictionalReliabilityScore, IntegrationQualityScore: DefaultPierIntegrationQualityScore, } } idx.OverallScore = DefaultPierOverallScore(idx.JurisdictionalReliabilityScore, idx.IntegrationQualityScore, idx.FiduciaryRecordHash) s.Keeper.SetPierSelectionIndex(sdkCtx, idx) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pier_selected", sdk.NewAttribute("guild_id", msg.GuildID), sdk.NewAttribute("pier_id", msg.PierID), sdk.NewAttribute("selected_by", msg.Signer), sdk.NewAttribute("overall_score", fmt.Sprintf("%.4f", idx.OverallScore)), )) return &types.MsgSelectPierResponse{}, nil } // RevokePierSelection revokes a Guild's Pier selection (REQ-066). The // revocation is reversible by a Cover Pool supermajority + a Counsel // witness. The handler enforces: // 1. ValidateBasic (stateless). // 2. The PierSelectionRecord for the GuildID must exist (nothing to // revoke -> REJECT). // 3. Authorization: RevocationApproved must be true + CounselWitness must // be non-empty (the Cover Pool supermajority + Counsel witness gate — // a revocation without either is REJECTED). // 4. Remove the PierSelectionRecord. Emit cover.pier_selection_revoked. func (s msgServer) RevokePierSelection(ctx interface{}, msg *types.MsgRevokePierSelection) (*types.MsgRevokePierSelectionResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // The PierSelectionRecord for the GuildID must exist. record, ok := s.Keeper.GetPierSelectionRecord(sdkCtx, msg.GuildID) if !ok { return nil, fmt.Errorf("cover: guild %q has no Pier selection to revoke (REQ-066)", msg.GuildID) } // Authorization: RevocationApproved must be true + CounselWitness must // be non-empty (the Cover Pool supermajority + Counsel witness gate). if !msg.RevocationApproved { return nil, fmt.Errorf("cover: REQ-066 revocation requires RevocationApproved=true (Cover Pool supermajority not secured)") } if msg.CounselWitness == "" { return nil, fmt.Errorf("cover: REQ-066 revocation requires a non-empty CounselWitness (Counsel witness not secured)") } removed := s.Keeper.RemovePierSelectionRecord(sdkCtx, msg.GuildID) if !removed { return nil, fmt.Errorf("cover: PierSelectionRecord for guild %q was not removed (internal error — REQ-066)", msg.GuildID) } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "cover.pier_selection_revoked", sdk.NewAttribute("guild_id", msg.GuildID), sdk.NewAttribute("pier_id", record.PierID), sdk.NewAttribute("counsel_witness", msg.CounselWitness), )) return &types.MsgRevokePierSelectionResponse{}, nil }