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---
This commit is contained in:
@@ -331,3 +331,216 @@ func (s msgServer) AddLien(ctx interface{}, msg *types.MsgAddLien) (*types.MsgAd
|
||||
))
|
||||
return &types.MsgAddLienResponse{}, nil
|
||||
}
|
||||
|
||||
// --- v0.7 P5: Secession + Stand→Pier escalation handlers (REQ-064, REQ-059) ---
|
||||
//
|
||||
// (REQ-064 secession cooling enforcement, REQ-059/D-074 Stand→Pier boundary.)
|
||||
// The four handlers exercise the secession lifecycle (initiate + complete
|
||||
// with the Cover-active 21d / non-Cover 14d cooling) + the Stand→Pier
|
||||
// escalation soft-upgrade (eligibility flag + acceptance).
|
||||
|
||||
// InitiateSecession initiates a Chapter's secession (REQ-064). The handler
|
||||
// enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. The Guild must exist + be a Chapter (IsChapter=true). A non-Chapter
|
||||
// Guild REJECTS (a Parent Guild does not secede).
|
||||
// 3. The Chapter must not have already initiated (SecessionStartedAt == 0;
|
||||
// a second initiation REJECTS — use CompleteSecession or reset).
|
||||
// 4. Set SecessionStartedAt = now. Persist the Chapter.
|
||||
// 5. Invoke the lien audit (CheckLiensCleared — true if every lien has
|
||||
// Cleared=true or Amount=0). The audit result is returned in the
|
||||
// response (LienAuditPassed) for simtest assertion; the handler still
|
||||
// records SecessionStartedAt so the cooling clock starts regardless
|
||||
// (the lien audit is re-checked at completion — an outstanding lien at
|
||||
// completion REJECTS).
|
||||
// 6. Emit guild.secession_initiated.
|
||||
func (s msgServer) InitiateSecession(ctx interface{}, msg *types.MsgInitiateSecession) (*types.MsgInitiateSecessionResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
g, ok := s.Keeper.GetGuild(sdkCtx, msg.GuildID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("guild: guild %q not found (InitiateSecession rejected)", msg.GuildID)
|
||||
}
|
||||
if !g.IsChapter {
|
||||
return nil, fmt.Errorf("guild: guild %q is not a Chapter (a Parent Guild does not secede — REQ-064)", msg.GuildID)
|
||||
}
|
||||
if g.SecessionStartedAt > 0 {
|
||||
return nil, fmt.Errorf("guild: chapter %q already initiated secession (SecessionStartedAt=%d — use CompleteSecession — REQ-064)", msg.GuildID, g.SecessionStartedAt)
|
||||
}
|
||||
|
||||
g.SecessionStartedAt = sdkCtx.BlockTime().Unix()
|
||||
s.Keeper.SetGuild(sdkCtx, g)
|
||||
|
||||
lienAuditPassed := s.Keeper.CheckLiensCleared(sdkCtx, msg.GuildID)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"guild.secession_initiated",
|
||||
sdk.NewAttribute("guild_id", msg.GuildID),
|
||||
sdk.NewAttribute("secession_started_at", fmt.Sprintf("%d", g.SecessionStartedAt)),
|
||||
sdk.NewAttribute("lien_audit_passed", fmt.Sprintf("%v", lienAuditPassed)),
|
||||
))
|
||||
return &types.MsgInitiateSecessionResponse{LienAuditPassed: lienAuditPassed}, nil
|
||||
}
|
||||
|
||||
// CompleteSecession completes a Chapter's secession (REQ-064). The handler
|
||||
// enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. The Guild must exist + be a Chapter.
|
||||
// 3. SecessionStartedAt > 0 (secession was initiated).
|
||||
// 4. Cooling check (REQ-064): compute coolingSeconds based on whether the
|
||||
// Chapter is Cover-active (any lien references a Cover Pool covenant).
|
||||
// Cover-active: CoolingSecessionCoverActiveDays*86400 (21d). Non-Cover:
|
||||
// CoolingSecessionNonCoverDays*86400 (14d). Check now >=
|
||||
// SecessionStartedAt + coolingSeconds. If not, REJECT with "secession
|
||||
// cooling not elapsed".
|
||||
// 5. Lien audit: CheckLiensCleared must return true (all liens Cleared or
|
||||
// Amount=0). If not, REJECT.
|
||||
// 6. Covenant clearance: the Msg's CovenantClearancePassed must be true
|
||||
// (simtest-grade — the live Cover Pool covenant clearance is a v0.8+
|
||||
// concern). If not, REJECT.
|
||||
// 7. Pro-rata Cover-Fee settlement: emit guild.pro_rata_settlement with
|
||||
// the ProRataSettlementGrain from the Msg (the actual settlement is a
|
||||
// v0.8+ Grain-ledger concern).
|
||||
// 8. Set SecededAt = now. Persist the Chapter. Emit
|
||||
// guild.secession_completed.
|
||||
func (s msgServer) CompleteSecession(ctx interface{}, msg *types.MsgCompleteSecession) (*types.MsgCompleteSecessionResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
g, ok := s.Keeper.GetGuild(sdkCtx, msg.GuildID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("guild: guild %q not found (CompleteSecession rejected)", msg.GuildID)
|
||||
}
|
||||
if !g.IsChapter {
|
||||
return nil, fmt.Errorf("guild: guild %q is not a Chapter (a Parent Guild does not secede — REQ-064)", msg.GuildID)
|
||||
}
|
||||
if g.SecessionStartedAt == 0 {
|
||||
return nil, fmt.Errorf("guild: chapter %q has not initiated secession (SecessionStartedAt=0 — call InitiateSecession first — REQ-064)", msg.GuildID)
|
||||
}
|
||||
|
||||
// Cooling check (REQ-064): compute coolingSeconds based on whether the
|
||||
// Chapter is Cover-active (any lien references a Cover Pool covenant).
|
||||
// Cover-active: 21d. Non-Cover: 14d.
|
||||
var coolingSeconds int64
|
||||
coverActive := s.Keeper.ChapterIsCoverActive(sdkCtx, msg.GuildID)
|
||||
if coverActive {
|
||||
coolingSeconds = int64(types.CoolingSecessionCoverActiveDays) * 24 * 60 * 60
|
||||
} else {
|
||||
coolingSeconds = int64(types.CoolingSecessionNonCoverDays) * 24 * 60 * 60
|
||||
}
|
||||
now := sdkCtx.BlockTime().Unix()
|
||||
if now-g.SecessionStartedAt < coolingSeconds {
|
||||
return nil, fmt.Errorf("guild: secession cooling not elapsed (now=%d, SecessionStartedAt=%d, cooling=%d seconds, elapsed=%d — REQ-064)",
|
||||
now, g.SecessionStartedAt, coolingSeconds, now-g.SecessionStartedAt)
|
||||
}
|
||||
|
||||
// Lien audit: all liens must be Cleared (or Amount=0).
|
||||
if !s.Keeper.CheckLiensCleared(sdkCtx, msg.GuildID) {
|
||||
return nil, fmt.Errorf("guild: lien audit failed — outstanding liens remain (REQ-064 — all Good-Standing Liens must be cleared before secession completes)")
|
||||
}
|
||||
|
||||
// Covenant clearance: the Msg's CovenantClearancePassed must be true
|
||||
// (simtest-grade — the live Cover Pool covenant clearance is a v0.8+
|
||||
// concern).
|
||||
if !msg.CovenantClearancePassed {
|
||||
return nil, fmt.Errorf("guild: covenant clearance failed (CovenantClearancePassed=false — REQ-064 — all Cover Pool covenants must be cleared before secession completes)")
|
||||
}
|
||||
|
||||
// Pro-rata Cover-Fee settlement: emit the event with the settlement
|
||||
// amount (the actual settlement is a v0.8+ Grain-ledger concern — the
|
||||
// simtest-grade ProRataSettlementGrain on the Msg carries the amount).
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"guild.pro_rata_settlement",
|
||||
sdk.NewAttribute("guild_id", msg.GuildID),
|
||||
sdk.NewAttribute("pro_rata_settlement_grain", fmt.Sprintf("%d", msg.ProRataSettlementGrain)),
|
||||
))
|
||||
|
||||
// Mark the Chapter as seceded.
|
||||
g.SecededAt = now
|
||||
s.Keeper.SetGuild(sdkCtx, g)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"guild.secession_completed",
|
||||
sdk.NewAttribute("guild_id", msg.GuildID),
|
||||
sdk.NewAttribute("seceded_at", fmt.Sprintf("%d", g.SecededAt)),
|
||||
sdk.NewAttribute("cooling_seconds", fmt.Sprintf("%d", coolingSeconds)),
|
||||
sdk.NewAttribute("cover_active", fmt.Sprintf("%v", coverActive)),
|
||||
))
|
||||
return &types.MsgCompleteSecessionResponse{
|
||||
CoolingSeconds: coolingSeconds,
|
||||
ProRataSettlementGrain: msg.ProRataSettlementGrain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EscalateStandToPier escalates a Stand to Pier-eligibility (REQ-059,
|
||||
// D-074). The handler enforces:
|
||||
// 1. ValidateBasic (stateless — non-empty StandID + AnnualPassVolumeCents
|
||||
// > 0).
|
||||
// 2. Compare AnnualPassVolumeCents against
|
||||
// StandPierEscalationAnnualPassVolumeCents (the D-074 const, imported
|
||||
// from x/stand/types — G-003-clean: consts are not structs).
|
||||
// 3. If AnnualPassVolumeCents > the const: set the Stand-Pier-eligible
|
||||
// flag (pier_eligible/ store: StandID -> true). Soft upgrade, not a
|
||||
// ban — the Stand may decline (the flag is set + the event is emitted,
|
||||
// but no enforcement follows; the Stand must separately accept via
|
||||
// MsgAcceptPierInvitation). If the volume does NOT exceed the const:
|
||||
// the flag is NOT set (the response PierEligible=false; the event is
|
||||
// still emitted for observability).
|
||||
// 4. Emit guild.stand_pier_eligible (with PierEligible=true) OR
|
||||
// guild.stand_pier_escalation_below_threshold (with PierEligible=false).
|
||||
func (s msgServer) EscalateStandToPier(ctx interface{}, msg *types.MsgEscalateStandToPier) (*types.MsgEscalateStandToPierResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
pierEligible := msg.AnnualPassVolumeCents > types.StandPierEscalationAnnualPassVolumeCents
|
||||
if pierEligible {
|
||||
s.Keeper.SetStandPierEligible(sdkCtx, msg.StandID, true)
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"guild.stand_pier_eligible",
|
||||
sdk.NewAttribute("stand_id", msg.StandID),
|
||||
sdk.NewAttribute("annual_pass_volume_cents", fmt.Sprintf("%d", msg.AnnualPassVolumeCents)),
|
||||
sdk.NewAttribute("threshold_cents", fmt.Sprintf("%d", types.StandPierEscalationAnnualPassVolumeCents)),
|
||||
))
|
||||
} else {
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"guild.stand_pier_escalation_below_threshold",
|
||||
sdk.NewAttribute("stand_id", msg.StandID),
|
||||
sdk.NewAttribute("annual_pass_volume_cents", fmt.Sprintf("%d", msg.AnnualPassVolumeCents)),
|
||||
sdk.NewAttribute("threshold_cents", fmt.Sprintf("%d", types.StandPierEscalationAnnualPassVolumeCents)),
|
||||
))
|
||||
}
|
||||
return &types.MsgEscalateStandToPierResponse{PierEligible: pierEligible}, nil
|
||||
}
|
||||
|
||||
// AcceptPierInvitation records a Stand's acceptance of a Pier invitation
|
||||
// (REQ-059, D-074). The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. The Stand must be Pier-eligible (the flag set by
|
||||
// MsgEscalateStandToPier). If not, REJECT.
|
||||
// 3. Record the acceptance (pier_accepted/ store: StandID -> true). Emit
|
||||
// guild.stand_pier_accepted.
|
||||
func (s msgServer) AcceptPierInvitation(ctx interface{}, msg *types.MsgAcceptPierInvitation) (*types.MsgAcceptPierInvitationResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
if !s.Keeper.GetStandPierEligible(sdkCtx, msg.StandID) {
|
||||
return nil, fmt.Errorf("guild: stand %q is not Pier-eligible (call EscalateStandToPier first — REQ-059/D-074)", msg.StandID)
|
||||
}
|
||||
|
||||
s.Keeper.SetStandPierAccepted(sdkCtx, msg.StandID, true)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"guild.stand_pier_accepted",
|
||||
sdk.NewAttribute("stand_id", msg.StandID),
|
||||
))
|
||||
return &types.MsgAcceptPierInvitationResponse{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user