feat(bond,cover,standing): P4 MAB + Cover Claims Voucher + Shadow vouch

v0.7 P4 (REQ-054, REQ-055, REQ-060, REQ-063, D-080, D-089, D-090):

x/bond (Mutual Aid Bond): MAB struct (Bond anonymous embed) mirroring
GrowthBond; CouponDenom enum (CoverCall/MutualAidCredit/Bread-rejected);
MABIssuanceCeilingAnnualSurplusMultiple=3 locked const; ValidateMAB
rejects CouponDenomBread (FR-MAB-3 dual firewall); D-080 tagged streaming
(reserve_build_out); 4 handlers (IssueMAB with 3x ceiling check,
DebitMABProceeds with auto-Still on misuse, WitnessMABProceedsRelease
with Watcher quorum, WatcherAttestMAB); CoverKeeper reverse edge (D-089).

x/cover (Cover Claims Voucher + dissolution): CoverClaimsVoucher struct;
D-090(2) cold-start bond = max(10x avgCallSize, MinimumVoucherBond); 4
handlers (RegisterCoverClaimsVoucher, AdjudicateCoverCall with FR-CPCV-2
no self-adjudication, SlashCoverClaimsVoucher with cross-Pool bucket
drop, DissolveCoverPool with FR-MAB-4 waterfall Cover-Fee > MAB > Bread);
MAB holders have NO Voice (REQ-063).

x/standing (Shadow vouch): Vouch.IsShadow field; ShadowVouchWeightMultiplier
=0.5 locked const (REQ-060); GetVoucherWeight extended with isShadow param
(post-step 0.5x multiplier; all call sites updated); SlashReasonFraudulent
CoverCall const (REQ-055).

Coverage: bond/keeper 92.1%, cover/keeper 95.0%, standing/types 90.3%.
G-006/G-028 intact. go.mod/go.sum diff EMPTY. go vet clean. Lexicon green.

