9e7fc403f5
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---
412 lines
14 KiB
Go
412 lines
14 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
|
||
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/<bondID>/<timestamp> -> 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/<bondID>; 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/<bondID>/<timestamp> -> 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
|
||
}
|