package keeper // keeper.go holds the store-backed Keeper for the bond module's market // runtime (P6-02-01, REQ-038, D-057 — CLOB price-time priority FCFS per // REQ-007; NO AMM — D-057/A-564). // // The Keeper wraps an sdk.KVStore via a storeKey. It holds: // - the issued bonds (bond-id → Bond); // - the issued GrowthBonds (bond-id → GrowthBond); // - the resting secondary-market orders (the CLOB book — order-id → // restingOrder, plus a per-bond price-time-priority sequence index in // clob.go). // // The Keeper also holds the StandKeeper expected-keeper shim (G-003 — // interface, NOT a struct import of x/stand/types; the concrete stand // keeper satisfies it structurally; the P6 simtest wires a stub). // // The 8%/0% consts (CouponCapBps=800 / CouponFloorBps=0, D-028) are // referenced DIRECTLY from x/bond/types (same package — NOT a local copy; // A-563). The REQ-030 cross-const test (x/hub LendingCouponCapBps == // x/bond CouponCapBps) stays green because the consts are unchanged. // // State-machine ordering (vision §7, enforced in every handler): // ValidateBasic → keeper authz → state mutation → ctx.EventManager().EmitEvent // // D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no // real IBC light clients, no real Stand keeper (the StandKeeper shim is a // stub), no real DEX venues. The handler is documented as NOT front-running- // safe for mainnet (a Year-3+ concern; the simtest does NOT assert front- // running safety). import ( "encoding/json" "fmt" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/oy/openyield/x/bond/types" ) // Keeper is the store-backed bond market keeper. type Keeper struct { cdc codec.Codec storeKey storetypes.StoreKey standKeeper types.StandKeeper coverKeeper types.CoverKeeper watcherKeeper types.WatcherKeeper stillKeeper types.StillKeeper seq uint64 // monotonic sequence for price-time priority (CLOB) } // NewKeeper constructs a new store-backed bond Keeper. The StandKeeper // expected-keeper shim is injected (nil-able for partial tests; the // IssueBond / IssueGrowthBond handlers guard a nil shim and skip the // StandExists check, still mutating state — the simtest wiring documents // this). The v0.7 P4 MAB shims (CoverKeeper, WatcherKeeper, StillKeeper) // are wired via the Set* methods (post-construction wiring for app wiring // or test setup); the MAB handlers guard nil shims per the documented // contract. func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandKeeper) Keeper { return Keeper{ cdc: cdc, storeKey: storeKey, standKeeper: sk, } } // SetStandKeeper sets the StandKeeper expected-keeper shim (for post- // construction wiring, e.g., app wiring or test setup). func (k *Keeper) SetStandKeeper(sk types.StandKeeper) { k.standKeeper = sk } // SetCoverKeeper sets the CoverKeeper expected-keeper shim (D-089(2) reverse // edge — for post-construction wiring, e.g., app wiring or test setup). func (k *Keeper) SetCoverKeeper(ck types.CoverKeeper) { k.coverKeeper = ck } // SetWatcherKeeper sets the WatcherKeeper expected-keeper shim (for the MAB // proceeds-release quorum check — post-construction wiring). func (k *Keeper) SetWatcherKeeper(wk types.WatcherKeeper) { k.watcherKeeper = wk } // SetStillKeeper sets the StillKeeper expected-keeper shim (D-089(1) — for // the MAB misuse auto-Still on a destination mismatch; post-construction // wiring). func (k *Keeper) SetStillKeeper(stK types.StillKeeper) { k.stillKeeper = stK } // StoreKey returns the keeper's store key (exported for simtest access to // the raw KVStore for corrupt-byte injection in marshal-error coverage // paths). func (k Keeper) StoreKey() storetypes.StoreKey { return k.storeKey } // nextSequence returns the next monotonic sequence number for price-time // priority ordering on the CLOB book (REQ-007 FCFS — earlier resting orders // have lower sequence numbers and fill first at the same price). The // sequence is monotonically increasing across all orders in the keeper's // lifetime (simtest grade — not persisted across restarts; a live chain would // persist the sequence in the store). func (k *Keeper) nextSequence() uint64 { k.seq++ return k.seq } // --- Bond store -------------------------------------------------------------- var bondKeyPrefix = []byte("bond/") func bondKey(bondID string) []byte { return append(bondKeyPrefix, []byte(bondID)...) } // GetBond loads an issued Bond by bond-id. Returns the Bond and true if // found, or zero value + false if not. func (k Keeper) GetBond(ctx sdk.Context, bondID string) (types.Bond, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(bondKey(bondID)) if bz == nil { return types.Bond{}, false } var b types.Bond if err := json.Unmarshal(bz, &b); err != nil { return types.Bond{}, false } return b, true } // SetBond persists an issued Bond by bond-id. func (k Keeper) SetBond(ctx sdk.Context, b types.Bond) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(b) if err != nil { panic(fmt.Sprintf("bond: marshal bond %q: %v", b.BondID, err)) } store.Set(bondKey(b.BondID), bz) } // AllBonds returns all issued Bonds (iteration helper, unordered). func (k Keeper) AllBonds(ctx sdk.Context) []types.Bond { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(bondKeyPrefix, prefixEnd(bondKeyPrefix)) defer iterator.Close() out := []types.Bond{} for ; iterator.Valid(); iterator.Next() { var b types.Bond if err := json.Unmarshal(iterator.Value(), &b); err == nil { out = append(out, b) } } return out } // --- GrowthBond store -------------------------------------------------------- var growthBondKeyPrefix = []byte("growth/") func growthBondKey(bondID string) []byte { return append(growthBondKeyPrefix, []byte(bondID)...) } // GetGrowthBond loads an issued GrowthBond by bond-id. Returns the GrowthBond // and true if found, or zero value + false if not. func (k Keeper) GetGrowthBond(ctx sdk.Context, bondID string) (types.GrowthBond, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(growthBondKey(bondID)) if bz == nil { return types.GrowthBond{}, false } var gb types.GrowthBond if err := json.Unmarshal(bz, &gb); err != nil { return types.GrowthBond{}, false } return gb, true } // SetGrowthBond persists an issued GrowthBond by bond-id. func (k Keeper) SetGrowthBond(ctx sdk.Context, gb types.GrowthBond) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(gb) if err != nil { panic(fmt.Sprintf("bond: marshal growth bond %q: %v", gb.BondID, err)) } store.Set(growthBondKey(gb.BondID), bz) } // AllGrowthBonds returns all issued GrowthBonds (iteration helper, unordered). func (k Keeper) AllGrowthBonds(ctx sdk.Context) []types.GrowthBond { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(growthBondKeyPrefix, prefixEnd(growthBondKeyPrefix)) defer iterator.Close() out := []types.GrowthBond{} for ; iterator.Valid(); iterator.Next() { var gb types.GrowthBond if err := json.Unmarshal(iterator.Value(), &gb); err == nil { out = append(out, gb) } } return out } // --- MAB store (v0.7 P4 — REQ-054, D-080, D-089(2)) --------------------------- // // The MAB store is keyed by bond-id -> MAB. A separate mab-pool index // (bond-id -> pool-id) records the pool each MAB was issued for, so the // 3× annual surplus ceiling check can sum the MAB principals for a pool, // and the MsgDebitMABProceeds handler can query the CoverKeeper for the // pool's ReserveAccount. The mab-attest store records the quarterly Watcher // attestations (mab_attest// -> attestationRef). var mabKeyPrefix = []byte("mab/") func mabKey(bondID string) []byte { return append(mabKeyPrefix, []byte(bondID)...) } // GetMAB loads an issued MAB by bond-id. Returns the MAB and true if found, // or zero value + false if not. func (k Keeper) GetMAB(ctx sdk.Context, bondID string) (types.MAB, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(mabKey(bondID)) if bz == nil { return types.MAB{}, false } var m types.MAB if err := json.Unmarshal(bz, &m); err != nil { return types.MAB{}, false } return m, true } // SetMAB persists an issued MAB by bond-id. func (k Keeper) SetMAB(ctx sdk.Context, m types.MAB) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(m) if err != nil { panic(fmt.Sprintf("bond: marshal mab %q: %v", m.BondID, err)) } store.Set(mabKey(m.BondID), bz) } // AllMABs returns all issued MABs (iteration helper, unordered). func (k Keeper) AllMABs(ctx sdk.Context) []types.MAB { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(mabKeyPrefix, prefixEnd(mabKeyPrefix)) defer iterator.Close() out := []types.MAB{} for ; iterator.Valid(); iterator.Next() { var m types.MAB if err := json.Unmarshal(iterator.Value(), &m); err == nil { out = append(out, m) } } return out } // --- MAB pool index (bond-id -> pool-id) -------------------------------------- var mabPoolKeyPrefix = []byte("mab-pool/") func mabPoolKey(bondID string) []byte { return append(mabPoolKeyPrefix, []byte(bondID)...) } // setMABPool records the pool-id a MAB was issued for (bond-id -> pool-id). func (k Keeper) setMABPool(ctx sdk.Context, bondID, poolID string) { store := ctx.KVStore(k.storeKey) store.Set(mabPoolKey(bondID), []byte(poolID)) } // GetMABPool returns the pool-id a MAB was issued for (bond-id -> pool-id). // Returns the pool-id and true if found, or "" + false if not. func (k Keeper) GetMABPool(ctx sdk.Context, bondID string) (string, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(mabPoolKey(bondID)) if bz == nil { return "", false } return string(bz), true } // MABsForPool returns all MABs issued for the given pool-id (the 3× annual // surplus ceiling check sums their principals). Iterates the mab-pool index // + loads each MAB by bond-id. func (k Keeper) MABsForPool(ctx sdk.Context, poolID string) []types.MAB { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(mabPoolKeyPrefix, prefixEnd(mabPoolKeyPrefix)) defer iterator.Close() out := []types.MAB{} for ; iterator.Valid(); iterator.Next() { if string(iterator.Value()) != poolID { continue } // The key is mab-pool/; extract the bondID (strip the // prefix) and load the MAB. bondID := string(iterator.Key()[len(mabPoolKeyPrefix):]) if m, ok := k.GetMAB(ctx, bondID); ok { out = append(out, m) } } return out } // --- MAB attestation store (mab_attest// -> ref) ----------- var mabAttestKeyPrefix = []byte("mab_attest/") func mabAttestKey(bondID string, ts int64) []byte { return append(append(mabAttestKeyPrefix, []byte(bondID)...), []byte(fmt.Sprintf("/%d", ts))...) } // SetMABAttest records a quarterly Watcher attestation on a MAB (bond-id + // timestamp -> attestation-ref). func (k Keeper) SetMABAttest(ctx sdk.Context, bondID string, ts int64, attestationRef string) { store := ctx.KVStore(k.storeKey) store.Set(mabAttestKey(bondID, ts), []byte(attestationRef)) } // AllMABAttests returns all recorded Watcher attestations for a MAB // (bond-id -> []attestationRef, unordered). func (k Keeper) AllMABAttests(ctx sdk.Context, bondID string) []string { store := ctx.KVStore(k.storeKey) prefix := append(mabAttestKeyPrefix, []byte(bondID+"/")...) iterator := store.Iterator(prefix, prefixEnd(prefix)) defer iterator.Close() out := []string{} for ; iterator.Valid(); iterator.Next() { out = append(out, string(iterator.Value())) } return out } // --- Order store (CLOB resting book) ----------------------------------------- // // The resting book is keyed by order-id → restingOrder (the in-keeper book // entry carrying the order + its price-time-priority sequence). The CLOB // matching engine (clob.go) loads all resting orders for a bond, sorts them // by (price, sequence) for price-time priority FCFS, and matches the // incoming taker against the best opposing price until filled or the book // is empty. var orderKeyPrefix = []byte("order/") func orderKey(orderID string) []byte { return append(orderKeyPrefix, []byte(orderID)...) } // GetRestingOrder loads a resting order by order-id. Returns the order and // true if found, or zero value + false if not. func (k Keeper) GetRestingOrder(ctx sdk.Context, orderID string) (restingOrder, bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(orderKey(orderID)) if bz == nil { return restingOrder{}, false } var o restingOrder if err := json.Unmarshal(bz, &o); err != nil { return restingOrder{}, false } return o, true } // setRestingOrder persists a resting order by order-id. func (k Keeper) setRestingOrder(ctx sdk.Context, o restingOrder) { store := ctx.KVStore(k.storeKey) bz, err := json.Marshal(o) if err != nil { panic(fmt.Sprintf("bond: marshal order %q: %v", o.Order.OrderID, err)) } store.Set(orderKey(o.Order.OrderID), bz) } // deleteRestingOrder removes a resting order by order-id. func (k Keeper) deleteRestingOrder(ctx sdk.Context, orderID string) { store := ctx.KVStore(k.storeKey) store.Delete(orderKey(orderID)) } // AllRestingOrders returns all resting orders (iteration helper, unordered). // Exported for simtest assertion. func (k Keeper) AllRestingOrders(ctx sdk.Context) []restingOrder { store := ctx.KVStore(k.storeKey) iterator := store.Iterator(orderKeyPrefix, prefixEnd(orderKeyPrefix)) defer iterator.Close() out := []restingOrder{} for ; iterator.Valid(); iterator.Next() { var o restingOrder if err := json.Unmarshal(iterator.Value(), &o); err == nil { out = append(out, o) } } return out } // --- prefixEnd helper -------------------------------------------------------- // prefixEnd returns the key that sorts immediately after all keys sharing // the given prefix (the standard prefix-iteration end key: increment the // last byte, drop overflow). Mirrors x/hub/keeper/keeper.go. func prefixEnd(prefix []byte) []byte { if len(prefix) == 0 { return nil } end := make([]byte, len(prefix)) copy(end, prefix) for i := len(end) - 1; i >= 0; i-- { end[i]++ if end[i] != 0 { return end } } // All bytes were 0xFF; return nil (iterate to end of store). return nil }