Files
openyield/x/bond/keeper/keeper.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

412 lines
14 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
// 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
}