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 }