package keeper // msg_server.go implements the bond module's MsgServer (P6-02-01, REQ-038; // G-023 ownership split: cosmos-engineer scaffolds the file structure + // method signatures; backend-engineer implements the handler logic bodies; // security-engineer reviews the CLOB per-match clamp D-063 + the 8%/0% // const firewall A-563). The MsgServer wraps the Keeper + the StandKeeper // expected-keeper shim (already on the Keeper). // // Each method returns a (*Response, error). Handler state-machine ordering // is enforced: ValidateBasic → keeper authz → state mutation → // ctx.EventManager().EmitEvent. // // Handler set (REQ-038): // - IssueBond: invokes v0.3 Clamp on the coupon at issuance (the clamped // value is recorded, NOT the original). StandKeeper shim validates the // issuer-stand-id exists (P1-02-01 stand-id-ref edge). // - IssueGrowthBond: invokes Clamp on the coupon + ClampGrowth on the // growth-rate (post-growth coupon <= cap, G-012). // - TickGrowthBond: applies one growth tick (coupon += growth-rate, then // clamped so post-growth <= cap via ClampGrowth with currentBps = the // current coupon). // - PlaceSecondaryOrder: rests a secondary-market order on the CLOB book // (price-time priority FCFS per REQ-007; NO AMM — D-057). // - CancelSecondaryOrder: removes a resting order (status -> Cancelled). // - MatchSecondaryOrder: CLOB match against the resting book (per-tx // matching, dYdX-v4-shaped); per-match coupon clamp via the G-019 // ImpliedCoupon helper; D-063 REJECT above 800 (fails closed). // // Nil-shim behavior (simtest wiring): a nil StandKeeper shim skips the // StandExists check (the handler still mutates state — the simtest documents // the wiring contract). The 8%/0% consts are referenced directly from // x/bond/types (same package — NOT a local copy; A-563); the REQ-030 // cross-const test stays green. // // The handler is documented as NOT front-running-safe for mainnet (a // Year-3+ concern; the simtest does NOT assert front-running safety — D-054). import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/oy/openyield/x/bond/types" ) // init wires the emitMatchEventHook so the CLOB engine (clob.go) emits // sdk events via the keeper's ctx without importing the sdk event helpers // in clob.go (keeps clob.go's import list minimal). func init() { emitMatchEventHook = func(ctx sdk.Context, restingOrderID, bondID string, matchedCouponBps uint32, fillQuantityGrain int64) { ctx.EventManager().EmitEvent(sdk.NewEvent( "bond.match", sdk.NewAttribute("resting_order_id", restingOrderID), sdk.NewAttribute("bond_id", bondID), sdk.NewAttribute("matched_coupon_bps", fmt.Sprintf("%d", matchedCouponBps)), sdk.NewAttribute("fill_quantity_grain", fmt.Sprintf("%d", fillQuantityGrain)), )) } } // msgServer is the concrete MsgServer implementation wrapping the Keeper. type msgServer struct { Keeper } // NewMsgServerImpl returns the bond 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("bond: expected sdk.Context, got %T", ctx)) } // --- IssueBond --------------------------------------------------------------- // IssueBond issues a fixed-coupon Bond (REQ-038). The handler enforces: // 1. ValidateBasic (stateless). // 2. Idempotency: bond-id must not already exist. // 3. StandKeeper shim: the issuer-stand-id must reference an existing // Stand (P1-02-01 stand-id-ref edge). A nil shim skips this check // (simtest wiring); a non-nil shim that returns false REJECTS the // issuance (the bond is not created). // 4. Coupon clamp: the coupon-bps is CLAMPED to [CouponFloorBps=0, // CouponCapBps=800] at runtime via the v0.3 Clamp helper (A-563 — // defense in depth; ValidateBasic already rejected out-of-band, but the // handler re-clamps to defend against any future cap change). // // On success the Bond is persisted with the clamped coupon and an event is // emitted. func (s msgServer) IssueBond(ctx interface{}, msg *types.MsgIssueBond) (*types.MsgIssueBondResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: bond-id must not already exist. if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); ok { return nil, fmt.Errorf("bond: bond-id %q already exists", msg.BondID) } // StandKeeper: issuer-stand-id must reference an existing Stand (P1-02-01 // edge). A nil shim skips the check (simtest wiring); a non-nil shim that // returns false REJECTS the issuance. if s.Keeper.standKeeper != nil { if !s.Keeper.standKeeper.StandExists(msg.IssuerStandID) { return nil, fmt.Errorf("bond: issuer-stand-id %q does not exist (IssueBond rejected)", msg.IssuerStandID) } } // A-563: coupon clamp at runtime. The clamped value (NOT the original) // is recorded. ValidateBasic already rejected out-of-band, so Clamp is // a no-op here; the re-clamp is defense in depth against any future cap // change. clamped := types.Clamp(msg.CouponBps) b := types.Issue(msg.BondID, msg.IssuerStandID, msg.PrincipalGrain, clamped, msg.TermDays, msg.IssuedAt, msg.Maturity) s.Keeper.SetBond(sdkCtx, b) if clamped != msg.CouponBps { sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.coupon_clamped", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("original_coupon_bps", fmt.Sprintf("%d", msg.CouponBps)), sdk.NewAttribute("clamped_coupon_bps", fmt.Sprintf("%d", clamped)), )) } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.issued", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("issuer_stand_id", msg.IssuerStandID), sdk.NewAttribute("coupon_bps", fmt.Sprintf("%d", clamped)), )) return &types.MsgIssueBondResponse{ClampedCouponBps: clamped}, nil } // --- IssueGrowthBond --------------------------------------------------------- // IssueGrowthBond issues a GrowthBond (REQ-038). The handler enforces: // 1. ValidateBasic (stateless). // 2. Idempotency: bond-id must not already exist (as a Bond or GrowthBond). // 3. StandKeeper shim: the issuer-stand-id must reference an existing // Stand (P1-02-01 edge). A nil shim skips (simtest wiring). // 4. Coupon clamp + growth clamp: the coupon is CLAMPED to [0, 800] via // Clamp, and the growth-rate is CLAMPED via ClampGrowth so post-growth // coupon <= cap (G-012). // // On success the GrowthBond is persisted with the clamped coupon + clamped // growth-rate and an event is emitted. func (s msgServer) IssueGrowthBond(ctx interface{}, msg *types.MsgIssueGrowthBond) (*types.MsgIssueGrowthBondResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: bond-id must not already exist (as Bond or GrowthBond). if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); ok { return nil, fmt.Errorf("bond: bond-id %q already exists (as a Bond)", msg.BondID) } if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); ok { return nil, fmt.Errorf("bond: bond-id %q already exists (as a GrowthBond)", msg.BondID) } // StandKeeper: issuer-stand-id must reference an existing Stand. if s.Keeper.standKeeper != nil { if !s.Keeper.standKeeper.StandExists(msg.IssuerStandID) { return nil, fmt.Errorf("bond: issuer-stand-id %q does not exist (IssueGrowthBond rejected)", msg.IssuerStandID) } } // Coupon clamp + growth clamp. The v0.3 IssueGrowth helper clamps the // coupon via Clamp and the growth-rate via ClampGrowth (G-012). clampedCoupon := types.Clamp(msg.CouponBps) clampedGrowth := types.ClampGrowth(clampedCoupon, msg.GrowthRateBps) gb := types.IssueGrowth(msg.BondID, msg.IssuerStandID, msg.PrincipalGrain, clampedCoupon, clampedGrowth, msg.TermDays, msg.IssuedAt, msg.Maturity) s.Keeper.SetGrowthBond(sdkCtx, gb) if clampedCoupon != msg.CouponBps || clampedGrowth != msg.GrowthRateBps { sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.growth_coupon_clamped", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("original_coupon_bps", fmt.Sprintf("%d", msg.CouponBps)), sdk.NewAttribute("clamped_coupon_bps", fmt.Sprintf("%d", clampedCoupon)), sdk.NewAttribute("original_growth_rate_bps", fmt.Sprintf("%d", msg.GrowthRateBps)), sdk.NewAttribute("clamped_growth_rate_bps", fmt.Sprintf("%d", clampedGrowth)), )) } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.growth_issued", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("issuer_stand_id", msg.IssuerStandID), sdk.NewAttribute("coupon_bps", fmt.Sprintf("%d", clampedCoupon)), sdk.NewAttribute("growth_rate_bps", fmt.Sprintf("%d", clampedGrowth)), )) return &types.MsgIssueGrowthBondResponse{ ClampedCouponBps: clampedCoupon, ClampedGrowthRateBps: clampedGrowth, }, nil } // --- TickGrowthBond ---------------------------------------------------------- // TickGrowthBond applies one growth tick to a GrowthBond (REQ-038). The // handler enforces: // 1. ValidateBasic (stateless). // 2. The GrowthBond must exist. // 3. Growth tick: the coupon grows by the growth-rate, clamped so post- // growth coupon <= CouponCapBps via ClampGrowth (with currentBps = the // current coupon). The growth-rate is NOT changed (it persists across // ticks). // // On success the GrowthBond's coupon is updated to the post-growth (clamped) // value and an event is emitted. func (s msgServer) TickGrowthBond(ctx interface{}, msg *types.MsgTickGrowthBond) (*types.MsgTickGrowthBondResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) gb, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID) if !ok { return nil, fmt.Errorf("bond: growth-bond %q not found (TickGrowthBond rejected)", msg.BondID) } // Growth tick: coupon += growth-rate, clamped so post-growth <= cap. // ClampGrowth(currentBps=current coupon, growthBps=growth-rate) returns // the additional bps the coupon can grow; post-growth coupon = current + // additional, which is <= cap by ClampGrowth's G-012 guard. additional := types.ClampGrowth(gb.CouponBps, gb.GrowthRateBps) postGrowth := gb.CouponBps + additional gb.CouponBps = postGrowth s.Keeper.SetGrowthBond(sdkCtx, gb) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.growth_ticked", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("post_growth_coupon_bps", fmt.Sprintf("%d", postGrowth)), sdk.NewAttribute("growth_rate_bps", fmt.Sprintf("%d", gb.GrowthRateBps)), )) return &types.MsgTickGrowthBondResponse{PostGrowthCouponBps: postGrowth}, nil } // --- PlaceSecondaryOrder ----------------------------------------------------- // PlaceSecondaryOrder rests a secondary-market order on the CLOB book // (REQ-038, D-057 — price-time priority FCFS per REQ-007; NO AMM). The // handler enforces: // 1. ValidateBasic (stateless). // 2. Idempotency: order-id must not already exist. // 3. The referenced bond must exist (the order rests on an issued bond). // 4. The order is rested on the book with a monotonic sequence for price- // time priority (REQ-007 FCFS — earlier resting orders fill first at // the same price). // // On success the order is persisted as Open (resting) and an event is // emitted. func (s msgServer) PlaceSecondaryOrder(ctx interface{}, msg *types.MsgPlaceSecondaryOrder) (*types.MsgPlaceSecondaryOrderResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: order-id must not already exist. if _, ok := s.Keeper.GetRestingOrder(sdkCtx, msg.OrderID); ok { return nil, fmt.Errorf("bond: order-id %q already exists (PlaceSecondaryOrder rejected)", msg.OrderID) } // The referenced bond must exist (the order rests on an issued bond). if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); !ok { if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); !ok { return nil, fmt.Errorf("bond: bond-id %q does not exist (PlaceSecondaryOrder rejected)", msg.BondID) } } // Construct the public v0.3 SecondaryOrder (the frozen contract). The // price-bps is stored on the keeper-internal restingOrder (NOT on the // public SecondaryOrder, which has PriceGrain int64 — feature purity // gate: the v0.3 contract is not amended). PriceGrain is seeded from // PriceBps for cross-reference (the v0.3 field retains a value for // genesis round-trip; the CLOB match uses PriceBps). so := types.SecondaryOrder{ OrderID: msg.OrderID, BondID: msg.BondID, Side: msg.Side, PriceGrain: int64(msg.PriceBps), HolderReachID: msg.HolderReachID, Status: types.OrderOpen, CreatedAt: sdkCtx.BlockTime().Unix(), } ro := restingOrder{ Order: so, PriceBps: msg.PriceBps, Sequence: s.Keeper.nextSequence(), RemainingQuantityGrain: msg.QuantityGrain, } s.Keeper.setRestingOrder(sdkCtx, ro) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.order_placed", sdk.NewAttribute("order_id", msg.OrderID), sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("side", string(msg.Side)), sdk.NewAttribute("price_bps", fmt.Sprintf("%d", msg.PriceBps)), sdk.NewAttribute("quantity_grain", fmt.Sprintf("%d", msg.QuantityGrain)), )) return &types.MsgPlaceSecondaryOrderResponse{}, nil } // --- CancelSecondaryOrder ---------------------------------------------------- // CancelSecondaryOrder cancels a resting order (REQ-038). The handler // enforces: // 1. ValidateBasic (stateless). // 2. The order must exist and be Open (resting). // 3. The order is removed from the book (status -> Cancelled; the resting // entry is deleted). // // On success the order is cancelled and an event is emitted. func (s msgServer) CancelSecondaryOrder(ctx interface{}, msg *types.MsgCancelSecondaryOrder) (*types.MsgCancelSecondaryOrderResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) ro, ok := s.Keeper.GetRestingOrder(sdkCtx, msg.OrderID) if !ok { return nil, fmt.Errorf("bond: order %q not found (CancelSecondaryOrder rejected)", msg.OrderID) } if ro.Order.Status != types.OrderOpen { return nil, fmt.Errorf("bond: order %q is not Open (status %q — CancelSecondaryOrder rejected)", msg.OrderID, ro.Order.Status) } ro.Order.Status = types.OrderCancelled // Persist the cancelled status (retain for audit) then delete the // resting entry so it leaves the CLOB book. The Cancelled status is // observable via the v0.3 SecondaryOrder.Status field on the persisted // entry (the restingOrder embeds it). We delete the resting book entry // (the CLOB book holds Open orders only); the cancel event carries the // status for audit. s.Keeper.deleteRestingOrder(sdkCtx, msg.OrderID) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.order_cancelled", sdk.NewAttribute("order_id", msg.OrderID), sdk.NewAttribute("status", string(types.OrderCancelled)), )) return &types.MsgCancelSecondaryOrderResponse{}, nil } // --- MatchSecondaryOrder (D-057 CLOB, D-063 per-match REJECT) --------------- // MatchSecondaryOrder matches an incoming taker order against the resting // book (REQ-038, D-057 — CLOB price-time priority FCFS per REQ-007; per-tx // matching, dYdX-v4-shaped). The handler enforces: // 1. ValidateBasic (stateless). // 2. The referenced bond must exist. // 3. The CLOB match (clob.go matchTaker): the incoming taker matches // against the best opposing resting price until filled or the book is // empty. Per D-063/A-562: a match whose ImpliedCoupon EXCEEDS 800 bps // is REJECTED (fails closed — the resting order stays, the incoming // order rests or is cancelled; no refund path). // // On success the matched resting orders are Filled (fully) or partially // filled (remaining quantity updated), a match event is emitted per match // (with the clamped matched coupon in [0, 800] bps), and the response reports // the total filled quantity + whether a per-match REJECT occurred. // // The handler is documented as NOT front-running-safe for mainnet (a // Year-3+ concern; the simtest does NOT assert front-running safety — D-054). func (s msgServer) MatchSecondaryOrder(ctx interface{}, msg *types.MsgMatchSecondaryOrder) (*types.MsgMatchSecondaryOrderResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // The referenced bond must exist. if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); !ok { if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); !ok { return nil, fmt.Errorf("bond: bond-id %q does not exist (MatchSecondaryOrder rejected)", msg.BondID) } } // CLOB match (clob.go). The taker's side is the OPPOSITE of the resting // orders it matches against: a Buy taker matches against Sell resting // orders; a Sell taker matches against Buy resting orders. filled, _, rejected := s.Keeper.matchTaker( sdkCtx, msg.BondID, msg.Side, msg.PriceBps, msg.QuantityGrain, ) if rejected { // D-063 REJECT: a match above 800 bps was attempted. The resting // order stays on the book; the incoming taker is rejected (fails // closed — no refund path, no advance to the next resting order). // Emit a reject event for simtest assertion. sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.match_rejected_above_cap", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("incoming_order_id", msg.IncomingOrderID), sdk.NewAttribute("cap_bps", fmt.Sprintf("%d", types.CouponCapBps)), )) return &types.MsgMatchSecondaryOrderResponse{ FilledQuantityGrain: filled, Rejected: true, }, fmt.Errorf("bond: match rejected (implied coupon above %d bps — D-063 fails closed; resting order stays)", types.CouponCapBps) } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.match_completed", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("incoming_order_id", msg.IncomingOrderID), sdk.NewAttribute("filled_quantity_grain", fmt.Sprintf("%d", filled)), )) return &types.MsgMatchSecondaryOrderResponse{ FilledQuantityGrain: filled, Rejected: false, }, nil } // --- v0.7 P4: MAB handlers (REQ-054, D-080, D-089(1), D-089(2)) ---------------- // // (Mutual Aid Bond runtime — IssueMAB + DebitMABProceeds + // WitnessMABProceedsRelease + WatcherAttestMAB). The four handlers exercise // the 3× annual surplus ceiling, the FR-MAB-3 Bread-coupon rejection, the // D-080 tagged-streaming destination check (CoverKeeper reverse edge — // D-089(2)), the D-089(1) auto-Still on misuse, and the Watcher quorum // (6-of-9) on proceeds release. // checkMABIssuanceCeiling asserts the 3× annual surplus ceiling (REQ-054 // locked). It sums the existing MAB principals for the poolID + the new // principal and asserts the sum <= MABIssuanceCeilingAnnualSurplusMultiple × // annualSurplusAtIssuance. Returns the post-issuance // (sumMABPrincipal / annualSurplusAtIssuance) ratio (for the response) and // an error if above ceiling. The check re-runs at every issuance (not just // the first), so a pool that issues up to the ceiling cannot issue more. func (s msgServer) checkMABIssuanceCeiling(ctx sdk.Context, poolID string, newPrincipal int64, annualSurplusAtIssuance int64) (int64, error) { existing := int64(0) for _, m := range s.Keeper.MABsForPool(ctx, poolID) { existing += m.PrincipalGrain } total := existing + newPrincipal ceiling := int64(types.MABIssuanceCeilingAnnualSurplusMultiple) * annualSurplusAtIssuance if total > ceiling { return 0, fmt.Errorf("bond: MAB issuance ceiling breached (sum %d + new %d = %d > 3× annual-surplus %d = %d — REQ-054 locked)", existing, newPrincipal, total, annualSurplusAtIssuance, ceiling) } if annualSurplusAtIssuance == 0 { return 0, nil } return total / annualSurplusAtIssuance, nil } // IssueMAB issues a Mutual Aid Bond (REQ-054, D-080). The handler enforces: // 1. ValidateBasic (stateless — includes ValidateMAB: rejects // CouponDenomBread with FR-MAB-3). // 2. Idempotency: bond-id must not already exist (as a Bond, GrowthBond, or // MAB). // 3. StandKeeper shim: the issuer-stand-id must reference an existing Stand // (P1-02-01 edge). A nil shim skips (simtest wiring). // 4. FR-MAB-3 defense-in-depth: ValidateMAB re-check (rejects // CouponDenomBread — the handler re-checks in case of a future // ValidateBasic bypass). // 5. 3× annual surplus ceiling: checkMABIssuanceCeiling asserts // sum(existingMABPrincipal for poolID) + PrincipalGrain <= // MABIssuanceCeilingAnnualSurplusMultiple × AnnualSurplusAtIssuance. // REJECT if above ceiling. // 6. Coupon clamp via Clamp (A-563 — defense in depth). // 7. Persist the MAB with UseOfProceedsTag = MABUseOfProceedsReserveBuildOut // + record the pool-id in the mab-pool index. Emit bond.mab_issued. func (s msgServer) IssueMAB(ctx interface{}, msg *types.MsgIssueMAB) (*types.MsgIssueMABResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) // Idempotency: bond-id must not already exist (as Bond, GrowthBond, or MAB). if _, ok := s.Keeper.GetBond(sdkCtx, msg.BondID); ok { return nil, fmt.Errorf("bond: bond-id %q already exists (as a Bond)", msg.BondID) } if _, ok := s.Keeper.GetGrowthBond(sdkCtx, msg.BondID); ok { return nil, fmt.Errorf("bond: bond-id %q already exists (as a GrowthBond)", msg.BondID) } if _, ok := s.Keeper.GetMAB(sdkCtx, msg.BondID); ok { return nil, fmt.Errorf("bond: bond-id %q already exists (as a MAB)", msg.BondID) } // StandKeeper: issuer-stand-id must reference an existing Stand. if s.Keeper.standKeeper != nil { if !s.Keeper.standKeeper.StandExists(msg.IssuerStandID) { return nil, fmt.Errorf("bond: issuer-stand-id %q does not exist (IssueMAB rejected)", msg.IssuerStandID) } } // FR-MAB-3 defense-in-depth: re-run ValidateMAB (the handler re-checks // in case of a future ValidateBasic bypass). if err := types.ValidateMAB(types.MAB{CouponKind: msg.CouponKind}); err != nil { return nil, err } // 3× annual surplus ceiling (REQ-054 locked). ceilingMultiple, err := s.checkMABIssuanceCeiling(sdkCtx, msg.PoolID, msg.PrincipalGrain, msg.AnnualSurplusAtIssuance) if err != nil { return nil, err } // Coupon clamp (A-563 — defense in depth; ValidateBasic already // rejected out-of-band, so Clamp is a no-op here). clamped := types.Clamp(msg.CouponBps) m := types.IssueMAB(msg.BondID, msg.IssuerStandID, msg.PrincipalGrain, clamped, msg.CouponKind, msg.AnnualSurplusAtIssuance, msg.TermDays, sdkCtx.BlockTime().Unix(), sdkCtx.BlockTime().Unix()+int64(msg.TermDays)*24*60*60) s.Keeper.SetMAB(sdkCtx, m) s.Keeper.setMABPool(sdkCtx, msg.BondID, msg.PoolID) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.mab_issued", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("pool_id", msg.PoolID), sdk.NewAttribute("issuer_stand_id", msg.IssuerStandID), sdk.NewAttribute("coupon_bps", fmt.Sprintf("%d", clamped)), sdk.NewAttribute("coupon_kind", string(msg.CouponKind)), sdk.NewAttribute("use_of_proceeds_tag", m.UseOfProceedsTag), sdk.NewAttribute("ceiling_multiple", fmt.Sprintf("%d", ceilingMultiple)), )) return &types.MsgIssueMABResponse{ ClampedCouponBps: clamped, CeilingMultiple: ceilingMultiple, }, nil } // DebitMABProceeds debits a MAB's tagged proceeds to the Pool's // ReserveAccount (D-080). The handler enforces: // 1. ValidateBasic (stateless). // 2. The MAB must exist. // 3. D-080 tagged streaming: query the mab-pool index for the MAB's poolID, // then query CoverKeeper.GetPoolReserveAccount(poolID). If the // DestinationAccount != the pool's ReserveAccount -> StillKeeper.Still( // bondID, "MAB misuse — proceeds routed outside reserve") (D-089(1) — a // nil StillKeeper skips the Still recording but the handler STILL // REJECTS) AND REJECT. A nil CoverKeeper is a wiring error -> REJECT // (the destination cannot be validated). If match -> emit // bond.mab_proceeds_debited (simtest: the debit is the event; no actual // Grain transfer in P4). func (s msgServer) DebitMABProceeds(ctx interface{}, msg *types.MsgDebitMABProceeds) (*types.MsgDebitMABProceedsResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) m, ok := s.Keeper.GetMAB(sdkCtx, msg.BondID) if !ok { return nil, fmt.Errorf("bond: mab %q not found (DebitMABProceeds rejected)", msg.BondID) } _ = m poolID, ok := s.Keeper.GetMABPool(sdkCtx, msg.BondID) if !ok { return nil, fmt.Errorf("bond: mab %q has no pool binding (DebitMABProceeds rejected)", msg.BondID) } // D-080 tagged streaming: the destination must == the pool's // ReserveAccount. A nil CoverKeeper is a wiring error -> REJECT (the // destination cannot be validated). if s.Keeper.coverKeeper == nil { return nil, fmt.Errorf("bond: CoverKeeper shim not wired (DebitMABProceeds cannot validate destination — D-089(2) reverse edge required)") } reserveAccount, exists := s.Keeper.coverKeeper.GetPoolReserveAccount(poolID) if !exists { return nil, fmt.Errorf("bond: pool %q ReserveAccount not found (DebitMABProceeds rejected)", poolID) } if msg.DestinationAccount != reserveAccount { // D-080 misuse -> D-089(1) auto-Still. A nil StillKeeper skips the // Still recording but the handler STILL REJECTS (the debit is not // committed regardless). if s.Keeper.stillKeeper != nil { _ = s.Keeper.stillKeeper.Still(msg.BondID, "MAB misuse — proceeds routed outside reserve") } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.mab_proceeds_misuse", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("pool_id", poolID), sdk.NewAttribute("destination_account", msg.DestinationAccount), sdk.NewAttribute("expected_reserve_account", reserveAccount), )) return nil, fmt.Errorf("bond: MAB %q proceeds destination %q != pool %q ReserveAccount %q (D-080 tagged-streaming misuse — auto-Still + REJECT)", msg.BondID, msg.DestinationAccount, poolID, reserveAccount) } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.mab_proceeds_debited", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("pool_id", poolID), sdk.NewAttribute("destination_account", msg.DestinationAccount), )) return &types.MsgDebitMABProceedsResponse{}, nil } // WitnessMABProceedsRelease is a Watcher-witnessed release of a MAB's tagged // proceeds from staging to the reserve (D-080). The handler enforces: // 1. ValidateBasic (stateless). // 2. The MAB must exist. // 3. 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. A nil WatcherKeeper // skips the quorum check (simtest wiring — the handler still mutates // state; the simtest documents the wiring). func (s msgServer) WitnessMABProceedsRelease(ctx interface{}, msg *types.MsgWitnessMABProceedsRelease) (*types.MsgWitnessMABProceedsReleaseResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) if _, ok := s.Keeper.GetMAB(sdkCtx, msg.BondID); !ok { return nil, fmt.Errorf("bond: mab %q not found (WitnessMABProceedsRelease rejected)", msg.BondID) } // Watcher quorum (D-080). A nil WatcherKeeper skips the quorum check // (simtest wiring — the handler still mutates state). if s.Keeper.watcherKeeper != nil { if !s.Keeper.watcherKeeper.AttestMABRelease(msg.BondID, msg.AttestationRef) { return nil, fmt.Errorf("bond: MAB %q proceeds release rejected (Watcher quorum not met — D-080 6-of-9 required)", msg.BondID) } } sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.mab_proceeds_released", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("attestation_ref", msg.AttestationRef), )) return &types.MsgWitnessMABProceedsReleaseResponse{}, nil } // WatcherAttestMAB records a quarterly Watcher audit attestation on a MAB // (D-080). The handler enforces: // 1. ValidateBasic (stateless). // 2. The MAB must exist. // 3. Record the attestation (a store entry mab_attest// // -> attestationRef). Emit bond.mab_watcher_attested. func (s msgServer) WatcherAttestMAB(ctx interface{}, msg *types.MsgWatcherAttestMAB) (*types.MsgWatcherAttestMABResponse, error) { if err := msg.ValidateBasic(); err != nil { return nil, err } sdkCtx := unwrapCtx(ctx) if _, ok := s.Keeper.GetMAB(sdkCtx, msg.BondID); !ok { return nil, fmt.Errorf("bond: mab %q not found (WatcherAttestMAB rejected)", msg.BondID) } ts := sdkCtx.BlockTime().Unix() s.Keeper.SetMABAttest(sdkCtx, msg.BondID, ts, msg.AttestationRef) sdkCtx.EventManager().EmitEvent(sdk.NewEvent( "bond.mab_watcher_attested", sdk.NewAttribute("bond_id", msg.BondID), sdk.NewAttribute("attestation_ref", msg.AttestationRef), sdk.NewAttribute("timestamp", fmt.Sprintf("%d", ts)), )) return &types.MsgWatcherAttestMABResponse{}, nil }