---ci---
project: oy
phase: 4
milestone: v0.7
status: execute
---/ci---
This commit is contained in:
2026-08-19 02:31:52 +00:00
parent bcae60666b
commit 9e7fc403f5
20 changed files with 3294 additions and 36 deletions
+67 -4
View File
@@ -36,15 +36,78 @@ package types
// A non-existent Stand REJECTS the issuance (the bond is not created).
// - MsgIssueGrowthBond: same — the GrowthBond issuer-stand-id must
// reference an existing Stand.
// - MsgIssueMAB: same — the MAB issuer-stand-id must reference an
// existing Stand (v0.7 P4 extension).
//
// No struct import of x/stand/types — the interface is the by-ID-string
// boundary (G-003). The standID is an opaque string (the Stand's ID, by-
// ID-string ref to x/stand).
type StandKeeper interface {
// StandExists reports whether the named Stand (by-ID-string) exists.
// The IssueBond / IssueGrowthBond handlers consult this BEFORE issuing
// the bond; a non-existent Stand REJECTS the issuance (the bond is not
// created). A nil shim skips this check (simtest wiring — documented in
// the handler).
// The IssueBond / IssueGrowthBond / IssueMAB handlers consult this
// BEFORE issuing the bond; a non-existent Stand REJECTS the issuance
// (the bond is not created). A nil shim skips this check (simtest
// wiring — documented in the handler).
StandExists(standID string) bool
}
// CoverKeeper is the expected-keeper interface for x/cover (G-003 — D-089(2)
// reverse edge). The v0.7 MAB handler calls it for:
// - MsgDebitMABProceeds: the handler queries GetPoolReserveAccount(poolID)
// to validate the destination == the Pool's ReserveAccount
// (D-080 tagged streaming). A mismatch -> auto-Still via StillKeeper +
// REJECT. A nil CoverKeeper is a wiring error (the handler REJECTS a
// debit when no CoverKeeper is wired — the destination cannot be
// validated; the simtest wires a stub).
//
// No struct import of x/cover/types — the interface is the by-ID-string
// boundary (G-003 — D-089(2) reverse edge). The poolID is an opaque string
// (the Cover Pool's ID). No import cycle (interface only — the concrete
// cover keeper satisfies this structurally; the simtest wires a stub).
type CoverKeeper interface {
// GetPoolReserveAccount returns the Cover Pool's ReserveAccount by
// pool-id (D-089(2) reverse edge). The MsgDebitMABProceeds handler
// compares the destination against this; a mismatch triggers the
// auto-Still. Returns ("", false) if the pool does not exist.
GetPoolReserveAccount(poolID string) (reserveAccount string, exists bool)
}
// WatcherKeeper is the expected-keeper interface for x/watcher (G-003). The
// v0.7 MAB handler calls it for:
// - MsgWitnessMABProceedsRelease: the handler requires Watcher quorum
// (6-of-9) before the tagged proceeds move from staging to the reserve.
// AttestMABRelease(bondID, attestationRef) returns true if quorum is
// met (the simtest stub returns a configurable bool). A nil
// WatcherKeeper skips the quorum check (simtest wiring — the handler
// still mutates state; the simtest documents the wiring).
//
// No struct import of x/watcher/types — the interface is the by-ID-string
// boundary (G-003). The bondID + attestationRef are opaque strings.
type WatcherKeeper interface {
// AttestMABRelease reports whether the Watcher quorum (6-of-9) is met
// for the MAB proceeds release (D-080). Returns true if quorum present;
// false if not (the handler REJECTS the release). The attestationRef
// is the Watcher-signed observation ref.
AttestMABRelease(bondID string, attestationRef string) bool
}
// StillKeeper is the expected-keeper interface for x/still (G-003 — D-089(1)
// simtest stub). The v0.7 MAB handler calls it for:
// - MsgDebitMABProceeds: on a destination mismatch (D-080 tagged-streaming
// misuse), the handler invokes Still(bondID, "MAB misuse — proceeds
// routed outside reserve") BEFORE rejecting. A nil StillKeeper skips
// the Still recording (simtest wiring — the handler still REJECTS the
// debit; the Still event is just not recorded in a still store).
//
// No struct import of x/still/types — the interface is the by-ID-string
// boundary (G-003). P4 satisfies this by a simtest-local stub (x/still is
// NOT extended this milestone — the simtest stub records Still() calls for
// assertion).
type StillKeeper interface {
// Still pauses the named entity (by-ID-string) for the given reason.
// The MsgDebitMABProceeds handler calls this on a destination mismatch
// (D-080 misuse -> D-089(1) auto-Still). A non-nil error does NOT
// suppress the handler's REJECT (the handler REJECTS regardless; the
// Still is the pause-recording side-effect).
Still(bondID string, reason string) error
}
+38
View File
@@ -142,3 +142,41 @@ func knownOrderStatus(s OrderStatus) bool {
}
return false
}
// --- v0.7 extension: MAB genesis helpers (REQ-054, G-008) ---------------------
//
// genesis.go also holds the data-engineer's genesis schema helpers for the
// v0.7 MAB set (G-008). ValidateGenesis in types.go composes ValidateMABs;
// the security-engineer's test assertions live in types_test.go.
// ValidateMABs asserts mab bond-ids are present and unique, that each
// embedded Bond's coupon-bps is within the LOCKED [floor, cap] bounds
// (D-028), and that each MAB passes ValidateMAB (FR-MAB-3 — rejects
// CouponDenomBread). The genesis-side ValidateMAB is the authoritative
// check (a genesis MAB with a rejected CouponKind is rejected at genesis
// load rather than silently dropped).
func ValidateMABs(mabs []MAB) error {
seen := make(map[string]bool, len(mabs))
for i, m := range mabs {
if m.BondID == "" {
return fmt.Errorf("mab [%d]: empty bond-id", i)
}
if seen[m.BondID] {
return fmt.Errorf("mab: duplicate bond-id %q", m.BondID)
}
seen[m.BondID] = true
if !knownBondStatus(m.Status) {
return fmt.Errorf("mab %q: unknown bond status %q", m.BondID, m.Status)
}
// D-028 clamp on the embedded Bond's coupon.
if m.CouponBps < CouponFloorBps || m.CouponBps > CouponCapBps {
return fmt.Errorf("mab %q: coupon-bps %d outside [%d, %d] (D-028 clamp at genesis load)",
m.BondID, m.CouponBps, CouponFloorBps, CouponCapBps)
}
// FR-MAB-3: MAB coupons NEVER Bread (the dual-firewall runtime gate).
if err := ValidateMAB(m); err != nil {
return fmt.Errorf("mab %q: %w", m.BondID, err)
}
}
return nil
}
+6
View File
@@ -394,6 +394,12 @@ type MsgServer interface {
PlaceSecondaryOrder(ctx interface{}, msg *MsgPlaceSecondaryOrder) (*MsgPlaceSecondaryOrderResponse, error)
CancelSecondaryOrder(ctx interface{}, msg *MsgCancelSecondaryOrder) (*MsgCancelSecondaryOrderResponse, error)
MatchSecondaryOrder(ctx interface{}, msg *MsgMatchSecondaryOrder) (*MsgMatchSecondaryOrderResponse, error)
// v0.7 MAB handlers (REQ-054, D-080, D-089(1), D-089(2)) — defined in
// msg_mab.go.
IssueMAB(ctx interface{}, msg *MsgIssueMAB) (*MsgIssueMABResponse, error)
DebitMABProceeds(ctx interface{}, msg *MsgDebitMABProceeds) (*MsgDebitMABProceedsResponse, error)
WitnessMABProceedsRelease(ctx interface{}, msg *MsgWitnessMABProceedsRelease) (*MsgWitnessMABProceedsReleaseResponse, error)
WatcherAttestMAB(ctx interface{}, msg *MsgWatcherAttestMAB) (*MsgWatcherAttestMABResponse, error)
}
// Response types (hand-rolled; the response is the state mutation + event).
+330
View File
@@ -0,0 +1,330 @@
package types
// msg_mab.go holds the v0.7 Mutual Aid Bond Msg* types implementing sdk.Msg
// (REQ-054, D-080, D-089(1), D-089(2); G-006 controlled exception: types/
// gains the cosmos-sdk import for sdk.Msg — D-055; the invariant/lexicon
// tests in *_test.go stay stdlib-only per G-024, isolated from this
// msg_*.go file).
//
// The four MAB Msg types drive the MAB runtime (REQ-054):
// - MsgIssueMAB: issue a Mutual Aid Bond (the handler enforces the 3×
// annual surplus ceiling + the FR-MAB-3 Bread-coupon rejection +
// Clamp on the coupon).
// - MsgDebitMABProceeds: debit the MAB's tagged proceeds to the Pool's
// ReserveAccount (D-080 — the handler checks destination ==
// CoverKeeper.GetPoolReserveAccount; mismatch -> auto-Still via
// StillKeeper + REJECT).
// - MsgWitnessMABProceedsRelease: a Watcher-witnessed release of the
// tagged proceeds from staging to the reserve (D-080 — the handler
// requires WatcherKeeper.AttestMABRelease quorum 6-of-9).
// - MsgWatcherAttestMAB: the quarterly Watcher audit attestation on a
// MAB (records the attestation-ref against the MAB).
//
// All cross-module refs are by-ID-string (G-003): pool-id refs a Cover Pool
// (via the CoverKeeper shim — D-089(2) reverse edge); the WatcherKeeper +
// StillKeeper shims are interfaces defined in expected_keepers.go. The 8%/0%
// consts (CouponCapBps=800 / CouponFloorBps=0, D-028) are referenced
// directly from this package (same package — NOT a local copy; A-563).
//
// Lexicon (REQ-012, A-210): "Mutual Aid Bond", "MAB", "Cover Call",
// "coupon", "use-of-proceeds", "reserve build-out" are clean. The
// CouponDenomBread const VALUE "Bread" is the OY unit (clean — not a banned
// term). The banned coupon-synonyms are NEVER used.
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// --- MsgIssueMAB --------------------------------------------------------------
// MsgIssueMAB issues a Mutual Aid Bond (REQ-054, D-080). The handler enforces:
// - ValidateBasic (stateless — includes ValidateMAB: rejects
// CouponDenomBread with FR-MAB-3).
// - Idempotency: bond-id must not already exist.
// - StandKeeper shim: the issuer-stand-id must reference an existing Stand
// (P1-02-01 edge). A nil shim skips (simtest wiring).
// - 3× annual surplus ceiling: checkMABIssuanceCeiling asserts
// sum(existingMABPrincipal for poolID) + PrincipalGrain <=
// MABIssuanceCeilingAnnualSurplusMultiple × AnnualSurplusAtIssuance.
// REJECT if above ceiling (re-checked at every issuance).
// - Coupon clamp via Clamp (A-563 — defense in depth).
// - UseOfProceedsTag locked to MABUseOfProceedsReserveBuildOut.
//
// pool-id is on the msg (NOT on the MAB struct — the MAB struct mirrors
// GrowthBond's anonymous-embed pattern; the pool binding is via the
// CoverKeeper reverse edge). The handler records the pool-id in the
// keeper's mab-pool index (BondID -> PoolID) for the ceiling check +
// the DebitMABProceeds destination validation.
type MsgIssueMAB struct {
BondID string `json:"bond_id" yaml:"bond_id"`
PoolID string `json:"pool_id" yaml:"pool_id"`
IssuerStandID string `json:"issuer_stand_id" yaml:"issuer_stand_id"`
PrincipalGrain int64 `json:"principal_grain" yaml:"principal_grain"`
CouponBps uint32 `json:"coupon_bps" yaml:"coupon_bps"`
CouponKind CouponDenom `json:"coupon_kind" yaml:"coupon_kind"`
AnnualSurplusAtIssuance int64 `json:"annual_surplus_at_issuance" yaml:"annual_surplus_at_issuance"`
TermDays uint32 `json:"term_days" yaml:"term_days"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message (sdk.Msg = proto.Message).
func (m *MsgIssueMAB) Reset() { *m = MsgIssueMAB{} }
// String implements proto.Message.
func (m *MsgIssueMAB) String() string {
return fmt.Sprintf("MsgIssueMAB{BondID:%s PoolID:%s IssuerStandID:%s PrincipalGrain:%d CouponBps:%d CouponKind:%s AnnualSurplusAtIssuance:%d TermDays:%d Signer:%s}",
m.BondID, m.PoolID, m.IssuerStandID, m.PrincipalGrain, m.CouponBps, m.CouponKind, m.AnnualSurplusAtIssuance, m.TermDays, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueMAB) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty fields, PrincipalGrain
// > 0, AnnualSurplusAtIssuance > 0, coupon-bps within [CouponFloorBps,
// CouponCapBps] (the stateless clamp guard; the handler re-clamps at
// runtime per A-563), AND ValidateMAB (FR-MAB-3 — rejects CouponDenomBread).
// The 3× annual surplus ceiling is a keeper-handler check (stateful — it
// sums existing MAB principals for the poolID).
func (m *MsgIssueMAB) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.PoolID == "" {
return fmt.Errorf("bond: empty pool-id")
}
if m.IssuerStandID == "" {
return fmt.Errorf("bond: empty issuer-stand-id")
}
if m.PrincipalGrain <= 0 {
return fmt.Errorf("bond: principal-grain must be > 0")
}
if m.AnnualSurplusAtIssuance <= 0 {
return fmt.Errorf("bond: annual-surplus-at-issuance must be > 0")
}
if m.CouponBps < CouponFloorBps || m.CouponBps > CouponCapBps {
return fmt.Errorf("bond: coupon-bps %d out of band [%d, %d] (D-028 stateless guard)", m.CouponBps, CouponFloorBps, CouponCapBps)
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
// FR-MAB-3 dual firewall: ValidateMAB rejects CouponDenomBread at the
// stateless gate (the handler re-checks in defense in depth).
if err := ValidateMAB(MAB{CouponKind: m.CouponKind}); err != nil {
return err
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgIssueMAB) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgDebitMABProceeds ------------------------------------------------------
// MsgDebitMABProceeds debits a MAB's tagged proceeds to the Pool's
// ReserveAccount (D-080). The handler enforces:
// - ValidateBasic (stateless).
// - The MAB must exist.
// - D-080 tagged streaming: DestinationAccount ==
// CoverKeeper.GetPoolReserveAccount(mab's poolID). If mismatch ->
// StillKeeper.Still(bondID, "MAB misuse — proceeds routed outside
// reserve") (D-089(1) — a nil StillKeeper skips the Still recording)
// AND REJECT. If match -> emit bond.mab_proceeds_debited (simtest: the
// debit is the event; no actual Grain transfer in P4).
type MsgDebitMABProceeds struct {
BondID string `json:"bond_id" yaml:"bond_id"`
DestinationAccount string `json:"destination_account" yaml:"destination_account"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgDebitMABProceeds) Reset() { *m = MsgDebitMABProceeds{} }
// String implements proto.Message.
func (m *MsgDebitMABProceeds) String() string {
return fmt.Sprintf("MsgDebitMABProceeds{BondID:%s DestinationAccount:%s Signer:%s}",
m.BondID, m.DestinationAccount, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgDebitMABProceeds) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty bond-id, non-empty
// DestinationAccount, non-empty signer.
func (m *MsgDebitMABProceeds) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.DestinationAccount == "" {
return fmt.Errorf("bond: empty DestinationAccount")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgDebitMABProceeds) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgWitnessMABProceedsRelease ---------------------------------------------
// MsgWitnessMABProceedsRelease is a Watcher-witnessed release of a MAB's
// tagged proceeds from staging to the reserve (D-080). The handler enforces:
// - ValidateBasic (stateless).
// - The MAB must exist.
// - Watcher quorum: WatcherKeeper.AttestMABRelease(bondID, attestationRef)
// returns true if quorum (6-of-9) is met. If false (quorum not met) ->
// REJECT. If true -> emit bond.mab_proceeds_released (the proceeds move
// from tagged staging to the reserve — simtest event).
type MsgWitnessMABProceedsRelease struct {
BondID string `json:"bond_id" yaml:"bond_id"`
AttestationRef string `json:"attestation_ref" yaml:"attestation_ref"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgWitnessMABProceedsRelease) Reset() { *m = MsgWitnessMABProceedsRelease{} }
// String implements proto.Message.
func (m *MsgWitnessMABProceedsRelease) String() string {
return fmt.Sprintf("MsgWitnessMABProceedsRelease{BondID:%s AttestationRef:%s Signer:%s}",
m.BondID, m.AttestationRef, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgWitnessMABProceedsRelease) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty bond-id, non-empty
// attestation-ref, non-empty signer.
func (m *MsgWitnessMABProceedsRelease) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.AttestationRef == "" {
return fmt.Errorf("bond: empty attestation-ref")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgWitnessMABProceedsRelease) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgWatcherAttestMAB ------------------------------------------------------
// MsgWatcherAttestMAB records a quarterly Watcher audit attestation on a MAB
// (D-080). The handler enforces:
// - ValidateBasic (stateless).
// - The MAB must exist.
// - Record the attestation (a store entry mab_attest/<bondID>/<timestamp>
// -> attestationRef). Emit bond.mab_watcher_attested.
type MsgWatcherAttestMAB struct {
BondID string `json:"bond_id" yaml:"bond_id"`
AttestationRef string `json:"attestation_ref" yaml:"attestation_ref"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgWatcherAttestMAB) Reset() { *m = MsgWatcherAttestMAB{} }
// String implements proto.Message.
func (m *MsgWatcherAttestMAB) String() string {
return fmt.Sprintf("MsgWatcherAttestMAB{BondID:%s AttestationRef:%s Signer:%s}",
m.BondID, m.AttestationRef, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgWatcherAttestMAB) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty bond-id, non-empty
// attestation-ref, non-empty signer.
func (m *MsgWatcherAttestMAB) ValidateBasic() error {
if m.BondID == "" {
return fmt.Errorf("bond: empty bond-id")
}
if m.AttestationRef == "" {
return fmt.Errorf("bond: empty attestation-ref")
}
if m.Signer == "" {
return fmt.Errorf("bond: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgWatcherAttestMAB) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MAB Response types -------------------------------------------------------
// MsgIssueMABResponse is the response to MsgIssueMAB. ClampedCouponBps
// reports the runtime-clamped coupon (for simtest assertion that issuance
// clamped it). CeilingMultiple reports the post-issuance
// (sumMABPrincipal / AnnualSurplusAtIssuance) ratio (for simtest assertion
// the ceiling was respected).
type MsgIssueMABResponse struct {
ClampedCouponBps uint32 `json:"clamped_coupon_bps" yaml:"clamped_coupon_bps"`
CeilingMultiple int64 `json:"ceiling_multiple" yaml:"ceiling_multiple"`
}
// Reset implements proto.Message.
func (m *MsgIssueMABResponse) Reset() { *m = MsgIssueMABResponse{} }
// String implements proto.Message.
func (m *MsgIssueMABResponse) String() string {
return fmt.Sprintf("MsgIssueMABResponse{ClampedCouponBps:%d CeilingMultiple:%d}",
m.ClampedCouponBps, m.CeilingMultiple)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueMABResponse) ProtoMessage() {}
// MsgDebitMABProceedsResponse is the response to MsgDebitMABProceeds.
type MsgDebitMABProceedsResponse struct{}
// Reset implements proto.Message.
func (m *MsgDebitMABProceedsResponse) Reset() { *m = MsgDebitMABProceedsResponse{} }
// String implements proto.Message.
func (m *MsgDebitMABProceedsResponse) String() string { return "MsgDebitMABProceedsResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgDebitMABProceedsResponse) ProtoMessage() {}
// MsgWitnessMABProceedsReleaseResponse is the response to
// MsgWitnessMABProceedsRelease.
type MsgWitnessMABProceedsReleaseResponse struct{}
// Reset implements proto.Message.
func (m *MsgWitnessMABProceedsReleaseResponse) Reset() { *m = MsgWitnessMABProceedsReleaseResponse{} }
// String implements proto.Message.
func (m *MsgWitnessMABProceedsReleaseResponse) String() string {
return "MsgWitnessMABProceedsReleaseResponse{}"
}
// ProtoMessage implements proto.Message.
func (*MsgWitnessMABProceedsReleaseResponse) ProtoMessage() {}
// MsgWatcherAttestMABResponse is the response to MsgWatcherAttestMAB.
type MsgWatcherAttestMABResponse struct{}
// Reset implements proto.Message.
func (m *MsgWatcherAttestMABResponse) Reset() { *m = MsgWatcherAttestMABResponse{} }
// String implements proto.Message.
func (m *MsgWatcherAttestMABResponse) String() string { return "MsgWatcherAttestMABResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgWatcherAttestMABResponse) ProtoMessage() {}
+153
View File
@@ -29,6 +29,33 @@ const (
// §17, REQ-021). A regression firewall: adding/removing/renaming a bond
// status breaks this const's test.
BondStatusCount = 5
// MABIssuanceCeilingAnnualSurplusMultiple is the LOCKED ceiling on the
// total outstanding MAB principal for a pool, expressed as a multiple of
// the pool's AnnualSurplusAtIssuance (vision §17, REQ-054 locked — the
// 3× annual surplus mission-locked ceiling). The handler re-checks at
// every issuance (not just the first): sum(existingMABPrincipal) +
// newPrincipal <= 3 × AnnualSurplusAtIssuance. A regression here is a
// mission-lock breach.
MABIssuanceCeilingAnnualSurplusMultiple = 3
// MABUseOfProceedsReserveBuildOut is the D-080 tagged-streaming use-of-
// proceeds tag for a MAB: the proceeds are tagged for "reserve_build_out"
// (the Cover Pool's ReserveAccount build-out). The MsgDebitMABProceeds
// handler checks the destination == the Pool's ReserveAccount;
// the MsgWitnessMABProceedsRelease handler requires Watcher quorum before
// the tagged proceeds move from staging to the reserve. The tag is the
// D-080 lock — a MAB's proceeds are NEVER routable outside reserve
// build-out (mismatch -> auto-Still + REJECT).
MABUseOfProceedsReserveBuildOut = "reserve_build_out"
// CouponDenomCount is the count of CouponDenom enum values (vision §17,
// REQ-054). A regression firewall: adding/removing/renaming a CouponDenom
// breaks this const's test. The three values are CouponDenomCoverCall,
// CouponDenomMutualAidCredit, CouponDenomBread (the last exists ONLY to
// be rejected at ValidateMAB with "FR-MAB-3: MAB coupons NEVER Bread" —
// the dual-firewall runtime gate mirroring MissionLockAmendmentRejected).
CouponDenomCount = 3
)
// BondStatus enumerates the bond lifecycle states (vision §17, REQ-021).
@@ -124,6 +151,7 @@ type GenesisState struct {
Bonds []Bond `json:"bonds" yaml:"bonds"`
GrowthBonds []GrowthBond `json:"growth_bonds" yaml:"growth_bonds"`
Orders []SecondaryOrder `json:"orders" yaml:"orders"`
MABs []MAB `json:"mabs" yaml:"mabs"`
}
func DefaultGenesisState() *GenesisState {
@@ -132,6 +160,7 @@ func DefaultGenesisState() *GenesisState {
Bonds: []Bond{},
GrowthBonds: []GrowthBond{},
Orders: []SecondaryOrder{},
MABs: []MAB{},
}
}
@@ -154,6 +183,9 @@ func ValidateGenesis(bz json.RawMessage) error {
if err := ValidateOrders(gs.Orders); err != nil {
return fmt.Errorf("bond: %w", err)
}
if err := ValidateMABs(gs.MABs); err != nil {
return fmt.Errorf("bond: %w", err)
}
return nil
}
@@ -281,6 +313,127 @@ func IssueGrowth(bondID, issuerStandID string, principalGrain int64, couponBps,
}
}
// --- v0.7 extension: Mutual Aid Bond (MAB) (REQ-054, D-080, D-089(2)) -----------
//
// The v0.7 bond extension adds the Mutual Aid Bond (MAB): a mission-locked
// bond a Cover Pool issues to build out its reserve (vision §17, REQ-054).
// The MAB embeds the v0.2 Bond (anonymous field) so it carries all Bond
// fields PLUS a CouponKind (the coupon denomination: Cover-Call or Mutual-Aid
// Credit — Bread is the rejected sentinel), an AnnualSurplusAtIssuance (the
// pool's annual surplus at issuance, used for the 3× ceiling check), and a
// UseOfProceedsTag (D-080 — locked to "reserve_build_out"). The coupon rate
// is clamped to [CouponFloorBps, CouponCapBps] via Clamp (the 8%/0% consts
// D-028 apply to MABs too).
//
// The 3× annual surplus ceiling (MABIssuanceCeilingAnnualSurplusMultiple) is
// the mission-locked upper bound on the total outstanding MAB principal for
// a pool (vision §17, REQ-054 locked). The handler re-checks at every
// issuance: sum(existingMABPrincipal) + newPrincipal <= 3 ×
// AnnualSurplusAtIssuance. A regression here is a mission-lock breach.
//
// D-080 tagged streaming: the UseOfProceedsTag is locked to
// "reserve_build_out"; the MsgDebitMABProceeds handler checks the destination
// == the Pool's ReserveAccount (queried via the CoverKeeper shim — D-089(2)
// reverse edge); mismatch -> auto-Still via StillKeeper + REJECT. The
// MsgWitnessMABProceedsRelease handler requires Watcher quorum (6-of-9)
// before the tagged proceeds move from staging to the reserve.
//
// Lexicon (REQ-012, A-210): "Mutual Aid Bond", "MAB", "Cover Call", "coupon",
// "use-of-proceeds", "reserve build-out" are clean. The CouponDenomBread
// const VALUE is "Bread" (the OY unit, not a banned term — clean). The
// banned coupon-synonyms are NEVER used.
// CouponDenom enumerates the three coupon denominations a MAB may carry
// (vision §17, REQ-054). Two are valid (CoverCall, MutualAidCredit); the
// third — Bread — exists ONLY to be rejected at ValidateMAB with
// "FR-MAB-3: MAB coupons NEVER Bread" (the dual-firewall runtime gate
// mirroring MissionLockAmendmentRejected at x/council/types/types.go:242).
// The enum value EXISTS to document in code that MAB coupons are NEVER Bread;
// the ValidateMAB gate rejects it; the locked-const test asserts the count.
type CouponDenom string
const (
// CouponDenomCoverCall is the Cover-Call coupon denomination (a MAB
// whose coupon is settled in Cover-Call units — the primary MAB kind).
CouponDenomCoverCall CouponDenom = "CoverCall"
// CouponDenomMutualAidCredit is the Mutual-Aid-Credit coupon
// denomination (a MAB whose coupon is settled in mutual-aid credit
// units — the secondary MAB kind).
CouponDenomMutualAidCredit CouponDenom = "MutualAidCredit"
// CouponDenomBread is the REJECTED sentinel coupon denomination
// (FR-MAB-3 — MAB coupons NEVER Bread). The enum value EXISTS to
// document in code that MAB coupons are NEVER Bread; the ValidateMAB
// gate rejects any MAB with this CouponKind. The const VALUE "Bread"
// is the OY unit (clean — not a banned term). Mirrors
// ProposalMissionLockAmendmentRejected at x/council/types/types.go:242.
CouponDenomBread CouponDenom = "Bread"
)
// AllCouponDenoms returns all three CouponDenom values in REQ-054 order. The
// locked-const test asserts exactly 3 entries (the regression firewall).
func AllCouponDenoms() []CouponDenom {
return []CouponDenom{
CouponDenomCoverCall,
CouponDenomMutualAidCredit,
CouponDenomBread,
}
}
// MAB is a Mutual Aid Bond: a mission-locked bond a Cover Pool issues to
// build out its reserve (vision §17, REQ-054, D-080, D-089(2)). It embeds
// the v0.2 Bond (anonymous field) so it carries all Bond fields (bond-id,
// issuer-stand-id, principal-grain, coupon-bps, term-days, issued-at,
// maturity, status) PLUS a CouponKind (the coupon denomination), an
// AnnualSurplusAtIssuance (the pool's annual surplus at issuance, used for
// the 3× ceiling check), and a UseOfProceedsTag (D-080 — locked to
// "reserve_build_out"). The coupon rate is clamped to [CouponFloorBps,
// CouponCapBps] via Clamp at issuance (the 8%/0% consts D-028 apply).
//
// pool-id is NOT a field on MAB (the MAB is issued by a Stand for a pool;
// the pool binding is via the CoverKeeper.GetPoolReserveAccount reverse
// edge — D-089(2)). The MsgDebitMABProceeds handler queries the CoverKeeper
// for the pool's ReserveAccount by the MAB's PoolID (carried on the msg,
// not the MAB struct — the MAB struct mirrors GrowthBond's anonymous-embed
// pattern + the MAB-specific fields only).
type MAB struct {
Bond // anonymous embed — carries all v0.2 Bond fields
CouponKind CouponDenom `json:"coupon_kind" yaml:"coupon_kind"`
AnnualSurplusAtIssuance int64 `json:"annual_surplus_at_issuance" yaml:"annual_surplus_at_issuance"`
UseOfProceedsTag string `json:"use_of_proceeds_tag" yaml:"use_of_proceeds_tag"`
}
// IssueMAB is the MAB issuance stub (REQ-054, D-080). It constructs a MAB
// with the coupon clamped to [CouponFloorBps, CouponCapBps] via Clamp, the
// CouponKind set, and the UseOfProceedsTag locked to
// MABUseOfProceedsReserveBuildOut. The returned MAB has status BondIssued
// (inherited from Issue's Bond construction). The stub does not persist or
// enforce the 3× annual surplus ceiling (that is a keeper-handler concern);
// it only enforces the coupon clamp invariant at construction time.
func IssueMAB(bondID, issuerStandID string, principalGrain int64, couponBps uint32, couponKind CouponDenom, annualSurplusAtIssuance int64, termDays uint32, issuedAt, maturity int64) MAB {
clampedCoupon := Clamp(couponBps)
return MAB{
Bond: Issue(bondID, issuerStandID, principalGrain, clampedCoupon, termDays, issuedAt, maturity),
CouponKind: couponKind,
AnnualSurplusAtIssuance: annualSurplusAtIssuance,
UseOfProceedsTag: MABUseOfProceedsReserveBuildOut,
}
}
// ValidateMAB is the MAB runtime firewall (REQ-054, FR-MAB-3). It rejects a
// MAB whose CouponKind == CouponDenomBread with "FR-MAB-3: MAB coupons
// NEVER Bread" — the dual-firewall runtime gate mirroring
// MissionLockAmendmentRejected at x/council/types/types.go:242. The
// CouponDenomBread const EXISTS to document in code that MAB coupons are
// NEVER Bread; this gate rejects any MAB with that CouponKind. The
// ValidateBasic on MsgIssueMAB calls this; the keeper handler re-checks in
// defense in depth.
func ValidateMAB(m MAB) error {
if m.CouponKind == CouponDenomBread {
return fmt.Errorf("FR-MAB-3: MAB coupons NEVER Bread (CouponDenomBread is the rejected sentinel — REQ-054 dual firewall)")
}
return nil
}
// SecondaryOrder is a secondary-market order on an issued bond (vision §17,
// REQ-026, D-041, A-313). order-id is the unique identifier. bond-id references
// a Bond (by-ID-string ref to a Bond — same package, so this is an in-package
+172
View File
@@ -962,3 +962,175 @@ func packageDir(t *testing.T, importPath string) string {
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
return filepath.Join(repoRoot, rel)
}
// --- v0.7 P4: MAB locked consts + ValidateMAB + IssueMAB (REQ-054) -----------
//
// The MAB locked-const + ValidateMAB + IssueMAB regression tests (REQ-054,
// FR-MAB-3, D-080). A regression here is a mission-lock breach.
// TestMABIssuanceCeilingAnnualSurplusMultiple asserts the 3× annual surplus
// ceiling multiple is the locked 3 (REQ-054 locked — vision §17 3× annual
// surplus mission-locked ceiling).
func TestMABIssuanceCeilingAnnualSurplusMultiple(t *testing.T) {
if btypes.MABIssuanceCeilingAnnualSurplusMultiple != 3 {
t.Errorf("MABIssuanceCeilingAnnualSurplusMultiple = %d, want 3 (REQ-054 locked — 3× annual surplus ceiling)", btypes.MABIssuanceCeilingAnnualSurplusMultiple)
}
}
// TestMABUseOfProceedsReserveBuildOut asserts the D-080 tagged-streaming
// use-of-proceeds tag is "reserve_build_out".
func TestMABUseOfProceedsReserveBuildOut(t *testing.T) {
if btypes.MABUseOfProceedsReserveBuildOut != "reserve_build_out" {
t.Errorf("MABUseOfProceedsReserveBuildOut = %q, want %q (D-080 tagged-streaming use-of-proceeds)", btypes.MABUseOfProceedsReserveBuildOut, "reserve_build_out")
}
}
// TestCouponDenomCount asserts CouponDenomCount == 3 (the regression
// firewall — the three CouponDenom values are CoverCall, MutualAidCredit,
// Bread).
func TestCouponDenomCount(t *testing.T) {
if btypes.CouponDenomCount != 3 {
t.Errorf("CouponDenomCount = %d, want 3 (REQ-054 — CoverCall + MutualAidCredit + Bread)", btypes.CouponDenomCount)
}
if len(btypes.AllCouponDenoms()) != 3 {
t.Errorf("AllCouponDenoms len = %d, want 3", len(btypes.AllCouponDenoms()))
}
}
// TestCouponDenomValues asserts the three CouponDenom string values.
func TestCouponDenomValues(t *testing.T) {
cases := []struct {
d btypes.CouponDenom
want string
}{
{btypes.CouponDenomCoverCall, "CoverCall"},
{btypes.CouponDenomMutualAidCredit, "MutualAidCredit"},
{btypes.CouponDenomBread, "Bread"},
}
for _, c := range cases {
if string(c.d) != c.want {
t.Errorf("CouponDenom(%q) value = %q, want %q", c.d, c.d, c.want)
}
}
}
// TestValidateMABRejectsBread asserts ValidateMAB rejects CouponDenomBread
// with "FR-MAB-3" (the dual-firewall runtime gate mirroring
// MissionLockAmendmentRejected).
func TestValidateMABRejectsBread(t *testing.T) {
m := btypes.MAB{CouponKind: btypes.CouponDenomBread}
err := btypes.ValidateMAB(m)
if err == nil {
t.Fatal("ValidateMAB on CouponDenomBread should be REJECTED (FR-MAB-3)")
}
if !strings.Contains(err.Error(), "FR-MAB-3") {
t.Errorf("err = %q, want 'FR-MAB-3'", err.Error())
}
if !strings.Contains(err.Error(), "NEVER Bread") {
t.Errorf("err = %q, want 'NEVER Bread'", err.Error())
}
// A valid CouponKind passes.
if err := btypes.ValidateMAB(btypes.MAB{CouponKind: btypes.CouponDenomCoverCall}); err != nil {
t.Errorf("ValidateMAB on CouponDenomCoverCall should pass; got: %v", err)
}
if err := btypes.ValidateMAB(btypes.MAB{CouponKind: btypes.CouponDenomMutualAidCredit}); err != nil {
t.Errorf("ValidateMAB on CouponDenomMutualAidCredit should pass; got: %v", err)
}
}
// TestIssueMABClampsCoupon asserts IssueMAB clamps the coupon to
// [CouponFloorBps, CouponCapBps] (the cross-const test extending REQ-030 —
// the MAB coupon cap == CouponCapBps).
func TestIssueMABClampsCoupon(t *testing.T) {
// In-band coupon: unchanged.
m := btypes.IssueMAB("mab-1", "stand-1", 1_000_000, 500, btypes.CouponDenomCoverCall, 5_000_000, 365, 1000, 1365)
if m.CouponBps != 500 {
t.Errorf("in-band CouponBps = %d, want 500 (unchanged)", m.CouponBps)
}
if m.CouponKind != btypes.CouponDenomCoverCall {
t.Errorf("CouponKind = %q, want CoverCall", m.CouponKind)
}
if m.UseOfProceedsTag != btypes.MABUseOfProceedsReserveBuildOut {
t.Errorf("UseOfProceedsTag = %q, want %q (D-080 lock)", m.UseOfProceedsTag, btypes.MABUseOfProceedsReserveBuildOut)
}
if m.Status != btypes.BondIssued {
t.Errorf("Status = %q, want BondIssued", m.Status)
}
// Above-cap coupon: clamped to cap.
m2 := btypes.IssueMAB("mab-2", "stand-1", 1_000_000, 1200, btypes.CouponDenomMutualAidCredit, 5_000_000, 365, 1000, 1365)
if m2.CouponBps != btypes.CouponCapBps {
t.Errorf("above-cap CouponBps = %d, want cap %d (IssueMAB must clamp)", m2.CouponBps, btypes.CouponCapBps)
}
}
// TestValidateMABsRejectsBreadAtGenesis asserts ValidateMABs rejects a
// genesis MAB with CouponDenomBread (FR-MAB-3 at genesis load).
func TestValidateMABsRejectsBreadAtGenesis(t *testing.T) {
mabs := []btypes.MAB{
{Bond: btypes.Bond{BondID: "mab-1", Status: btypes.BondIssued, CouponBps: 500}, CouponKind: btypes.CouponDenomCoverCall, UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut},
{Bond: btypes.Bond{BondID: "mab-bad", Status: btypes.BondIssued, CouponBps: 500}, CouponKind: btypes.CouponDenomBread, UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut},
}
err := btypes.ValidateMABs(mabs)
if err == nil {
t.Fatal("ValidateMABs with CouponDenomBread should be REJECTED at genesis (FR-MAB-3)")
}
if !strings.Contains(err.Error(), "FR-MAB-3") {
t.Errorf("err = %q, want 'FR-MAB-3'", err.Error())
}
}
// TestValidateMABsRejectsDupIDs asserts ValidateMABs rejects duplicate
// bond-ids (A-212 ID-uniqueness at genesis load).
func TestValidateMABsRejectsDupIDs(t *testing.T) {
mabs := []btypes.MAB{
{Bond: btypes.Bond{BondID: "dup", Status: btypes.BondIssued, CouponBps: 500}, CouponKind: btypes.CouponDenomCoverCall, UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut},
{Bond: btypes.Bond{BondID: "dup", Status: btypes.BondIssued, CouponBps: 500}, CouponKind: btypes.CouponDenomMutualAidCredit, UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut},
}
if err := btypes.ValidateMABs(mabs); err == nil {
t.Fatal("ValidateMABs with duplicate bond-ids should be REJECTED")
}
}
// TestValidateMABsAcceptsClean asserts ValidateMABs accepts a clean set.
func TestValidateMABsAcceptsClean(t *testing.T) {
mabs := []btypes.MAB{
{Bond: btypes.Bond{BondID: "m1", Status: btypes.BondIssued, CouponBps: 500}, CouponKind: btypes.CouponDenomCoverCall, UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut},
{Bond: btypes.Bond{BondID: "m2", Status: btypes.BondActive, CouponBps: 600}, CouponKind: btypes.CouponDenomMutualAidCredit, UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut},
}
if err := btypes.ValidateMABs(mabs); err != nil {
t.Errorf("ValidateMABs should accept clean set; got: %v", err)
}
}
// TestMABStructFields asserts the MAB struct carries the anonymous Bond
// embed + the MAB-specific fields (CouponKind + AnnualSurplusAtIssuance +
// UseOfProceedsTag).
func TestMABStructFields(t *testing.T) {
m := btypes.MAB{
Bond: btypes.Bond{BondID: "mab-x", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000, CouponBps: 500, Status: btypes.BondIssued},
CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000,
UseOfProceedsTag: btypes.MABUseOfProceedsReserveBuildOut,
}
if m.BondID != "mab-x" {
t.Errorf("MAB.BondID = %q (anonymous embed access)", m.BondID)
}
if m.CouponKind != btypes.CouponDenomCoverCall {
t.Errorf("MAB.CouponKind = %q", m.CouponKind)
}
if m.AnnualSurplusAtIssuance != 5_000_000 {
t.Errorf("MAB.AnnualSurplusAtIssuance = %d", m.AnnualSurplusAtIssuance)
}
if m.UseOfProceedsTag != btypes.MABUseOfProceedsReserveBuildOut {
t.Errorf("MAB.UseOfProceedsTag = %q", m.UseOfProceedsTag)
}
}
// TestGenesisStateMABsField asserts DefaultGenesisState returns a non-nil
// empty slice for MABs (the v0.7 P4 genesis extension).
func TestGenesisStateMABsField(t *testing.T) {
gs := btypes.DefaultGenesisState()
if gs.MABs == nil || len(gs.MABs) != 0 {
t.Errorf("Default MABs should be non-nil empty slice; got len=%d nil=%v", len(gs.MABs), gs.MABs == nil)
}
}