6c34650a0d
v0.5 Bearers Runtime — 7 runtime REQs (REQ-033..039) shipped as feature. 8 modules promoted to runtime (MsgServer + simtest). cosmos-sdk v0.50.8 + ibc-go v8.2.1 added (G-006 controlled exception). G-003 + locked-const firewalls intact. 8 keeper packages ≥80% coverage. 5 GRILL decisions ratified; 8 binding fixes landed; 5 P1+ flagged for v0.6+. ---ci--- project: oy phase: 8 milestone: v0.5 status: complete requirements: covered: [REQ-033, REQ-034, REQ-035, REQ-036, REQ-037, REQ-038, REQ-039] partial: [] ---/ci---
262 lines
8.7 KiB
Go
262 lines
8.7 KiB
Go
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
|
|
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).
|
|
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 }
|
|
|
|
// 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
|
|
}
|
|
|
|
// --- 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
|
|
}
|