Files
cloudinit-bot 9abda8d01e feat(cover,guild,stand): P5 Anti-Capture Bill ceremony + secession + Pier
P5 (final execution phase) of v0.7 delivers REQ-056, REQ-059, REQ-064,
REQ-066:

x/cover — Bill of Rights ceremony (REQ-056) + Pier Selection (REQ-066):
MsgCounselReviewBillOfRights (bonded Counsel, Staked=true gate);
MsgSelectPier (Guild Council + GuildKeeper shim + PierSelectionIndex);
MsgRevokePierSelection (supermajority + Counsel witness); PierSelectionIndex
+ PierSelectionRecord structs; bill_review/pier_selection/pier_index stores.

x/guild — Secession cooling (REQ-064) + Stand->Pier (REQ-059, D-074):
MsgInitiateSecession (SecessionStartedAt + lien audit); MsgCompleteSecession
(21d Cover-active / 14d non-Cover cooling + lien audit + covenant clearance
+ pro-rata settlement event); MsgEscalateStandToPier (10M Grain-cents D-074
threshold, soft upgrade); MsgAcceptPierInvitation; Guild.SecessionStartedAt
+ SecededAt + Lien.Cleared additive; LOCAL StandPierEscalationAnnualPass
VolumeCents const (G-003 local-const mirror of x/stand canonical).

x/stand — StandPierEscalationAnnualPassVolumeCents=10000000 canonical const.

Coverage: cover/keeper 94.6%, guild/keeper 94.1%, stand/types 100%.
G-006/G-028 intact. go.mod/go.sum diff EMPTY. go vet clean. Lexicon green.

---ci---
project: oy
phase: 5
milestone: v0.7
status: execute
---/ci---
2026-08-19 02:44:12 +00:00

1102 lines
47 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}