Files
openyield/x/cover/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

544 lines
19 KiB
Go

package keeper
// keeper.go holds the store-backed Keeper for the cover module's Cover Pool
// runtime (REQ-046, REQ-047, REQ-049, REQ-050, REQ-055, D-077, D-086,
// D-088, D-089).
//
// The Keeper wraps an sdk.KVStore via a storeKey. It holds:
// - the CoverPool records (pool-id -> CoverPool);
// - the CoverCall records (call-id -> CoverCall; the FileCoverCall
// handler persists here; P4 adds the Voucher adjudication).
//
// The Cover-Fee routing (RouteCoverFee) does NOT persist a separate record
// in P1 — the routing is the event (the reserve balance update is a
// simtest-grade stub). P2 may add a CoverFeeRouting record; P1 ships the
// event-only path.
//
// The Keeper also holds the FOUR expected-keeper shims (StandingKeeper for
// the D-077 gate; WatcherKeeper for the launch attestation; BondKeeper for
// the P4 MAB check; StillKeeper for the below-floor auto-pause). The shims
// are interfaces (G-003 — no struct import of x/standing/types,
// x/watcher/types, x/bond/types, x/still/types); the concrete keepers (or
// simtest stubs) satisfy them structurally.
//
// State-machine ordering (vision §7, enforced in every handler):
// ValidateBasic -> handler authz/gate -> state mutation -> ctx.EventManager().EmitEvent
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/cover/types"
)
// Keeper is the store-backed cover Cover-Pool keeper.
type Keeper struct {
cdc codec.Codec
storeKey storetypes.StoreKey
standingKeeper types.StandingKeeper
watcherKeeper types.WatcherKeeper
bondKeeper types.BondKeeper
stillKeeper types.StillKeeper
// paramsOverride is a simtest-grade Params override (nil = use
// DefaultParams). A future P2+ will load the Params from the params
// store; for now the handler uses DefaultParams unless an override is
// set via SetParamsOverride (the D-086 simtest case (f) uses this to
// restrict FactoryAllowedPhases to [Phase2, Phase3] only and reject a
// Phase4 launch).
paramsOverride *types.Params
}
// NewKeeper constructs a new store-backed cover Keeper. The four expected-
// keeper shims are injected (all nil-able for partial tests; the handlers
// guard nil shims and skip the corresponding check, still mutating state —
// the simtest wiring documents this). The StandingKeeper gates the launch
// (D-077); the WatcherKeeper attests the launch (REQ-046); the BondKeeper
// is held for P4 (the P1 handlers do not call it); the StillKeeper records
// the below-floor auto-pause (D-089(1)).
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandingKeeper, wk types.WatcherKeeper, bk types.BondKeeper, stK types.StillKeeper) Keeper {
return Keeper{
cdc: cdc,
storeKey: storeKey,
standingKeeper: sk,
watcherKeeper: wk,
bondKeeper: bk,
stillKeeper: stK,
}
}
// SetStandingKeeper sets the StandingKeeper expected-keeper shim (for
// post-construction wiring, e.g., app wiring or test setup).
func (k *Keeper) SetStandingKeeper(sk types.StandingKeeper) { k.standingKeeper = sk }
// SetWatcherKeeper sets the WatcherKeeper expected-keeper shim.
func (k *Keeper) SetWatcherKeeper(wk types.WatcherKeeper) { k.watcherKeeper = wk }
// SetBondKeeper sets the BondKeeper expected-keeper shim.
func (k *Keeper) SetBondKeeper(bk types.BondKeeper) { k.bondKeeper = bk }
// SetStillKeeper sets the StillKeeper expected-keeper shim.
func (k *Keeper) SetStillKeeper(stK types.StillKeeper) { k.stillKeeper = stK }
// SetParamsOverride sets a simtest-grade Params override (nil = use
// DefaultParams). The D-086 simtest case (f) uses this to restrict
// FactoryAllowedPhases to [Phase2, Phase3] only and reject a Phase4
// launch. A future P2+ will replace this with a params-store load.
func (k *Keeper) SetParamsOverride(p types.Params) { k.paramsOverride = &p }
// Params returns the effective Params (the override if set, else
// DefaultParams). The handler calls this to get FactoryAllowedPhases +
// PoolStandingGate.
func (k Keeper) Params() types.Params {
if k.paramsOverride != nil {
return *k.paramsOverride
}
return types.DefaultParams()
}
// StoreKey returns the keeper's store key (exported for simtest access to
// the underlying KVStore, e.g. to inject corrupt bytes for marshal-error
// coverage). Mirrors the x/hub simtest pattern (the simtest reaches the
// store via ctx.KVStore(k.StoreKey())).
func (k Keeper) StoreKey() storetypes.StoreKey { return k.storeKey }
// --- CoverPool store ----------------------------------------------------------
var poolKeyPrefix = []byte("pool/")
func poolKey(poolID string) []byte {
return append(poolKeyPrefix, []byte(poolID)...)
}
// GetCoverPool loads a CoverPool by pool-id. Returns the pool and true if
// found, or zero value + false if not.
func (k Keeper) GetCoverPool(ctx sdk.Context, poolID string) (types.CoverPool, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(poolKey(poolID))
if bz == nil {
return types.CoverPool{}, false
}
var p types.CoverPool
if err := json.Unmarshal(bz, &p); err != nil {
return types.CoverPool{}, false
}
return p, true
}
// SetCoverPool persists a CoverPool by pool-id.
func (k Keeper) SetCoverPool(ctx sdk.Context, p types.CoverPool) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(p)
if err != nil {
panic(fmt.Sprintf("cover: marshal pool %q: %v", p.PoolID, err))
}
store.Set(poolKey(p.PoolID), bz)
}
// AllCoverPools returns all persisted CoverPool records (iteration helper,
// unordered).
func (k Keeper) AllCoverPools(ctx sdk.Context) []types.CoverPool {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(poolKeyPrefix, prefixEnd(poolKeyPrefix))
defer iterator.Close()
out := []types.CoverPool{}
for ; iterator.Valid(); iterator.Next() {
var p types.CoverPool
if err := json.Unmarshal(iterator.Value(), &p); err == nil {
out = append(out, p)
}
}
return out
}
// --- CoverCall store ----------------------------------------------------------
var callKeyPrefix = []byte("call/")
func callKey(callID string) []byte {
return append(callKeyPrefix, []byte(callID)...)
}
// GetCoverCall loads a CoverCall by call-id. Returns the call and true if
// found, or zero value + false if not.
func (k Keeper) GetCoverCall(ctx sdk.Context, callID string) (types.CoverCall, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(callKey(callID))
if bz == nil {
return types.CoverCall{}, false
}
var c types.CoverCall
if err := json.Unmarshal(bz, &c); err != nil {
return types.CoverCall{}, false
}
return c, true
}
// SetCoverCall persists a CoverCall by call-id.
func (k Keeper) SetCoverCall(ctx sdk.Context, c types.CoverCall) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(c)
if err != nil {
panic(fmt.Sprintf("cover: marshal call %q: %v", c.CallID, err))
}
store.Set(callKey(c.CallID), bz)
}
// AllCoverCalls returns all persisted CoverCall records (iteration helper,
// unordered).
func (k Keeper) AllCoverCalls(ctx sdk.Context) []types.CoverCall {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(callKeyPrefix, prefixEnd(callKeyPrefix))
defer iterator.Close()
out := []types.CoverCall{}
for ; iterator.Valid(); iterator.Next() {
var c types.CoverCall
if err := json.Unmarshal(iterator.Value(), &c); err == nil {
out = append(out, c)
}
}
return out
}
// --- P4: CoverClaimsVoucher store (REQ-055, D-090(2)) ------------------------
//
// The Voucher store is keyed by voucher-reach-id + pool-id (composite key)
// -> CoverClaimsVoucher. A Voucher is registered per-Pool; the composite key
// enforces idempotency (no duplicate Voucher for the same Pool). The
// GetAvgCallSize helper computes the average Cover Call amount for a Pool
// from the call/ store (returns 0 if no Calls — the D-090(2) cold-start
// case).
var voucherKeyPrefix = []byte("voucher/")
func voucherKey(voucherReachID, poolID string) []byte {
return append(append(voucherKeyPrefix, []byte(voucherReachID)...), []byte("/"+poolID)...)
}
// GetCoverClaimsVoucher loads a CoverClaimsVoucher by voucher-reach-id +
// pool-id. Returns the Voucher and true if found, or zero value + false if
// not.
func (k Keeper) GetCoverClaimsVoucher(ctx sdk.Context, voucherReachID, poolID string) (types.CoverClaimsVoucher, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(voucherKey(voucherReachID, poolID))
if bz == nil {
return types.CoverClaimsVoucher{}, false
}
var v types.CoverClaimsVoucher
if err := json.Unmarshal(bz, &v); err != nil {
return types.CoverClaimsVoucher{}, false
}
return v, true
}
// SetCoverClaimsVoucher persists a CoverClaimsVoucher by voucher-reach-id +
// pool-id.
func (k Keeper) SetCoverClaimsVoucher(ctx sdk.Context, v types.CoverClaimsVoucher) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("cover: marshal voucher %q/%q: %v", v.VoucherReachID, v.PoolID, err))
}
store.Set(voucherKey(v.VoucherReachID, v.PoolID), bz)
}
// AllCoverClaimsVouchers returns all persisted CoverClaimsVoucher records
// (iteration helper, unordered).
func (k Keeper) AllCoverClaimsVouchers(ctx sdk.Context) []types.CoverClaimsVoucher {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(voucherKeyPrefix, prefixEnd(voucherKeyPrefix))
defer iterator.Close()
out := []types.CoverClaimsVoucher{}
for ; iterator.Valid(); iterator.Next() {
var v types.CoverClaimsVoucher
if err := json.Unmarshal(iterator.Value(), &v); err == nil {
out = append(out, v)
}
}
return out
}
// GetAvgCallSize computes the average Cover Call amount (Grain) for a Pool
// from the call/ store (REQ-055, D-090(2)). Returns 0 if no Calls have been
// filed for the Pool — the D-090(2) cold-start case (the Voucher bond falls
// back to MinimumVoucherBond, NOT zero).
func (k Keeper) GetAvgCallSize(ctx sdk.Context, poolID string) int64 {
calls := k.AllCoverCalls(ctx)
sum := int64(0)
n := 0
for _, c := range calls {
if c.PoolID == poolID {
sum += c.AmountGrain
n++
}
}
if n == 0 {
return 0
}
return sum / int64(n)
}
// --- 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). Used for store.Iterator(start, prefixEnd(start))
// prefix scans. 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
}
// --- P2: CoverCharter / PoolCouncil / CoverCallVote / CharterAmendment stores --
//
// (REQ-052, REQ-062). Four new stores keyed by ID-string. The
// CoverCharter store is keyed by CharterID; the PoolCouncil store is keyed
// by PoolID (one council per pool); the CoverCallVote store is keyed by
// VoteID; the CharterAmendment store is keyed by AmendmentID. All four
// use the same JSON-marshal pattern as the P1 CoverPool / CoverCall
// stores. The Get/Set/All helpers mirror the P1 helpers.
var charterKeyPrefix = []byte("charter/")
func charterKey(charterID string) []byte {
return append(charterKeyPrefix, []byte(charterID)...)
}
// GetCoverCharter loads a CoverCharter by charter-id. Returns the charter
// and true if found, or zero value + false if not.
func (k Keeper) GetCoverCharter(ctx sdk.Context, charterID string) (types.CoverCharter, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(charterKey(charterID))
if bz == nil {
return types.CoverCharter{}, false
}
var c types.CoverCharter
if err := json.Unmarshal(bz, &c); err != nil {
return types.CoverCharter{}, false
}
return c, true
}
// SetCoverCharter persists a CoverCharter by charter-id.
func (k Keeper) SetCoverCharter(ctx sdk.Context, c types.CoverCharter) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(c)
if err != nil {
panic(fmt.Sprintf("cover: marshal charter %q: %v", c.CharterID, err))
}
store.Set(charterKey(c.CharterID), bz)
}
// AllCoverCharters returns all persisted CoverCharter records (iteration
// helper, unordered).
func (k Keeper) AllCoverCharters(ctx sdk.Context) []types.CoverCharter {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(charterKeyPrefix, prefixEnd(charterKeyPrefix))
defer iterator.Close()
out := []types.CoverCharter{}
for ; iterator.Valid(); iterator.Next() {
var c types.CoverCharter
if err := json.Unmarshal(iterator.Value(), &c); err == nil {
out = append(out, c)
}
}
return out
}
var councilKeyPrefix = []byte("council/")
func councilKey(poolID string) []byte {
return append(councilKeyPrefix, []byte(poolID)...)
}
// GetPoolCouncil loads a PoolCouncil by pool-id. Returns the council and
// true if found, or zero value + false if not.
func (k Keeper) GetPoolCouncil(ctx sdk.Context, poolID string) (types.PoolCouncil, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(councilKey(poolID))
if bz == nil {
return types.PoolCouncil{}, false
}
var c types.PoolCouncil
if err := json.Unmarshal(bz, &c); err != nil {
return types.PoolCouncil{}, false
}
return c, true
}
// SetPoolCouncil persists a PoolCouncil by pool-id.
func (k Keeper) SetPoolCouncil(ctx sdk.Context, c types.PoolCouncil) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(c)
if err != nil {
panic(fmt.Sprintf("cover: marshal council for pool %q: %v", c.PoolID, err))
}
store.Set(councilKey(c.PoolID), bz)
}
// AllPoolCouncils returns all persisted PoolCouncil records (iteration
// helper, unordered).
func (k Keeper) AllPoolCouncils(ctx sdk.Context) []types.PoolCouncil {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(councilKeyPrefix, prefixEnd(councilKeyPrefix))
defer iterator.Close()
out := []types.PoolCouncil{}
for ; iterator.Valid(); iterator.Next() {
var c types.PoolCouncil
if err := json.Unmarshal(iterator.Value(), &c); err == nil {
out = append(out, c)
}
}
return out
}
var voteKeyPrefix = []byte("vote/")
func voteKey(voteID string) []byte {
return append(voteKeyPrefix, []byte(voteID)...)
}
// GetCoverCallVote loads a CoverCallVote by vote-id. Returns the vote and
// true if found, or zero value + false if not.
func (k Keeper) GetCoverCallVote(ctx sdk.Context, voteID string) (types.CoverCallVote, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(voteKey(voteID))
if bz == nil {
return types.CoverCallVote{}, false
}
var v types.CoverCallVote
if err := json.Unmarshal(bz, &v); err != nil {
return types.CoverCallVote{}, false
}
return v, true
}
// SetCoverCallVote persists a CoverCallVote by vote-id.
func (k Keeper) SetCoverCallVote(ctx sdk.Context, v types.CoverCallVote) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("cover: marshal vote %q: %v", v.VoteID, err))
}
store.Set(voteKey(v.VoteID), bz)
}
// AllCoverCallVotes returns all persisted CoverCallVote records (iteration
// helper, unordered).
func (k Keeper) AllCoverCallVotes(ctx sdk.Context) []types.CoverCallVote {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(voteKeyPrefix, prefixEnd(voteKeyPrefix))
defer iterator.Close()
out := []types.CoverCallVote{}
for ; iterator.Valid(); iterator.Next() {
var v types.CoverCallVote
if err := json.Unmarshal(iterator.Value(), &v); err == nil {
out = append(out, v)
}
}
return out
}
var amendmentKeyPrefix = []byte("amendment/")
func amendmentKey(amendmentID string) []byte {
return append(amendmentKeyPrefix, []byte(amendmentID)...)
}
// GetCharterAmendment loads a CharterAmendment by amendment-id. Returns
// the amendment and true if found, or zero value + false if not.
func (k Keeper) GetCharterAmendment(ctx sdk.Context, amendmentID string) (types.CharterAmendment, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(amendmentKey(amendmentID))
if bz == nil {
return types.CharterAmendment{}, false
}
var a types.CharterAmendment
if err := json.Unmarshal(bz, &a); err != nil {
return types.CharterAmendment{}, false
}
return a, true
}
// SetCharterAmendment persists a CharterAmendment by amendment-id.
func (k Keeper) SetCharterAmendment(ctx sdk.Context, a types.CharterAmendment) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(a)
if err != nil {
panic(fmt.Sprintf("cover: marshal amendment %q: %v", a.AmendmentID, err))
}
store.Set(amendmentKey(a.AmendmentID), bz)
}
// AllCharterAmendments returns all persisted CharterAmendment records
// (iteration helper, unordered).
func (k Keeper) AllCharterAmendments(ctx sdk.Context) []types.CharterAmendment {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(amendmentKeyPrefix, prefixEnd(amendmentKeyPrefix))
defer iterator.Close()
out := []types.CharterAmendment{}
for ; iterator.Valid(); iterator.Next() {
var a types.CharterAmendment
if err := json.Unmarshal(iterator.Value(), &a); err == nil {
out = append(out, a)
}
}
return out
}
// CoolCharterAmendment transitions a Proposed CharterAmendment to Cooled
// if the 7-day cooling has elapsed (REQ-052). Returns an error if the
// amendment is not found, not in the Proposed status, or the cooling has
// not elapsed. The handler (or simtest) calls this after the cooling
// period; a separate RatifyCharterAmendment transitions to Ratified.
func (k Keeper) CoolCharterAmendment(ctx sdk.Context, amendmentID string, now int64) (types.CharterAmendment, error) {
a, ok := k.GetCharterAmendment(ctx, amendmentID)
if !ok {
return types.CharterAmendment{}, fmt.Errorf("cover: amendment %q not found", amendmentID)
}
if a.Status != types.AmendmentProposed {
return types.CharterAmendment{}, fmt.Errorf("cover: amendment %q status %q (only Proposed can be Cooled)", amendmentID, a.Status)
}
if now-a.ProposedAt < types.CharterAmendmentCoolingSeconds {
return types.CharterAmendment{}, fmt.Errorf("cover: amendment %q cooling not elapsed (now=%d ProposedAt=%d, need %d seconds)", amendmentID, now, a.ProposedAt, types.CharterAmendmentCoolingSeconds)
}
a.Status = types.AmendmentCooled
a.CooledAt = now
k.SetCharterAmendment(ctx, a)
return a, nil
}
// RatifyCharterAmendment transitions a Cooled CharterAmendment to
// Ratified (REQ-052). Returns an error if the amendment is not found or
// not in the Cooled status. The Pool supermajority + Watcher + Counsel
// are checked upstream (the handler); this helper does the state
// transition + appends the amendment to the parent charter's Amendments
// slice.
func (k Keeper) RatifyCharterAmendment(ctx sdk.Context, amendmentID string, now int64) (types.CharterAmendment, error) {
a, ok := k.GetCharterAmendment(ctx, amendmentID)
if !ok {
return types.CharterAmendment{}, fmt.Errorf("cover: amendment %q not found", amendmentID)
}
if a.Status != types.AmendmentCooled {
return types.CharterAmendment{}, fmt.Errorf("cover: amendment %q status %q (only Cooled can be Ratified)", amendmentID, a.Status)
}
a.Status = types.AmendmentRatified
a.RatifiedAt = now
k.SetCharterAmendment(ctx, a)
return a, nil
}