Files
openyield/x/bond/keeper/msg_server_simtest_test.go
T
cloudinit-bot 9e7fc403f5 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---
2026-08-19 02:31:52 +00:00

1836 lines
72 KiB
Go
Raw 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_test
// msg_server_simtest_test.go is the x/bond keeper simtest (P6-03-01,
// REQ-038).
//
// D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no
// real Stand keeper (the StandKeeper shim is wired to a stub; G-003 test
// exemption). The simtest exercises:
//
// Bond issuance (coupon clamp at issuance):
// - IssueBond with coupon in-band (e.g., 500) -> recorded unchanged; no
// clamp event.
// - IssueBond with coupon above 800 (e.g., 1200) -> ValidateBasic REJECTS
// (stateless guard; the handler re-clamps at runtime — defense in
// depth, but ValidateBasic is the first gate).
// - IssueBond on a non-existent Stand (StandKeeper stub reports false) ->
// REJECTED (the bond is not created).
// - IssueBond on an existing bond-id -> idempotent reject.
// - Nil StandKeeper shim -> skips the StandExists check (simtest wiring).
//
// GrowthBond issuance + tick (growth clamp):
// - IssueGrowthBond with coupon + growth in-band -> recorded unchanged.
// - IssueGrowthBond with growth that would push post-growth above cap ->
// growth clamped to room (G-012).
// - TickGrowthBond -> coupon grows by growth-rate, clamped so post-growth
// <= cap.
// - TickGrowthBond on a non-GrowthBond -> REJECTED.
//
// CLOB matching (D-057 — price-time priority FCFS per REQ-007; NO AMM):
// - PlaceSecondaryOrder rests an order on the book.
// - MatchSecondaryOrder full fill: taker fills the resting order
// completely; resting order -> Filled (deleted from book).
// - MatchSecondaryOrder partial fill + rest: taker partially fills the
// resting order; resting order's remaining quantity is updated; taker
// is not rested (simplification — the taker is a one-shot match).
// - MatchSecondaryOrder no-match: taker price does not cross any resting
// order -> filled quantity 0; the resting book is unchanged.
// - CancelSecondaryOrder: resting order removed from book (Cancelled).
// - Price-time priority FCFS: at the same price, the earlier resting
// order fills first (by sequence).
//
// Per-match coupon clamp (D-063 REJECT above 800 — G-019 ImpliedCoupon):
// - A match within [0, 800] bps clears (clamp event emitted; the matched
// coupon is within band).
// - A match whose implied coupon EXCEEDS 800 bps (resting price-bps <
// 9200) is REJECTED (fails closed — D-063; the resting order stays on
// the book; the incoming taker is rejected; no refund path).
//
// G-019 ImpliedCoupon boundary unit test (800/801/799 bps):
// - price-bps 9200 -> ImpliedCoupon 800 (== cap, in-band, clears).
// - price-bps 9199 -> ImpliedCoupon 801 (> cap, REJECTED).
// - price-bps 9201 -> ImpliedCoupon 799 (< cap, in-band, clears).
//
// D-028 regression: CouponCapBps=800, CouponFloorBps=0 unchanged.
// REQ-030 cross-const test green (run in x/hub/types/cross_const_test.go;
// this simtest asserts the bond consts are the mission-locked values).
// G-003 import-invariant green (the production firewall test in
// x/window/types scans all x/ production files; this simtest is a test
// file, G-003-exempt).
//
// Coverage target: >=80% on x/bond/keeper.
import (
"strings"
"testing"
"time"
"cosmossdk.io/log"
"cosmossdk.io/store"
storetypes "cosmossdk.io/store/types"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/oy/openyield/x/bond/keeper"
btypes "github.com/oy/openyield/x/bond/types"
)
// --- Stub expected-keepers (G-003 test exemption) ---------------------------
// stubStandKeeper satisfies btypes.StandKeeper for the simtest. It returns
// the configured StandExists result per stand-id (default: exists=true).
type stubStandKeeper struct {
exists map[string]bool
existsAll bool
}
func (s *stubStandKeeper) StandExists(standID string) bool {
if s.exists != nil {
return s.exists[standID]
}
return s.existsAll
}
// stubCoverKeeper satisfies btypes.CoverKeeper for the v0.7 P4 MAB simtest
// (D-089(2) reverse edge). It returns the configured ReserveAccount per
// pool-id.
type stubCoverKeeper struct {
reserveAccounts map[string]string
}
func (s *stubCoverKeeper) GetPoolReserveAccount(poolID string) (string, bool) {
if s.reserveAccounts == nil {
return "", false
}
acc, ok := s.reserveAccounts[poolID]
return acc, ok
}
// stubWatcherKeeperBond satisfies btypes.WatcherKeeper for the v0.7 P4 MAB
// simtest. It returns a configurable quorum-met bool per
// AttestMABRelease call.
type stubWatcherKeeperBond struct {
quorumMet bool
}
func (s *stubWatcherKeeperBond) AttestMABRelease(bondID string, attestationRef string) bool {
return s.quorumMet
}
// stubStillKeeperBond satisfies btypes.StillKeeper for the v0.7 P4 MAB
// simtest (D-089(1)). It records every Still() call for assertion (the
// tagged-streaming misuse simtest asserts Still was called with the right
// bond-id + reason).
type stubStillKeeperBond struct {
calls []struct {
bondID string
reason string
}
}
func (s *stubStillKeeperBond) Still(bondID string, reason string) error {
s.calls = append(s.calls, struct {
bondID string
reason string
}{bondID, reason})
return nil
}
// --- Simtest context helper --------------------------------------------------
// newSimtestContext constructs an in-memory sdk.Context with a KVStore
// mounted at the bond store key. D-054: in-memory, no real Stand keeper.
// Returns the ctx, the stub StandKeeper, and the Keeper.
func newSimtestContext(t *testing.T) (sdk.Context, *stubStandKeeper, keeper.Keeper) {
t.Helper()
db := dbm.NewMemDB()
cdc := newTestCodec()
storeKey := storetypes.NewKVStoreKey(btypes.StoreKey)
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
if err := cms.LoadLatestVersion(); err != nil {
t.Fatalf("load latest version: %v", err)
}
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
sk := &stubStandKeeper{existsAll: true}
k := keeper.NewKeeper(cdc, storeKey, sk)
return ctx, sk, k
}
// newTestCodec constructs a minimal codec for the simtest.
func newTestCodec() codec.Codec {
registry := codectypes.NewInterfaceRegistry()
return codec.NewProtoCodec(registry)
}
// hasEvent reports whether ctx emitted an event of the given type.
func hasEvent(ctx sdk.Context, eventType string) bool {
for _, ev := range ctx.EventManager().Events() {
if ev.Type == eventType {
return true
}
}
return false
}
// eventAttr returns the value of an attribute on the last event of the
// given type, or "" if not found.
func eventAttr(ctx sdk.Context, eventType, attrKey string) string {
for _, ev := range ctx.EventManager().Events() {
if ev.Type == eventType {
for _, a := range ev.Attributes {
if string(a.Key) == attrKey {
return string(a.Value)
}
}
}
}
return ""
}
// eventCount returns the number of events of the given type emitted on ctx.
func eventCount(ctx sdk.Context, eventType string) int {
n := 0
for _, ev := range ctx.EventManager().Events() {
if ev.Type == eventType {
n++
}
}
return n
}
// freshCtx returns a fresh ctx (no prior events) on the same multi-store,
// so event assertions per-test are isolated. The keeper is shared (state
// persists across calls within a test; tests that need a fresh store call
// newSimtestContext instead).
func freshCtx(t *testing.T) (sdk.Context, *stubStandKeeper, keeper.Keeper) {
return newSimtestContext(t)
}
// newMABSimtestContext constructs an in-memory sdk.Context with the MAB
// shims (CoverKeeper + WatcherKeeper + StillKeeper) wired for the v0.7 P4
// MAB simtest (D-089(1) + D-089(2)). Returns the ctx, the four stubs, and
// the Keeper.
func newMABSimtestContext(t *testing.T) (sdk.Context, *stubStandKeeper, *stubCoverKeeper, *stubWatcherKeeperBond, *stubStillKeeperBond, keeper.Keeper) {
t.Helper()
db := dbm.NewMemDB()
cdc := newTestCodec()
storeKey := storetypes.NewKVStoreKey(btypes.StoreKey)
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
if err := cms.LoadLatestVersion(); err != nil {
t.Fatalf("load latest version: %v", err)
}
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
sk := &stubStandKeeper{existsAll: true}
ck := &stubCoverKeeper{reserveAccounts: map[string]string{"pool-1": "reserve-acc-1"}}
wk := &stubWatcherKeeperBond{quorumMet: true}
stK := &stubStillKeeperBond{}
k := keeper.NewKeeper(cdc, storeKey, sk)
k.SetCoverKeeper(ck)
k.SetWatcherKeeper(wk)
k.SetStillKeeper(stK)
return ctx, sk, ck, wk, stK, k
}
// --- Bond issuance (coupon clamp at issuance) --------------------------------
// TestIssueBondInBand asserts an in-band coupon (500) is recorded unchanged
// and the bond.issued event is emitted.
func TestIssueBondInBand(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
resp, err := srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b1", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueBond: %v", err)
}
if resp.ClampedCouponBps != 500 {
t.Errorf("ClampedCouponBps = %d, want 500 (in-band, unchanged)", resp.ClampedCouponBps)
}
if !hasEvent(ctx, "bond.issued") {
t.Error("bond.issued event not emitted")
}
// Read it back.
b, ok := k.GetBond(ctx, "b1")
if !ok {
t.Fatal("bond not persisted")
}
if b.CouponBps != 500 {
t.Errorf("persisted CouponBps = %d, want 500", b.CouponBps)
}
if b.Status != btypes.BondIssued {
t.Errorf("Status = %q, want BondIssued", b.Status)
}
}
// TestIssueBondAboveCapRejectedAtValidateBasic asserts an above-cap coupon
// (1200) is REJECTED at ValidateBasic (the stateless guard; D-028).
func TestIssueBondAboveCapRejectedAtValidateBasic(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b2", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 1200, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err == nil {
t.Error("IssueBond with above-cap coupon should be REJECTED at ValidateBasic")
}
if !strings.Contains(err.Error(), "out of band") {
t.Errorf("err = %q, want 'out of band'", err.Error())
}
}
// TestIssueBondNonExistentStandRejected asserts a non-existent Stand
// REJECTS the issuance (the StandKeeper shim reports false).
func TestIssueBondNonExistentStandRejected(t *testing.T) {
ctx, sk, k := newSimtestContext(t)
sk.exists = map[string]bool{"stand-1": false}
sk.existsAll = false
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b3", IssuerStandID: "no-such-stand", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err == nil {
t.Error("IssueBond on non-existent Stand should be REJECTED")
}
if !strings.Contains(err.Error(), "does not exist") {
t.Errorf("err = %q, want 'does not exist'", err.Error())
}
}
// TestIssueBondIdempotentReject asserts issuing the same bond-id twice
// REJECTS the second issuance.
func TestIssueBondIdempotentReject(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b4", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("first IssueBond: %v", err)
}
_, err = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b4", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 600, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err == nil {
t.Error("second IssueBond on same bond-id should be REJECTED")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("err = %q, want 'already exists'", err.Error())
}
}
// TestIssueBondNilStandKeeperSkipsCheck asserts a nil StandKeeper shim skips
// the StandExists check (simtest wiring — the handler still mutates state).
func TestIssueBondNilStandKeeperSkipsCheck(t *testing.T) {
ctx, _, k := newSimtestContext(t)
k.SetStandKeeper(nil) // nil shim — skip StandExists check
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b5", IssuerStandID: "any-stand", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueBond with nil StandKeeper should skip the check, got: %v", err)
}
}
// --- GrowthBond issuance + tick ----------------------------------------------
// TestIssueGrowthBondInBand asserts an in-band coupon + growth are recorded
// unchanged.
func TestIssueGrowthBondInBand(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
resp, err := srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb1", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 200, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueGrowthBond: %v", err)
}
if resp.ClampedCouponBps != 500 {
t.Errorf("ClampedCouponBps = %d, want 500", resp.ClampedCouponBps)
}
if resp.ClampedGrowthRateBps != 200 {
t.Errorf("ClampedGrowthRateBps = %d, want 200", resp.ClampedGrowthRateBps)
}
if !hasEvent(ctx, "bond.growth_issued") {
t.Error("bond.growth_issued event not emitted")
}
}
// TestIssueGrowthBondGrowthClampedToRoom asserts a growth-rate that would
// push post-growth above cap is clamped to the room-to-cap (G-012).
func TestIssueGrowthBondGrowthClampedToRoom(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
// coupon=500, cap=800, room=300. growth=400 -> clamped to 300.
resp, err := srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb2", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 400, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueGrowthBond: %v", err)
}
if resp.ClampedCouponBps != 500 {
t.Errorf("ClampedCouponBps = %d, want 500", resp.ClampedCouponBps)
}
if resp.ClampedGrowthRateBps != 300 {
t.Errorf("ClampedGrowthRateBps = %d, want 300 (room=300, G-012)", resp.ClampedGrowthRateBps)
}
if !hasEvent(ctx, "bond.growth_coupon_clamped") {
t.Error("bond.growth_coupon_clamped event not emitted (growth was clamped)")
}
}
// TestTickGrowthBond asserts a growth tick grows the coupon by the growth-
// rate, clamped so post-growth <= cap.
func TestTickGrowthBond(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
// Issue a GrowthBond: coupon=500, growth=200 (room=300; growth<room).
_, err := srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb3", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 200, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueGrowthBond: %v", err)
}
// Tick: 500 + 200 = 700 (<= cap 800).
resp, err := srv.TickGrowthBond(ctx, &btypes.MsgTickGrowthBond{BondID: "gb3", Signer: "stand-1"})
if err != nil {
t.Fatalf("TickGrowthBond: %v", err)
}
if resp.PostGrowthCouponBps != 700 {
t.Errorf("PostGrowthCouponBps = %d, want 700", resp.PostGrowthCouponBps)
}
// Tick again: 700 + 200 = 900, but ClampGrowth(700, 200) = 100 (room=100);
// post-growth = 700 + 100 = 800 (== cap).
resp2, err := srv.TickGrowthBond(ctx, &btypes.MsgTickGrowthBond{BondID: "gb3", Signer: "stand-1"})
if err != nil {
t.Fatalf("second TickGrowthBond: %v", err)
}
if resp2.PostGrowthCouponBps != 800 {
t.Errorf("PostGrowthCouponBps after second tick = %d, want 800 (clamped to cap)", resp2.PostGrowthCouponBps)
}
// Tick again: 800 + 200 -> ClampGrowth(800, 200) = 0 (at cap, no room);
// post-growth = 800 + 0 = 800.
resp3, err := srv.TickGrowthBond(ctx, &btypes.MsgTickGrowthBond{BondID: "gb3", Signer: "stand-1"})
if err != nil {
t.Fatalf("third TickGrowthBond: %v", err)
}
if resp3.PostGrowthCouponBps != 800 {
t.Errorf("PostGrowthCouponBps after third tick = %d, want 800 (at cap, no room)", resp3.PostGrowthCouponBps)
}
}
// TestTickGrowthBondNotFound asserts TickGrowthBond on a non-GrowthBond is
// REJECTED.
func TestTickGrowthBondNotFound(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.TickGrowthBond(ctx, &btypes.MsgTickGrowthBond{BondID: "no-such-bond", Signer: "stand-1"})
if err == nil {
t.Error("TickGrowthBond on non-existent bond should be REJECTED")
}
}
// --- CLOB matching: Place + Match full fill --------------------------------
// TestPlaceSecondaryOrder rests an order on the book.
func TestPlaceSecondaryOrder(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
// Issue a bond first (the order rests on an issued bond).
_, err := srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b10", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueBond: %v", err)
}
_, err = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "o1", BondID: "b10", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-1", Signer: "holder-1",
})
if err != nil {
t.Fatalf("PlaceSecondaryOrder: %v", err)
}
if !hasEvent(ctx, "bond.order_placed") {
t.Error("bond.order_placed event not emitted")
}
// The order is on the book.
ro, ok := k.GetRestingOrder(ctx, "o1")
if !ok {
t.Fatal("resting order not persisted")
}
if ro.Order.Status != btypes.OrderOpen {
t.Errorf("Status = %q, want Open", ro.Order.Status)
}
if ro.PriceBps != 9500 {
t.Errorf("PriceBps = %d, want 9500", ro.PriceBps)
}
if ro.RemainingQuantityGrain != 100 {
t.Errorf("RemainingQuantityGrain = %d, want 100", ro.RemainingQuantityGrain)
}
}
// TestPlaceSecondaryOrderNonExistentBondRejected asserts placing an order on
// a non-existent bond is REJECTED.
func TestPlaceSecondaryOrderNonExistentBondRejected(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "o2", BondID: "no-such-bond", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-1", Signer: "holder-1",
})
if err == nil {
t.Error("PlaceSecondaryOrder on non-existent bond should be REJECTED")
}
}
// TestPlaceSecondaryOrderIdempotentReject asserts placing the same order-id
// twice REJECTS the second.
func TestPlaceSecondaryOrderIdempotentReject(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b11", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
_, err := srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "o3", BondID: "b11", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-1", Signer: "holder-1",
})
if err != nil {
t.Fatalf("first PlaceSecondaryOrder: %v", err)
}
_, err = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "o3", BondID: "b11", Side: btypes.OrderSell, PriceBps: 9600,
QuantityGrain: 100, HolderReachID: "holder-1", Signer: "holder-1",
})
if err == nil {
t.Error("second PlaceSecondaryOrder on same order-id should be REJECTED")
}
}
// TestMatchSecondaryOrderFullFill asserts a taker fully fills a resting
// order; the resting order is removed from the book (Filled).
func TestMatchSecondaryOrderFullFill(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b20", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell order at price 9500 (implied coupon 500 bps, in-band).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-1", BondID: "b20", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-sell", Signer: "holder-sell",
})
// Buy taker at price 9500 (willing to pay up to 9500; matches the Sell).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-1", BondID: "b20", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-buy", Signer: "holder-buy",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder: %v", err)
}
if resp.Rejected {
t.Error("Rejected = true, want false (in-band match)")
}
if resp.FilledQuantityGrain != 100 {
t.Errorf("FilledQuantityGrain = %d, want 100 (full fill)", resp.FilledQuantityGrain)
}
// The resting order is removed (Filled).
if _, ok := k.GetRestingOrder(ctx, "sell-1"); ok {
t.Error("resting order should be removed after full fill")
}
// A match event was emitted.
if !hasEvent(ctx, "bond.match") {
t.Error("bond.match event not emitted")
}
if !hasEvent(ctx, "bond.match_completed") {
t.Error("bond.match_completed event not emitted")
}
}
// TestMatchSecondaryOrderPartialFillRest asserts a taker partially fills a
// resting order; the resting order's remaining quantity is updated.
func TestMatchSecondaryOrderPartialFillRest(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b21", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell order at 9500 for 100.
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-2", BondID: "b21", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-sell", Signer: "holder-sell",
})
// Buy taker at 9500 for 40 (partial fill).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-2", BondID: "b21", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 40, HolderReachID: "holder-buy", Signer: "holder-buy",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder: %v", err)
}
if resp.FilledQuantityGrain != 40 {
t.Errorf("FilledQuantityGrain = %d, want 40 (partial fill)", resp.FilledQuantityGrain)
}
// The resting order is still on the book with 60 remaining.
ro, ok := k.GetRestingOrder(ctx, "sell-2")
if !ok {
t.Fatal("resting order should still be on the book after partial fill")
}
if ro.RemainingQuantityGrain != 60 {
t.Errorf("RemainingQuantityGrain = %d, want 60 (100 - 40)", ro.RemainingQuantityGrain)
}
}
// TestMatchSecondaryOrderNoMatch asserts a taker whose price does not cross
// any resting order results in filled quantity 0 (the resting book is
// unchanged).
func TestMatchSecondaryOrderNoMatch(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b22", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell order at 9500.
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-3", BondID: "b22", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-sell", Signer: "holder-sell",
})
// Buy taker at 9400 (below the Sell price — no cross).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-3", BondID: "b22", Side: btypes.OrderBuy, PriceBps: 9400,
QuantityGrain: 100, HolderReachID: "holder-buy", Signer: "holder-buy",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder: %v", err)
}
if resp.FilledQuantityGrain != 0 {
t.Errorf("FilledQuantityGrain = %d, want 0 (no cross)", resp.FilledQuantityGrain)
}
// The resting order is unchanged.
ro, ok := k.GetRestingOrder(ctx, "sell-3")
if !ok {
t.Fatal("resting order should still be on the book (no match)")
}
if ro.RemainingQuantityGrain != 100 {
t.Errorf("RemainingQuantityGrain = %d, want 100 (unchanged)", ro.RemainingQuantityGrain)
}
}
// --- CancelSecondaryOrder ---------------------------------------------------
// TestCancelSecondaryOrder asserts cancelling a resting order removes it
// from the book.
func TestCancelSecondaryOrder(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b30", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "o-cancel", BondID: "b30", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "holder-sell", Signer: "holder-sell",
})
_, err := srv.CancelSecondaryOrder(ctx, &btypes.MsgCancelSecondaryOrder{OrderID: "o-cancel", Signer: "holder-sell"})
if err != nil {
t.Fatalf("CancelSecondaryOrder: %v", err)
}
if !hasEvent(ctx, "bond.order_cancelled") {
t.Error("bond.order_cancelled event not emitted")
}
// The order is removed from the book.
if _, ok := k.GetRestingOrder(ctx, "o-cancel"); ok {
t.Error("resting order should be removed after cancel")
}
}
// TestCancelSecondaryOrderNotFound asserts cancelling a non-existent order
// is REJECTED.
func TestCancelSecondaryOrderNotFound(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.CancelSecondaryOrder(ctx, &btypes.MsgCancelSecondaryOrder{OrderID: "no-such-order", Signer: "holder-sell"})
if err == nil {
t.Error("CancelSecondaryOrder on non-existent order should be REJECTED")
}
}
// --- Price-time priority FCFS (REQ-007) --------------------------------------
// TestPriceTimePriorityFCFS asserts at the same price, the earlier resting
// order fills first (by sequence). Two Sell orders at the same price 9500;
// a Buy taker at 9500 for 50 fills the FIRST resting order (lower sequence)
// completely, leaving the second untouched.
func TestPriceTimePriorityFCFS(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b40", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest two Sell orders at the SAME price 9500 (implied coupon 500, in-band).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-first", BondID: "b40", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-second", BondID: "b40", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "h2", Signer: "h2",
})
// Buy taker at 9500 for 50 — should fill the FIRST resting order (lower
// sequence).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-fcfs", BondID: "b40", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 50, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder: %v", err)
}
if resp.FilledQuantityGrain != 50 {
t.Errorf("FilledQuantityGrain = %d, want 50", resp.FilledQuantityGrain)
}
// The FIRST resting order has 50 remaining (100 - 50); the SECOND is
// untouched at 100.
ro1, ok := k.GetRestingOrder(ctx, "sell-first")
if !ok {
t.Fatal("sell-first should still be on the book (partial fill)")
}
if ro1.RemainingQuantityGrain != 50 {
t.Errorf("sell-first RemainingQuantityGrain = %d, want 50 (FCFS — first fills first)", ro1.RemainingQuantityGrain)
}
ro2, ok := k.GetRestingOrder(ctx, "sell-second")
if !ok {
t.Fatal("sell-second should still be on the book (untouched)")
}
if ro2.RemainingQuantityGrain != 100 {
t.Errorf("sell-second RemainingQuantityGrain = %d, want 100 (untouched — FCFS)", ro2.RemainingQuantityGrain)
}
}
// TestPriceTimePriorityBestPriceFirst asserts the best price fills first
// (lowest Sell price for a Buy taker). A Sell at 9400 fills before a Sell at
// 9500 for a Buy taker.
func TestPriceTimePriorityBestPriceFirst(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b41", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell at 9500 (implied coupon 500) FIRST (lower sequence).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-9500", BondID: "b41", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
// Rest a Sell at 9400 (implied coupon 600 — better price for the buyer)
// SECOND (higher sequence).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-9400", BondID: "b41", Side: btypes.OrderSell, PriceBps: 9400,
QuantityGrain: 100, HolderReachID: "h2", Signer: "h2",
})
// Buy taker at 9500 for 50 — should fill the 9400 Sell FIRST (best price,
// even though it has a higher sequence — price beats sequence).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-best", BondID: "b41", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 50, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder: %v", err)
}
if resp.FilledQuantityGrain != 50 {
t.Errorf("FilledQuantityGrain = %d, want 50", resp.FilledQuantityGrain)
}
// The 9400 Sell has 50 remaining (filled first — best price); the 9500
// Sell is untouched at 100.
ro9400, ok := k.GetRestingOrder(ctx, "sell-9400")
if !ok {
t.Fatal("sell-9400 should still be on the book (partial fill)")
}
if ro9400.RemainingQuantityGrain != 50 {
t.Errorf("sell-9400 RemainingQuantityGrain = %d, want 50 (best price fills first)", ro9400.RemainingQuantityGrain)
}
ro9500, ok := k.GetRestingOrder(ctx, "sell-9500")
if !ok {
t.Fatal("sell-9500 should still be on the book (untouched — worse price)")
}
if ro9500.RemainingQuantityGrain != 100 {
t.Errorf("sell-9500 RemainingQuantityGrain = %d, want 100 (untouched — worse price)", ro9500.RemainingQuantityGrain)
}
}
// --- Per-match coupon clamp (D-063 REJECT above 800 — G-019 ImpliedCoupon) ---
// TestMatchInBandClears asserts a match within [0, 800] bps clears (the
// matched coupon is within band; clamp event emitted).
func TestMatchInBandClears(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b50", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell at 9250 (implied coupon 750 bps, in-band).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-750", BondID: "b50", Side: btypes.OrderSell, PriceBps: 9250,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
// Buy taker at 9250 (matches).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-750", BondID: "b50", Side: btypes.OrderBuy, PriceBps: 9250,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder: %v", err)
}
if resp.Rejected {
t.Error("Rejected = true, want false (in-band 750 bps clears)")
}
if resp.FilledQuantityGrain != 100 {
t.Errorf("FilledQuantityGrain = %d, want 100", resp.FilledQuantityGrain)
}
// The match event carries the clamped coupon (750, in-band).
attr := eventAttr(ctx, "bond.match", "matched_coupon_bps")
if attr != "750" {
t.Errorf("matched_coupon_bps = %q, want 750 (in-band)", attr)
}
}
// TestMatchAboveCapRejected asserts a match whose implied coupon EXCEEDS 800
// bps (resting price-bps < 9200) is REJECTED (fails closed — D-063). The
// resting order stays on the book; the incoming taker is rejected.
func TestMatchAboveCapRejected(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b51", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell at 9000 (implied coupon 1000 bps, ABOVE cap 800).
// PlaceSecondaryOrder does NOT reject (a resting order may rest at any
// price; the REJECT is at MATCH time per D-063).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-1000", BondID: "b51", Side: btypes.OrderSell, PriceBps: 9000,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
// Buy taker at 9000 (matches the price, but the implied coupon is above
// cap -> REJECTED per D-063).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-1000", BondID: "b51", Side: btypes.OrderBuy, PriceBps: 9000,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err == nil {
t.Error("MatchSecondaryOrder above cap should be REJECTED (D-063)")
}
if !resp.Rejected {
t.Error("Rejected = false, want true (above-cap match — D-063 fails closed)")
}
if resp.FilledQuantityGrain != 0 {
t.Errorf("FilledQuantityGrain = %d, want 0 (rejected — no fill)", resp.FilledQuantityGrain)
}
// The resting order STAYS on the book (D-063 — the resting order is not
// consumed by a rejected match).
ro, ok := k.GetRestingOrder(ctx, "sell-1000")
if !ok {
t.Fatal("resting order should STAY on the book after D-063 reject")
}
if ro.RemainingQuantityGrain != 100 {
t.Errorf("RemainingQuantityGrain = %d, want 100 (resting order unchanged)", ro.RemainingQuantityGrain)
}
// The reject event was emitted.
if !hasEvent(ctx, "bond.match_rejected_above_cap") {
t.Error("bond.match_rejected_above_cap event not emitted")
}
}
// --- G-019 ImpliedCoupon boundary unit test (800/801/799 bps) ---------------
// TestImpliedCouponBoundary asserts the G-019 ImpliedCoupon helper at the
// 800-bps cap boundary:
// - price-bps 9200 -> ImpliedCoupon 800 (== cap, in-band, clears via Clamp).
// - price-bps 9199 -> ImpliedCoupon 801 (> cap, REJECTED — D-063).
// - price-bps 9201 -> ImpliedCoupon 799 (< cap, in-band, clears).
//
// This is the G-019 BINDING boundary unit test — a single helper + boundary
// test closing the formula ambiguity in the D-063 REJECT threshold.
func TestImpliedCouponBoundary(t *testing.T) {
cases := []struct {
priceBps uint32
wantCoupon uint32
description string
}{
{9200, 800, "at cap (800) — in-band, clears"},
{9199, 801, "above cap (801) — REJECTED per D-063"},
{9201, 799, "below cap (799) — in-band, clears"},
{10000, 0, "par — 0 implied coupon"},
{10500, 0, "premium — 0 implied coupon (floored at 0)"},
{9000, 1000, "deep discount — 1000 bps implied coupon"},
{0, 10000, "zero price — 10000 bps implied coupon"},
}
for _, c := range cases {
got := keeper.ImpliedCoupon(c.priceBps, 0)
if got != c.wantCoupon {
t.Errorf("ImpliedCoupon(%d, 0) = %d, want %d (%s)", c.priceBps, got, c.wantCoupon, c.description)
}
}
}
// TestImpliedCouponBoundaryAtCapClears asserts a match at exactly the cap
// (800 bps, price-bps 9200) clears (in-band — the cap is inclusive; the
// REJECT is strictly above 800 per D-063).
func TestImpliedCouponBoundaryAtCapClears(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b60", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell at 9200 (implied coupon 800, == cap).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-800", BondID: "b60", Side: btypes.OrderSell, PriceBps: 9200,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-800", BondID: "b60", Side: btypes.OrderBuy, PriceBps: 9200,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder at cap: %v", err)
}
if resp.Rejected {
t.Error("Rejected = true, want false (at-cap 800 bps clears — D-063 rejects strictly above 800)")
}
}
// TestImpliedCouponBoundaryAboveCapRejected asserts a match at 801 bps
// (price-bps 9199) is REJECTED (D-063).
func TestImpliedCouponBoundaryAboveCapRejected(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b61", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell at 9199 (implied coupon 801, ABOVE cap).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-801", BondID: "b61", Side: btypes.OrderSell, PriceBps: 9199,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-801", BondID: "b61", Side: btypes.OrderBuy, PriceBps: 9199,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err == nil {
t.Error("MatchSecondaryOrder at 801 bps should be REJECTED (D-063)")
}
if !resp.Rejected {
t.Error("Rejected = false, want true (801 bps > cap 800 — D-063)")
}
}
// --- D-028 regression: 8%/0% consts unchanged --------------------------------
// TestCouponCapBpsUnchanged asserts CouponCapBps is 800 (D-028 — the 8%
// mission-locked cap is unchanged by the P6 runtime promotion).
func TestCouponCapBpsUnchanged(t *testing.T) {
if btypes.CouponCapBps != 800 {
t.Errorf("CouponCapBps = %d, want 800 (D-028 mission-locked 8pct — unchanged by P6)", btypes.CouponCapBps)
}
}
// TestCouponFloorBpsUnchanged asserts CouponFloorBps is 0 (D-028 — the 0%
// mission-locked floor is unchanged by the P6 runtime promotion).
func TestCouponFloorBpsUnchanged(t *testing.T) {
if btypes.CouponFloorBps != 0 {
t.Errorf("CouponFloorBps = %d, want 0 (D-028 mission-locked 0pct — unchanged by P6)", btypes.CouponFloorBps)
}
}
// TestOrderSideCountUnchanged asserts OrderSideCount is 2 (locked-const
// regression — the P6 runtime does not change the v0.3 OrderSide enum).
func TestOrderSideCountUnchanged(t *testing.T) {
if btypes.OrderSideCount != 2 {
t.Errorf("OrderSideCount = %d, want 2 (A-313 locked-const — unchanged by P6)", btypes.OrderSideCount)
}
}
// TestOrderStatusCountUnchanged asserts OrderStatusCount is 3 (locked-const
// regression — the P6 runtime does not change the v0.3 OrderStatus enum).
func TestOrderStatusCountUnchanged(t *testing.T) {
if btypes.OrderStatusCount != 3 {
t.Errorf("OrderStatusCount = %d, want 3 (A-313 locked-const — unchanged by P6)", btypes.OrderStatusCount)
}
}
// --- MatchSecondaryOrder on a GrowthBond + non-existent bond ----------------
// TestMatchSecondaryOrderOnGrowthBond asserts a match works on a GrowthBond
// (the order rests on an issued GrowthBond too).
func TestMatchSecondaryOrderOnGrowthBond(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb50", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 100, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-gb", BondID: "gb50", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-gb", BondID: "gb50", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder on GrowthBond: %v", err)
}
if resp.FilledQuantityGrain != 100 {
t.Errorf("FilledQuantityGrain = %d, want 100", resp.FilledQuantityGrain)
}
}
// TestMatchSecondaryOrderNonExistentBondRejected asserts a match on a non-
// existent bond is REJECTED.
func TestMatchSecondaryOrderNonExistentBondRejected(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-x", BondID: "no-such-bond", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err == nil {
t.Error("MatchSecondaryOrder on non-existent bond should be REJECTED")
}
}
// --- ValidateBasic error paths (coverage) -----------------------------------
// TestValidateBasicErrorPaths exercises each Msg* ValidateBasic error path
// to push coverage >=80%.
func TestValidateBasicErrorPaths(t *testing.T) {
// MsgIssueBond
if err := (&btypes.MsgIssueBond{}).ValidateBasic(); err == nil {
t.Error("empty MsgIssueBond should fail ValidateBasic")
}
if err := (&btypes.MsgIssueBond{BondID: "x", IssuerStandID: "s", PrincipalGrain: 1, CouponBps: 900}).ValidateBasic(); err == nil {
t.Error("above-cap MsgIssueBond should fail ValidateBasic")
}
// MsgIssueGrowthBond
if err := (&btypes.MsgIssueGrowthBond{}).ValidateBasic(); err == nil {
t.Error("empty MsgIssueGrowthBond should fail ValidateBasic")
}
// MsgTickGrowthBond
if err := (&btypes.MsgTickGrowthBond{}).ValidateBasic(); err == nil {
t.Error("empty MsgTickGrowthBond should fail ValidateBasic")
}
// MsgPlaceSecondaryOrder
if err := (&btypes.MsgPlaceSecondaryOrder{}).ValidateBasic(); err == nil {
t.Error("empty MsgPlaceSecondaryOrder should fail ValidateBasic")
}
if err := (&btypes.MsgPlaceSecondaryOrder{OrderID: "x", BondID: "b", Side: "Bogus", QuantityGrain: 1, Signer: "s"}).ValidateBasic(); err == nil {
t.Error("bad-side MsgPlaceSecondaryOrder should fail ValidateBasic")
}
if err := (&btypes.MsgPlaceSecondaryOrder{OrderID: "x", BondID: "b", Side: btypes.OrderBuy, QuantityGrain: 0, Signer: "s"}).ValidateBasic(); err == nil {
t.Error("zero-quantity MsgPlaceSecondaryOrder should fail ValidateBasic")
}
// MsgCancelSecondaryOrder
if err := (&btypes.MsgCancelSecondaryOrder{}).ValidateBasic(); err == nil {
t.Error("empty MsgCancelSecondaryOrder should fail ValidateBasic")
}
// MsgMatchSecondaryOrder
if err := (&btypes.MsgMatchSecondaryOrder{}).ValidateBasic(); err == nil {
t.Error("empty MsgMatchSecondaryOrder should fail ValidateBasic")
}
if err := (&btypes.MsgMatchSecondaryOrder{IncomingOrderID: "x", BondID: "b", Side: "Bogus", QuantityGrain: 1, Signer: "s"}).ValidateBasic(); err == nil {
t.Error("bad-side MsgMatchSecondaryOrder should fail ValidateBasic")
}
}
// --- Keeper accessors (coverage) --------------------------------------------
// TestKeeperAccessors exercises the exported Keeper accessors that the
// simtest above does not directly hit (AllBonds, AllGrowthBonds,
// AllRestingOrders empty paths; SetStandKeeper) to push coverage >=80%.
func TestKeeperAccessors(t *testing.T) {
ctx, sk, k := newSimtestContext(t)
_ = sk
// Empty-store accessors return empty (not nil) slices.
if got := k.AllBonds(ctx); len(got) != 0 {
t.Errorf("AllBonds empty = %d, want 0", len(got))
}
if got := k.AllGrowthBonds(ctx); len(got) != 0 {
t.Errorf("AllGrowthBonds empty = %d, want 0", len(got))
}
if got := k.AllRestingOrders(ctx); len(got) != 0 {
t.Errorf("AllRestingOrders empty = %d, want 0", len(got))
}
// Populate + read back.
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "acc-b", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
_, _ = srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "acc-gb", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 100, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if got := k.AllBonds(ctx); len(got) != 1 {
t.Errorf("AllBonds = %d, want 1", len(got))
}
if got := k.AllGrowthBonds(ctx); len(got) != 1 {
t.Errorf("AllGrowthBonds = %d, want 1", len(got))
}
// Marshal-error path on GetBond (corrupt bytes in store).
// Use the ctx's existing KVStore (the mounted store key) — creating a
// new store key here would panic (not mounted on the multi-store).
rawStore := ctx.KVStore(k.StoreKey())
rawStore.Set([]byte("bond/corrupt"), []byte("not-json"))
if _, ok := k.GetBond(ctx, "corrupt"); ok {
t.Error("GetBond on corrupt bytes should return false")
}
// Marshal-error path on GetGrowthBond (corrupt bytes).
rawStore.Set([]byte("growth/corrupt-gb"), []byte("not-json"))
if _, ok := k.GetGrowthBond(ctx, "corrupt-gb"); ok {
t.Error("GetGrowthBond on corrupt bytes should return false")
}
// Marshal-error path on GetRestingOrder (corrupt bytes).
rawStore.Set([]byte("order/corrupt-order"), []byte("not-json"))
if _, ok := k.GetRestingOrder(ctx, "corrupt-order"); ok {
t.Error("GetRestingOrder on corrupt bytes should return false")
}
// SetStandKeeper post-construction wiring coverage.
k.SetStandKeeper(nil)
}
// --- UnwrapCtx panic (coverage) ---------------------------------------------
// TestUnwrapCtxPanic asserts unwrapCtx panics on a non-sdk.Context value.
func TestUnwrapCtxPanic(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("unwrapCtx on non-sdk.Context should panic")
}
}()
_, _ = keeper.NewMsgServerImpl(keeper.Keeper{}).IssueBond("not-a-ctx",
&btypes.MsgIssueBond{BondID: "x", IssuerStandID: "s", PrincipalGrain: 1, CouponBps: 500, Signer: "s"})
}
// --- IssueGrowthBond idempotency + non-existent Stand ------------------------
// TestIssueGrowthBondIdempotentReject asserts issuing the same growth-bond-id
// twice REJECTS the second.
func TestIssueGrowthBondIdempotentReject(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb-dup", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 100, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("first IssueGrowthBond: %v", err)
}
_, err = srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb-dup", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 600, GrowthRateBps: 100, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err == nil {
t.Error("second IssueGrowthBond on same id should be REJECTED")
}
}
// TestIssueGrowthBondNonExistentStandRejected asserts a non-existent Stand
// REJECTS the GrowthBond issuance.
func TestIssueGrowthBondNonExistentStandRejected(t *testing.T) {
ctx, sk, k := newSimtestContext(t)
sk.exists = map[string]bool{"stand-1": false}
sk.existsAll = false
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueGrowthBond(ctx, &btypes.MsgIssueGrowthBond{
BondID: "gb-stand", IssuerStandID: "no-such-stand", PrincipalGrain: 1_000_000,
CouponBps: 500, GrowthRateBps: 100, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
if err == nil {
t.Error("IssueGrowthBond on non-existent Stand should be REJECTED")
}
}
// --- Sell taker against Buy resting orders (coverage of the Sell side) ------
// TestSellTakerMatchesBuyResting asserts a Sell taker matches against Buy
// resting orders (the opposite side of the Buy-taker tests above).
func TestSellTakerMatchesBuyResting(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b70", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Buy order at 9500 (implied coupon 500, in-band). A Buy bid is
// willing to pay UP TO 9500; a Sell taker at 9500 matches.
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "buy-rest", BondID: "b70", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
// Sell taker at 9500 (matches the Buy bid).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "sell-taker", BondID: "b70", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder Sell taker: %v", err)
}
if resp.FilledQuantityGrain != 100 {
t.Errorf("FilledQuantityGrain = %d, want 100", resp.FilledQuantityGrain)
}
}
// TestSellTakerNoCross asserts a Sell taker whose price does not cross the
// Buy resting order results in filled 0.
func TestSellTakerNoCross(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b71", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Buy at 9400 (bid — willing to pay up to 9400).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "buy-9400", BondID: "b71", Side: btypes.OrderBuy, PriceBps: 9400,
QuantityGrain: 100, HolderReachID: "h1", Signer: "h1",
})
// Sell taker at 9500 (above the Buy bid — no cross; the seller wants more
// than the buyer bids).
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "sell-taker", BondID: "b71", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder Sell taker no-cross: %v", err)
}
if resp.FilledQuantityGrain != 0 {
t.Errorf("FilledQuantityGrain = %d, want 0 (no cross)", resp.FilledQuantityGrain)
}
}
// --- Multiple matches in one taker (coverage) --------------------------------
// TestMatchTakerMultipleResting asserts a taker matches against multiple
// resting orders (filling against the best price first, then the next).
func TestMatchTakerMultipleResting(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b80", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest two Sell orders: one at 9400 (implied coupon 600, in-band) for 50,
// and one at 9500 (implied coupon 500, in-band) for 50.
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-9400", BondID: "b80", Side: btypes.OrderSell, PriceBps: 9400,
QuantityGrain: 50, HolderReachID: "h1", Signer: "h1",
})
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-9500", BondID: "b80", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 50, HolderReachID: "h2", Signer: "h2",
})
// Buy taker at 9500 for 100 — fills 50 at 9400 (best price, first) + 50
// at 9500 (next). Total filled = 100.
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-multi", BondID: "b80", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err != nil {
t.Fatalf("MatchSecondaryOrder multi: %v", err)
}
if resp.FilledQuantityGrain != 100 {
t.Errorf("FilledQuantityGrain = %d, want 100", resp.FilledQuantityGrain)
}
// Two match events emitted (one per resting fill).
if got := eventCount(ctx, "bond.match"); got != 2 {
t.Errorf("bond.match events = %d, want 2 (one per resting fill)", got)
}
// Both resting orders are removed (Filled).
if _, ok := k.GetRestingOrder(ctx, "sell-9400"); ok {
t.Error("sell-9400 should be removed (filled)")
}
if _, ok := k.GetRestingOrder(ctx, "sell-9500"); ok {
t.Error("sell-9500 should be removed (filled)")
}
}
// --- D-063 reject advances to no further resting (fails closed) --------------
// TestMatchAboveCapRejectStopsMatching asserts a D-063 REJECT on the best
// resting order STOPS matching (fails closed — the taker does not advance to
// the next resting order even if it is in-band). This is the mission-lock-
// true choice: the 8% cap is a hard invariant.
func TestMatchAboveCapRejectStopsMatching(t *testing.T) {
ctx, _, k := newSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, _ = srv.IssueBond(ctx, &btypes.MsgIssueBond{
BondID: "b90", IssuerStandID: "stand-1", PrincipalGrain: 1_000_000,
CouponBps: 500, TermDays: 365, IssuedAt: 1000, Maturity: 1365, Signer: "stand-1",
})
// Rest a Sell at 9000 (implied coupon 1000, ABOVE cap) — the BEST price
// for a Buy taker (lowest Sell price).
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-above", BondID: "b90", Side: btypes.OrderSell, PriceBps: 9000,
QuantityGrain: 50, HolderReachID: "h1", Signer: "h1",
})
// Rest a Sell at 9500 (implied coupon 500, in-band) — the WORSE price.
_, _ = srv.PlaceSecondaryOrder(ctx, &btypes.MsgPlaceSecondaryOrder{
OrderID: "sell-inband", BondID: "b90", Side: btypes.OrderSell, PriceBps: 9500,
QuantityGrain: 50, HolderReachID: "h2", Signer: "h2",
})
// Buy taker at 9500 for 100 — the best resting (9000) is ABOVE cap ->
// REJECTED (fails closed). The taker does NOT advance to the in-band
// 9500 order.
resp, err := srv.MatchSecondaryOrder(ctx, &btypes.MsgMatchSecondaryOrder{
IncomingOrderID: "buy-reject", BondID: "b90", Side: btypes.OrderBuy, PriceBps: 9500,
QuantityGrain: 100, HolderReachID: "hb", Signer: "hb",
})
if err == nil {
t.Error("MatchSecondaryOrder with above-cap best resting should be REJECTED (D-063)")
}
if !resp.Rejected {
t.Error("Rejected = false, want true (D-063 fails closed on the best resting)")
}
// The in-band 9500 order is UNTOUCHED (fails closed — no advance).
ro, ok := k.GetRestingOrder(ctx, "sell-inband")
if !ok {
t.Fatal("sell-inband should STAY on the book (D-063 fails closed — no advance)")
}
if ro.RemainingQuantityGrain != 50 {
t.Errorf("sell-inband RemainingQuantityGrain = %d, want 50 (untouched)", ro.RemainingQuantityGrain)
}
}
// --- v0.7 P4: MAB simtest (REQ-054, D-080, D-089(1), D-089(2)) ----------------
//
// (Mutual Aid Bond runtime — issuance + Bread-coupon rejection + 3× annual
// surplus ceiling + tagged-streaming misuse -> auto-Still + Watcher-witnessed
// release + quarterly attestation).
// TestMABIssuanceValidCoverCallCoupons (case a) asserts a MAB issuance with
// valid Cover-Call coupons (CouponDenomCoverCall) succeeds + the
// bond.mab_issued event is emitted + the UseOfProceedsTag is locked to
// "reserve_build_out".
func TestMABIssuanceValidCoverCallCoupons(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
resp, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
if resp.ClampedCouponBps != 500 {
t.Errorf("ClampedCouponBps = %d, want 500", resp.ClampedCouponBps)
}
if !hasEvent(ctx, "bond.mab_issued") {
t.Error("bond.mab_issued event not emitted")
}
// Read it back.
m, ok := k.GetMAB(ctx, "mab-1")
if !ok {
t.Fatal("MAB not persisted")
}
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)
}
// The mab-pool index recorded the pool binding.
poolID, ok := k.GetMABPool(ctx, "mab-1")
if !ok {
t.Fatal("mab-pool index not recorded")
}
if poolID != "pool-1" {
t.Errorf("mab-pool index = %q, want pool-1", poolID)
}
}
// TestMABIssuanceBreadCouponsRejected (case b) asserts a MAB issuance with
// Bread coupons (CouponDenomBread) is REJECTED at ValidateBasic (FR-MAB-3).
func TestMABIssuanceBreadCouponsRejected(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-bad", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomBread,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err == nil {
t.Fatal("IssueMAB with 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())
}
// The MAB was NOT persisted.
if _, ok := k.GetMAB(ctx, "mab-bad"); ok {
t.Error("MAB with Bread coupons should NOT be persisted")
}
}
// TestMABIssuanceAboveCeilingRejected (case c) asserts a MAB issuance that
// would push the total outstanding MAB principal above the 3× annual
// surplus ceiling is REJECTED (REQ-054 locked). Issue two MABs that
// together + a third exceed 3× annual surplus.
func TestMABIssuanceAboveCeilingRejected(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
// Annual surplus = 5M -> ceiling = 15M. Issue two MABs at 7M each
// (sum = 14M, within ceiling). A third at 2M would push the sum to
// 16M > 15M -> REJECT.
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-c1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 7_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("first IssueMAB: %v", err)
}
_, err = srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-c2", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 7_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomMutualAidCredit,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("second IssueMAB: %v", err)
}
// Third at 2M -> sum 16M > 15M ceiling -> REJECT.
_, err = srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-c3", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 2_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err == nil {
t.Fatal("third IssueMAB above 3× ceiling should be REJECTED")
}
if !strings.Contains(err.Error(), "ceiling breached") {
t.Errorf("err = %q, want 'ceiling breached'", err.Error())
}
}
// TestMABDebitProceedsMisuseAutoStill (case d) asserts a MAB proceeds debit
// with a destination != the Pool's ReserveAccount triggers the auto-Still
// (D-089(1)) AND is REJECTED (D-080 tagged-streaming misuse).
func TestMABDebitProceedsMisuseAutoStill(t *testing.T) {
ctx, _, _, _, stK, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
// Issue a MAB for pool-1 (whose ReserveAccount is "reserve-acc-1").
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-d1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
// Debit to a WRONG destination -> auto-Still + REJECT.
_, err = srv.DebitMABProceeds(ctx, &btypes.MsgDebitMABProceeds{
BondID: "mab-d1", DestinationAccount: "wrong-destination", Signer: "stand-1",
})
if err == nil {
t.Fatal("DebitMABProceeds with wrong destination should be REJECTED")
}
if !strings.Contains(err.Error(), "tagged-streaming misuse") {
t.Errorf("err = %q, want 'tagged-streaming misuse'", err.Error())
}
// The StillKeeper was called with the right bond-id + reason.
if len(stK.calls) != 1 {
t.Fatalf("StillKeeper.Still calls = %d, want 1", len(stK.calls))
}
if stK.calls[0].bondID != "mab-d1" {
t.Errorf("Still bondID = %q, want mab-d1", stK.calls[0].bondID)
}
if !strings.Contains(stK.calls[0].reason, "MAB misuse") {
t.Errorf("Still reason = %q, want 'MAB misuse'", stK.calls[0].reason)
}
// The misuse event was emitted.
if !hasEvent(ctx, "bond.mab_proceeds_misuse") {
t.Error("bond.mab_proceeds_misuse event not emitted")
}
}
// TestMABDebitProceedsMatchSucceeds asserts a MAB proceeds debit with the
// destination == the Pool's ReserveAccount succeeds + the
// bond.mab_proceeds_debited event is emitted.
func TestMABDebitProceedsMatchSucceeds(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-d2", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
// Debit to the CORRECT destination (reserve-acc-1) -> succeeds.
_, err = srv.DebitMABProceeds(ctx, &btypes.MsgDebitMABProceeds{
BondID: "mab-d2", DestinationAccount: "reserve-acc-1", Signer: "stand-1",
})
if err != nil {
t.Fatalf("DebitMABProceeds with matching destination: %v", err)
}
if !hasEvent(ctx, "bond.mab_proceeds_debited") {
t.Error("bond.mab_proceeds_debited event not emitted")
}
}
// TestMABWitnessProceedsReleaseQuorumPresent (case e) asserts a MAB
// proceeds release with Watcher quorum present succeeds + the
// bond.mab_proceeds_released event is emitted.
func TestMABWitnessProceedsReleaseQuorumPresent(t *testing.T) {
ctx, _, _, wk, _, k := newMABSimtestContext(t)
wk.quorumMet = true
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-w1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
_, err = srv.WitnessMABProceedsRelease(ctx, &btypes.MsgWitnessMABProceedsRelease{
BondID: "mab-w1", AttestationRef: "oy:attest:mab-w1", Signer: "watcher-1",
})
if err != nil {
t.Fatalf("WitnessMABProceedsRelease with quorum: %v", err)
}
if !hasEvent(ctx, "bond.mab_proceeds_released") {
t.Error("bond.mab_proceeds_released event not emitted")
}
}
// TestMABWitnessProceedsReleaseQuorumAbsent asserts a MAB proceeds release
// with Watcher quorum NOT met is REJECTED (D-080 — 6-of-9 required).
func TestMABWitnessProceedsReleaseQuorumAbsent(t *testing.T) {
ctx, _, _, wk, _, k := newMABSimtestContext(t)
wk.quorumMet = false
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-w2", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
_, err = srv.WitnessMABProceedsRelease(ctx, &btypes.MsgWitnessMABProceedsRelease{
BondID: "mab-w2", AttestationRef: "oy:attest:mab-w2", Signer: "watcher-1",
})
if err == nil {
t.Fatal("WitnessMABProceedsRelease without quorum should be REJECTED")
}
if !strings.Contains(err.Error(), "quorum not met") {
t.Errorf("err = %q, want 'quorum not met'", err.Error())
}
}
// TestMABWatcherAttest (case f) asserts a quarterly Watcher attestation on
// a MAB is recorded + the bond.mab_watcher_attested event is emitted.
func TestMABWatcherAttest(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-a1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
_, err = srv.WatcherAttestMAB(ctx, &btypes.MsgWatcherAttestMAB{
BondID: "mab-a1", AttestationRef: "oy:attest:quarterly:mab-a1", Signer: "watcher-1",
})
if err != nil {
t.Fatalf("WatcherAttestMAB: %v", err)
}
if !hasEvent(ctx, "bond.mab_watcher_attested") {
t.Error("bond.mab_watcher_attested event not emitted")
}
// The attestation was recorded.
atts := k.AllMABAttests(ctx, "mab-a1")
if len(atts) != 1 {
t.Fatalf("AllMABAttests = %d, want 1", len(atts))
}
if atts[0] != "oy:attest:quarterly:mab-a1" {
t.Errorf("attestation ref = %q, want oy:attest:quarterly:mab-a1", atts[0])
}
}
// TestMABIssueIdempotentReject asserts issuing the same MAB bond-id twice
// REJECTS the second.
func TestMABIssueIdempotentReject(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-i1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("first IssueMAB: %v", err)
}
_, err = srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-i1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 2_000_000, CouponBps: 600, CouponKind: btypes.CouponDenomMutualAidCredit,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err == nil {
t.Fatal("second IssueMAB on same bond-id should be REJECTED")
}
}
// TestMABIssueNonExistentStandRejected asserts a MAB issuance on a non-
// existent Stand is REJECTED (the StandKeeper stub reports false).
func TestMABIssueNonExistentStandRejected(t *testing.T) {
ctx, sk, _, _, _, k := newMABSimtestContext(t)
sk.exists = map[string]bool{"stand-1": false}
sk.existsAll = false
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-s1", PoolID: "pool-1", IssuerStandID: "no-such-stand",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err == nil {
t.Fatal("IssueMAB on non-existent Stand should be REJECTED")
}
}
// TestMABDebitProceedsNotFound asserts a debit on a non-existent MAB is
// REJECTED.
func TestMABDebitProceedsNotFound(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.DebitMABProceeds(ctx, &btypes.MsgDebitMABProceeds{
BondID: "no-such-mab", DestinationAccount: "reserve-acc-1", Signer: "stand-1",
})
if err == nil {
t.Error("DebitMABProceeds on non-existent MAB should be REJECTED")
}
}
// TestMABWitnessProceedsReleaseNotFound asserts a release on a non-existent
// MAB is REJECTED.
func TestMABWitnessProceedsReleaseNotFound(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.WitnessMABProceedsRelease(ctx, &btypes.MsgWitnessMABProceedsRelease{
BondID: "no-such-mab", AttestationRef: "ref", Signer: "watcher-1",
})
if err == nil {
t.Error("WitnessMABProceedsRelease on non-existent MAB should be REJECTED")
}
}
// TestMABWatcherAttestNotFound asserts an attestation on a non-existent MAB
// is REJECTED.
func TestMABWatcherAttestNotFound(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
_, err := srv.WatcherAttestMAB(ctx, &btypes.MsgWatcherAttestMAB{
BondID: "no-such-mab", AttestationRef: "ref", Signer: "watcher-1",
})
if err == nil {
t.Error("WatcherAttestMAB on non-existent MAB should be REJECTED")
}
}
// TestMABDebitProceedsNilCoverKeeperRejected asserts a debit with a nil
// CoverKeeper shim (wiring error) is REJECTED (the destination cannot be
// validated — D-089(2) reverse edge required).
func TestMABDebitProceedsNilCoverKeeperRejected(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
k.SetCoverKeeper(nil) // nil CoverKeeper — wiring error
srv := keeper.NewMsgServerImpl(k)
_, err := srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-n1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if err != nil {
t.Fatalf("IssueMAB: %v", err)
}
_, err = srv.DebitMABProceeds(ctx, &btypes.MsgDebitMABProceeds{
BondID: "mab-n1", DestinationAccount: "reserve-acc-1", Signer: "stand-1",
})
if err == nil {
t.Error("DebitMABProceeds with nil CoverKeeper should be REJECTED (wiring error)")
}
if !strings.Contains(err.Error(), "CoverKeeper shim not wired") {
t.Errorf("err = %q, want 'CoverKeeper shim not wired'", err.Error())
}
}
// TestMABMsgValidateBasicErrorPaths exercises each MAB Msg* ValidateBasic
// error path for coverage.
func TestMABMsgValidateBasicErrorPaths(t *testing.T) {
// MsgIssueMAB empty.
if err := (&btypes.MsgIssueMAB{}).ValidateBasic(); err == nil {
t.Error("empty MsgIssueMAB should fail ValidateBasic")
}
// MsgIssueMAB with Bread coupons -> FR-MAB-3.
if err := (&btypes.MsgIssueMAB{
BondID: "x", PoolID: "p", IssuerStandID: "s", PrincipalGrain: 1,
CouponBps: 500, CouponKind: btypes.CouponDenomBread,
AnnualSurplusAtIssuance: 1, TermDays: 365, Signer: "s",
}).ValidateBasic(); err == nil {
t.Error("MsgIssueMAB with Bread coupons should fail ValidateBasic (FR-MAB-3)")
}
// MsgIssueMAB with above-cap coupon.
if err := (&btypes.MsgIssueMAB{
BondID: "x", PoolID: "p", IssuerStandID: "s", PrincipalGrain: 1,
CouponBps: 1200, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 1, TermDays: 365, Signer: "s",
}).ValidateBasic(); err == nil {
t.Error("above-cap MsgIssueMAB should fail ValidateBasic")
}
// MsgIssueMAB with zero principal.
if err := (&btypes.MsgIssueMAB{
BondID: "x", PoolID: "p", IssuerStandID: "s", PrincipalGrain: 0,
CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 1, TermDays: 365, Signer: "s",
}).ValidateBasic(); err == nil {
t.Error("zero-principal MsgIssueMAB should fail ValidateBasic")
}
// MsgDebitMABProceeds empty.
if err := (&btypes.MsgDebitMABProceeds{}).ValidateBasic(); err == nil {
t.Error("empty MsgDebitMABProceeds should fail ValidateBasic")
}
// MsgWitnessMABProceedsRelease empty.
if err := (&btypes.MsgWitnessMABProceedsRelease{}).ValidateBasic(); err == nil {
t.Error("empty MsgWitnessMABProceedsRelease should fail ValidateBasic")
}
// MsgWatcherAttestMAB empty.
if err := (&btypes.MsgWatcherAttestMAB{}).ValidateBasic(); err == nil {
t.Error("empty MsgWatcherAttestMAB should fail ValidateBasic")
}
}
// TestMABKeeperAccessors exercises the MAB keeper accessors (AllMABs,
// MABsForPool, AllMABAttests) for coverage.
func TestMABKeeperAccessors(t *testing.T) {
ctx, _, _, _, _, k := newMABSimtestContext(t)
srv := keeper.NewMsgServerImpl(k)
// Empty-store accessors return empty (not nil) slices.
if got := k.AllMABs(ctx); len(got) != 0 {
t.Errorf("AllMABs empty = %d, want 0", len(got))
}
if got := k.MABsForPool(ctx, "pool-1"); len(got) != 0 {
t.Errorf("MABsForPool empty = %d, want 0", len(got))
}
if got := k.AllMABAttests(ctx, "mab-x"); len(got) != 0 {
t.Errorf("AllMABAttests empty = %d, want 0", len(got))
}
// Issue + read back.
_, _ = srv.IssueMAB(ctx, &btypes.MsgIssueMAB{
BondID: "mab-acc-1", PoolID: "pool-1", IssuerStandID: "stand-1",
PrincipalGrain: 1_000_000, CouponBps: 500, CouponKind: btypes.CouponDenomCoverCall,
AnnualSurplusAtIssuance: 5_000_000, TermDays: 365, Signer: "stand-1",
})
if got := k.AllMABs(ctx); len(got) != 1 {
t.Errorf("AllMABs = %d, want 1", len(got))
}
if got := k.MABsForPool(ctx, "pool-1"); len(got) != 1 {
t.Errorf("MABsForPool pool-1 = %d, want 1", len(got))
}
if got := k.MABsForPool(ctx, "other-pool"); len(got) != 0 {
t.Errorf("MABsForPool other-pool = %d, want 0", len(got))
}
// Marshal-error path on GetMAB (corrupt bytes in store).
rawStore := ctx.KVStore(k.StoreKey())
rawStore.Set([]byte("mab/corrupt"), []byte("not-json"))
if _, ok := k.GetMAB(ctx, "corrupt"); ok {
t.Error("GetMAB on corrupt bytes should return false")
}
}