Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfbaef6bc4 | |||
| 6b23409da2 | |||
| 141119f9ca | |||
| 99301f3ade | |||
| aaefde9295 | |||
| 99f68f903f | |||
| 8cc9cf26a0 | |||
| 899b76b572 | |||
| 40d878fd17 | |||
| 8c59bd48e0 | |||
| 79a72c48cf | |||
| edea9422ac |
@@ -0,0 +1,74 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "bloom"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
|
||||||
|
// Target Bloom rate: ~4-5% per year (§6)
|
||||||
|
// Honest expectation, not a promise
|
||||||
|
TargetBloomRateBasisPoints = 450 // 4.5%
|
||||||
|
MinBloomRateBasisPoints = 400 // 4.0%
|
||||||
|
MaxBloomRateBasisPoints = 500 // 5.0%
|
||||||
|
|
||||||
|
// Bloom accrues automatically to every Grain in every Stash (§6)
|
||||||
|
// Accrual period: daily (in blocks, assuming ~10min blocks)
|
||||||
|
AccrualPeriodBlocks = 144
|
||||||
|
)
|
||||||
|
|
||||||
|
// BloomRecord tracks Bloom accrued to a Stash
|
||||||
|
type BloomRecord struct {
|
||||||
|
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||||
|
AccruedGrain int64 `json:"accrued_grain" yaml:"accrued_grain"`
|
||||||
|
LastAccrualBlock int64 `json:"last_accrual_block" yaml:"last_accrual_block"`
|
||||||
|
RateBasisPoints uint32 `json:"rate_basis_points" yaml:"rate_basis_points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BloomBoosterBucket represents a higher-strategy bucket (§3 lexicon)
|
||||||
|
type BloomBoosterBucket struct {
|
||||||
|
BucketID string `json:"bucket_id" yaml:"bucket_id"`
|
||||||
|
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||||
|
PledgeAmount int64 `json:"pledge_amount" yaml:"pledge_amount"`
|
||||||
|
BoostRateBps uint32 `json:"boost_rate_bps" yaml:"boost_rate_bps"`
|
||||||
|
UnlockHeight int64 `json:"unlock_height" yaml:"unlock_height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BloomSource defines where Bloom originates (§6: only from real yield)
|
||||||
|
type BloomSource string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BloomFromTreasury BloomSource = "TreasuryCoupons"
|
||||||
|
BloomFromCorporate BloomSource = "CorporateCoupons"
|
||||||
|
BloomFromGoldCarry BloomSource = "GoldCarry"
|
||||||
|
BloomFromRwaCashflow BloomSource = "RwaCashflow"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MissionLockBloom: Bloom originates ONLY from real yield (§6)
|
||||||
|
// No synthetic Bloom. No protocol-printed Bloom.
|
||||||
|
// This is a Mission Lock — no Council vote can change it.
|
||||||
|
const MissionLockBloom = "Bloom originates only from real yield. No synthetic Bloom. No protocol-printed Bloom."
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
TargetRateBps uint32 `json:"target_rate_bps" yaml:"target_rate_bps"`
|
||||||
|
AccrualBlocks uint32 `json:"accrual_blocks" yaml:"accrual_blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{
|
||||||
|
TargetRateBps: TargetBloomRateBasisPoints,
|
||||||
|
AccrualBlocks: AccrualPeriodBlocks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/bloom/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTargetBloomRate(t *testing.T) {
|
||||||
|
if types.TargetBloomRateBasisPoints != 450 {
|
||||||
|
t.Errorf("TargetBloomRate = %d, expected 450 (§6: 4.5 pct)", types.TargetBloomRateBasisPoints)
|
||||||
|
}
|
||||||
|
if types.MinBloomRateBasisPoints != 400 {
|
||||||
|
t.Errorf("MinBloomRate = %d, expected 400 (§6: 4-5 pct)", types.MinBloomRateBasisPoints)
|
||||||
|
}
|
||||||
|
if types.MaxBloomRateBasisPoints != 500 {
|
||||||
|
t.Errorf("MaxBloomRate = %d, expected 500 (§6: 4-5 pct)", types.MaxBloomRateBasisPoints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBloomSources(t *testing.T) {
|
||||||
|
sources := []types.BloomSource{
|
||||||
|
types.BloomFromTreasury, types.BloomFromCorporate,
|
||||||
|
types.BloomFromGoldCarry, types.BloomFromRwaCashflow,
|
||||||
|
}
|
||||||
|
if len(sources) != 4 {
|
||||||
|
t.Errorf("Expected 4 Bloom sources, got %d", len(sources))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMissionLockBloom(t *testing.T) {
|
||||||
|
if types.MissionLockBloom == "" {
|
||||||
|
t.Error("MissionLockBloom should not be empty (§6: mission-locked)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccrualPeriod(t *testing.T) {
|
||||||
|
if types.AccrualPeriodBlocks != 144 {
|
||||||
|
t.Errorf("AccrualPeriodBlocks = %d, expected 144 (~daily)", types.AccrualPeriodBlocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "feecovenant"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
|
||||||
|
// Fee Covenant — LOCKED (§18)
|
||||||
|
// Mission-lock: fee can never exceed 0.1pct or fall below 0.01pct.
|
||||||
|
// Both bounds enforced by smart contract. No future Council vote can change them.
|
||||||
|
// Auto-decline only — fees only go down as the mesh grows, never up.
|
||||||
|
|
||||||
|
FeeCeilingBps = 10 // 0.1pct (ceiling, LOCKED)
|
||||||
|
FeeFloorBps = 1 // 0.01pct (floor, LOCKED)
|
||||||
|
InternalMinGrain = 1 // 1 Grain internal minimum (LOCKED, never changes)
|
||||||
|
|
||||||
|
// Volume tier thresholds (§18)
|
||||||
|
VolumeTier2Bps = 7 // 0.07pct at $10B
|
||||||
|
VolumeTier3Bps = 5 // 0.05pct at $50B
|
||||||
|
VolumeTier4Bps = 3 // 0.03pct at $200B
|
||||||
|
VolumeTier5Bps = 2 // 0.02pct at $500B
|
||||||
|
VolumeFloorBps = 1 // 0.01pct at $1T (floor)
|
||||||
|
|
||||||
|
// Processor share (§18: auto-declining 50pct -> 30pct -> 20pct -> 10pct)
|
||||||
|
ProcessorShareGenesis = 50 // 50pct at launch
|
||||||
|
ProcessorShareTier2 = 30 // 30pct at $10B
|
||||||
|
ProcessorShareTier3 = 20 // 20pct at $50B
|
||||||
|
ProcessorShareTier4 = 10 // 10pct at $200B+
|
||||||
|
)
|
||||||
|
|
||||||
|
// VolumeTier represents the current fee schedule tier (§18)
|
||||||
|
type VolumeTier string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TierGenesis VolumeTier = "Genesis" // 0.1pct (ceiling)
|
||||||
|
TierStage2 VolumeTier = "Stage2" // 0.07pct at $10B
|
||||||
|
TierStage3 VolumeTier = "Stage3" // 0.05pct at $50B
|
||||||
|
TierStage4 VolumeTier = "Stage4" // 0.03pct at $200B
|
||||||
|
TierStage5 VolumeTier = "Stage5" // 0.02pct at $500B
|
||||||
|
TierFloor VolumeTier = "Floor" // 0.01pct at $1T (floor)
|
||||||
|
)
|
||||||
|
|
||||||
|
// WaiverReason defines "always free" pass types (§18)
|
||||||
|
// 0pct protocol fee; only 1-Grain internal minimum
|
||||||
|
type WaiverReason string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WaiverHandPassGuild WaiverReason = "HandPassGuild"
|
||||||
|
WaiverCrewInternalVault WaiverReason = "CrewInternalVault"
|
||||||
|
WaiverHouseholdInternal WaiverReason = "HouseholdInternal"
|
||||||
|
WaiverCoverCall WaiverReason = "CoverCall"
|
||||||
|
WaiverTrustDistribution WaiverReason = "TrustDistribution"
|
||||||
|
WaiverFoundationGrant WaiverReason = "FoundationGrant"
|
||||||
|
WaiverCircleActiveCycle WaiverReason = "CircleActiveCycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeeSchedule is the current fee schedule (§18)
|
||||||
|
type FeeSchedule struct {
|
||||||
|
CeilingBps uint32 `json:"ceiling_bps" yaml:"ceiling_bps"`
|
||||||
|
FloorBps uint32 `json:"floor_bps" yaml:"floor_bps"`
|
||||||
|
CurrentBps uint32 `json:"current_bps" yaml:"current_bps"`
|
||||||
|
VolumeTier VolumeTier `json:"volume_tier" yaml:"volume_tier"`
|
||||||
|
InternalMinGrain int64 `json:"internal_min_grain" yaml:"internal_min_grain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultFeeSchedule returns the launch fee schedule (§18)
|
||||||
|
func DefaultFeeSchedule() FeeSchedule {
|
||||||
|
return FeeSchedule{
|
||||||
|
CeilingBps: FeeCeilingBps,
|
||||||
|
FloorBps: FeeFloorBps,
|
||||||
|
CurrentBps: FeeCeilingBps, // starts at ceiling
|
||||||
|
VolumeTier: TierGenesis,
|
||||||
|
InternalMinGrain: InternalMinGrain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp ensures fee is within LOCKED bounds (§18: never > ceiling, never < floor)
|
||||||
|
// This is automatic and authoritative. No Council vote can change it.
|
||||||
|
func Clamp(feeBps uint32) uint32 {
|
||||||
|
if feeBps > FeeCeilingBps {
|
||||||
|
return FeeCeilingBps
|
||||||
|
}
|
||||||
|
if feeBps < FeeFloorBps {
|
||||||
|
return FeeFloorBps
|
||||||
|
}
|
||||||
|
return feeBps
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWaived checks if a pass type is "always free" (§18)
|
||||||
|
func IsWaived(reason WaiverReason) bool {
|
||||||
|
switch reason {
|
||||||
|
case WaiverHandPassGuild, WaiverCrewInternalVault, WaiverHouseholdInternal,
|
||||||
|
WaiverCoverCall, WaiverTrustDistribution, WaiverFoundationGrant,
|
||||||
|
WaiverCircleActiveCycle:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissionLockFeeCovenant — enforced by code, not by promise (§18)
|
||||||
|
const MissionLockFeeCovenant = "Standard Pass-Act fee: never above 0.1pct, never below 0.01pct. Auto-decline on schedule, never auto-increase. Internal minimum: 1 Grain per waived Pass-Act. Never changes. This covenant is enforced by code, not by promise."
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
Schedule FeeSchedule `json:"schedule" yaml:"schedule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{Schedule: DefaultFeeSchedule()}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/feecovenant/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeeCeiling(t *testing.T) {
|
||||||
|
if types.FeeCeilingBps != 10 {
|
||||||
|
t.Errorf("FeeCeilingBps = %d, expected 10 (§18: 0.1pct ceiling LOCKED)", types.FeeCeilingBps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeeFloor(t *testing.T) {
|
||||||
|
if types.FeeFloorBps != 1 {
|
||||||
|
t.Errorf("FeeFloorBps = %d, expected 1 (§18: 0.01pct floor LOCKED)", types.FeeFloorBps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInternalMinGrain(t *testing.T) {
|
||||||
|
if types.InternalMinGrain != 1 {
|
||||||
|
t.Errorf("InternalMinGrain = %d, expected 1 (§18: 1 Grain LOCKED)", types.InternalMinGrain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClamp(t *testing.T) {
|
||||||
|
if types.Clamp(15) != 10 {
|
||||||
|
t.Error("Clamp(15) should return 10 (ceiling)")
|
||||||
|
}
|
||||||
|
if types.Clamp(0) != 1 {
|
||||||
|
t.Error("Clamp(0) should return 1 (floor)")
|
||||||
|
}
|
||||||
|
if types.Clamp(5) != 5 {
|
||||||
|
t.Error("Clamp(5) should return 5 (within bounds)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVolumeTiers(t *testing.T) {
|
||||||
|
if types.VolumeTier2Bps != 7 {
|
||||||
|
t.Error("VolumeTier2 should be 7 bps (§18)")
|
||||||
|
}
|
||||||
|
if types.VolumeFloorBps != 1 {
|
||||||
|
t.Error("VolumeFloor should be 1 bps (§18)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessorShares(t *testing.T) {
|
||||||
|
if types.ProcessorShareGenesis != 50 {
|
||||||
|
t.Error("ProcessorShareGenesis should be 50pct (§18)")
|
||||||
|
}
|
||||||
|
if types.ProcessorShareTier4 != 10 {
|
||||||
|
t.Error("ProcessorShareTier4 should be 10pct (§18)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaivers(t *testing.T) {
|
||||||
|
waivers := []types.WaiverReason{
|
||||||
|
types.WaiverHandPassGuild, types.WaiverCrewInternalVault,
|
||||||
|
types.WaiverHouseholdInternal, types.WaiverCoverCall,
|
||||||
|
types.WaiverTrustDistribution, types.WaiverFoundationGrant,
|
||||||
|
types.WaiverCircleActiveCycle,
|
||||||
|
}
|
||||||
|
for _, w := range waivers {
|
||||||
|
if !types.IsWaived(w) {
|
||||||
|
t.Errorf("Waiver %s should be free (§18)", w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultFeeSchedule(t *testing.T) {
|
||||||
|
schedule := types.DefaultFeeSchedule()
|
||||||
|
if schedule.CurrentBps != 10 {
|
||||||
|
t.Error("Default CurrentBps should be 10 (Genesis ceiling)")
|
||||||
|
}
|
||||||
|
if schedule.VolumeTier != types.TierGenesis {
|
||||||
|
t.Error("Default VolumeTier should be Genesis")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "identity"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reach is the mesh-native identity layer (§8, §9.6 Layer 1)
|
||||||
|
// Cryptographic, virtual, no state identity
|
||||||
|
type Reach struct {
|
||||||
|
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||||
|
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||||
|
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||||
|
PublicKey string `json:"public_key" yaml:"public_key"`
|
||||||
|
IsNomad bool `json:"is_nomad" yaml:"is_nomad"`
|
||||||
|
IsFreeholder bool `json:"is_freeholder" yaml:"is_freeholder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CitizenshipTier (§9)
|
||||||
|
type CitizenshipTier string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TierNomad CitizenshipTier = "Nomad" // any Holder (lifestyle tier)
|
||||||
|
TierFreeholder CitizenshipTier = "Freeholder" // earned full recognition (sovereign tier)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TwoLayerPassport (§9.6)
|
||||||
|
// Layer 1: Reach (mesh-native, virtual, cryptographic)
|
||||||
|
// Layer 2: Pier-routed real-world credentials (e-Residency, biometric IDs)
|
||||||
|
// OY does not issue passports; OY routes to Piers that do.
|
||||||
|
type TwoLayerPassport struct {
|
||||||
|
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||||
|
Layer1Reach bool `json:"layer1_reach" yaml:"layer1_reach"`
|
||||||
|
Layer2PierRouted bool `json:"layer2_pier_routed" yaml:"layer2_pier_routed"`
|
||||||
|
PierCredentialRef string `json:"pier_credential_ref" yaml:"pier_credential_ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WayfarersRecord is an optional Reach-signed attestation (§9.5)
|
||||||
|
// Credential of belonging, not state identity
|
||||||
|
type WayfarersRecord struct {
|
||||||
|
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||||
|
FreeholderStatus bool `json:"freeholder_status" yaml:"freeholder_status"`
|
||||||
|
StandingScore float64 `json:"standing_score" yaml:"standing_score"`
|
||||||
|
GuildAffiliations []string `json:"guild_affiliations" yaml:"guild_affiliations"`
|
||||||
|
BloomHistory string `json:"bloom_history" yaml:"bloom_history"`
|
||||||
|
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct{}
|
||||||
|
|
||||||
|
func DefaultParams() Params { return Params{} }
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
Reaches []Reach `json:"reaches" yaml:"reaches"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams(), Reaches: []Reach{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "rootpool"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// RootPool is the mesh's treasury (§5)
|
||||||
|
// Attested by Watchers, visible to every Holder.
|
||||||
|
type RootPool struct {
|
||||||
|
BalanceGrain int64 `json:"balance_grain" yaml:"balance_grain"`
|
||||||
|
LastAttestation int64 `json:"last_attestation" yaml:"last_attestation"`
|
||||||
|
QuorumMet bool `json:"quorum_met" yaml:"quorum_met"`
|
||||||
|
ReserveRatioPct uint32 `json:"reserve_ratio_pct" yaml:"reserve_ratio_pct"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootPoolEntry tracks incoming/outgoing Root-Pool movements
|
||||||
|
type RootPoolEntry struct {
|
||||||
|
EntryID string `json:"entry_id" yaml:"entry_id"`
|
||||||
|
Source string `json:"source" yaml:"source"`
|
||||||
|
Amount int64 `json:"amount" yaml:"amount"`
|
||||||
|
Reason string `json:"reason" yaml:"reason"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct{}
|
||||||
|
|
||||||
|
func DefaultParams() Params { return Params{} }
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
RootPool RootPool `json:"root_pool" yaml:"root_pool"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "standing"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
|
||||||
|
// Standing formula parameters (§9.2 — LOCKED)
|
||||||
|
PriorMean = 4.0 // Bayesian prior mean
|
||||||
|
PriorWeight = 10 // Bayesian prior weight
|
||||||
|
DecayBucket6mo = 1.0 // 6mo: 100pct
|
||||||
|
DecayBucket12mo = 0.5 // 12mo: 50pct
|
||||||
|
DecayBucket24mo = 0.25 // 24mo: 25pct
|
||||||
|
DecayBucket24plus = 0.0 // 24mo+: 0pct
|
||||||
|
|
||||||
|
// Diversity bonus (§9.2)
|
||||||
|
DiversityBonus3Cats = 0.05
|
||||||
|
DiversityBonus4Cats = 0.10
|
||||||
|
DiversityBonus5Cats = 0.15
|
||||||
|
|
||||||
|
// Voucher weights (§9.2)
|
||||||
|
VoucherWeightFreeholder = 1.5
|
||||||
|
VoucherWeight45Plus = 1.2
|
||||||
|
VoucherWeight40To45 = 1.0
|
||||||
|
VoucherWeightBelow40 = 0.5
|
||||||
|
VoucherWeightBelow10Ratings = 0.3
|
||||||
|
|
||||||
|
// Minimum counterparties (§9.2)
|
||||||
|
MinCounterpartiesValid = 3 // to be valid
|
||||||
|
MinCounterpartiesFreeholderEligible = 10 // to be Freeholder-eligible
|
||||||
|
MinCounterpartiesFreeholderStatus = 30 // across 3 categories for Freeholder status
|
||||||
|
|
||||||
|
// Freeholder Four Signals (§9.1 — LOCKED)
|
||||||
|
FreeholderStashMaturityDays = 90 // 90 days Stash activity
|
||||||
|
FreeholderStashMaxGapDays = 30 // no gap > 30 days
|
||||||
|
FreeholderMinStandingScore = 4.5 // 4.5+ in at least 3 service categories
|
||||||
|
FreeholderMinCategories = 3 // at least 3 service categories
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rating is a single rating event (§9.2)
|
||||||
|
type Rating struct {
|
||||||
|
RaterID string `json:"rater_id" yaml:"rater_id"`
|
||||||
|
RateeID string `json:"ratee_id" yaml:"ratee_id"`
|
||||||
|
Category string `json:"category" yaml:"category"`
|
||||||
|
Score float64 `json:"score" yaml:"score"` // 0.0-5.0
|
||||||
|
Weight float64 `json:"weight" yaml:"weight"` // derived from rater Voucher_Weight
|
||||||
|
TxRef string `json:"tx_ref" yaml:"tx_ref"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
DecayBucket uint8 `json:"decay_bucket" yaml:"decay_bucket"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vouch is a Freeholder vouch with skin-in-the-game (§9.1)
|
||||||
|
type Vouch struct {
|
||||||
|
VoucherID string `json:"voucher_id" yaml:"voucher_id"`
|
||||||
|
VoucheeID string `json:"vouchee_id" yaml:"vouchee_id"`
|
||||||
|
Category string `json:"category" yaml:"category"`
|
||||||
|
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"` // voucher skin-in-the-game
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slash penalizes a Holder (§9.4)
|
||||||
|
type Slash struct {
|
||||||
|
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||||
|
Amount float64 `json:"amount" yaml:"amount"`
|
||||||
|
Reason string `json:"reason" yaml:"reason"` // Crack, FraudulentCoverCall, InactivityTimeout
|
||||||
|
Attester string `json:"attester" yaml:"attester"` // Watcher ID
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StandingBucket for display (§9.2)
|
||||||
|
type StandingBucket string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BucketNew StandingBucket = "New" // <10 ratings
|
||||||
|
BucketTrusted StandingBucket = "Trusted" // 4.0-4.4
|
||||||
|
BucketPreferred StandingBucket = "Preferred" // 4.5-4.7
|
||||||
|
BucketTop StandingBucket = "Top" // 4.8+
|
||||||
|
BucketSlashed StandingBucket = "Slashed" // penalized
|
||||||
|
)
|
||||||
|
|
||||||
|
// FreeholderSignals — all four must be present (§9.1 LOCKED)
|
||||||
|
type FreeholderSignals struct {
|
||||||
|
StashMaturity bool `json:"stash_maturity" yaml:"stash_maturity"`
|
||||||
|
MultiDomainStanding bool `json:"multi_domain_standing" yaml:"multi_domain_standing"`
|
||||||
|
CommittedCapital bool `json:"committed_capital" yaml:"committed_capital"`
|
||||||
|
CommunityEndorsement bool `json:"community_endorsement" yaml:"community_endorsement"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFreeholderEligible checks all four signals (§9.1)
|
||||||
|
// No application, no committee, no form. All four must be present.
|
||||||
|
func (s FreeholderSignals) IsFreeholderEligible() bool {
|
||||||
|
return s.StashMaturity && s.MultiDomainStanding && s.CommittedCapital && s.CommunityEndorsement
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeDiversityBonus returns the bonus for category count (§9.2)
|
||||||
|
func ComputeDiversityBonus(categoryCount int) float64 {
|
||||||
|
switch {
|
||||||
|
case categoryCount >= 5:
|
||||||
|
return DiversityBonus5Cats
|
||||||
|
case categoryCount == 4:
|
||||||
|
return DiversityBonus4Cats
|
||||||
|
case categoryCount == 3:
|
||||||
|
return DiversityBonus3Cats
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVoucherWeight returns the weight for a given rater profile (§9.2)
|
||||||
|
func GetVoucherWeight(isFreeholder bool, standingScore float64, ratingCount int) float64 {
|
||||||
|
if isFreeholder {
|
||||||
|
return VoucherWeightFreeholder
|
||||||
|
}
|
||||||
|
if ratingCount < 10 {
|
||||||
|
return VoucherWeightBelow10Ratings
|
||||||
|
}
|
||||||
|
if standingScore >= 4.5 {
|
||||||
|
return VoucherWeight45Plus
|
||||||
|
}
|
||||||
|
if standingScore >= 4.0 {
|
||||||
|
return VoucherWeight40To45
|
||||||
|
}
|
||||||
|
return VoucherWeightBelow40
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStandingBucket returns the display bucket for a score (§9.2)
|
||||||
|
func GetStandingBucket(score float64, ratingCount int, isSlashed bool) StandingBucket {
|
||||||
|
if isSlashed {
|
||||||
|
return BucketSlashed
|
||||||
|
}
|
||||||
|
if ratingCount < 10 {
|
||||||
|
return BucketNew
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case score >= 4.8:
|
||||||
|
return BucketTop
|
||||||
|
case score >= 4.5:
|
||||||
|
return BucketPreferred
|
||||||
|
case score >= 4.0:
|
||||||
|
return BucketTrusted
|
||||||
|
}
|
||||||
|
return BucketNew
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct{}
|
||||||
|
|
||||||
|
func DefaultParams() Params { return Params{} }
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
Ratings []Rating `json:"ratings" yaml:"ratings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams(), Ratings: []Rating{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/standing/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFreeholderSignalsAllPresent(t *testing.T) {
|
||||||
|
signals := types.FreeholderSignals{
|
||||||
|
StashMaturity: true,
|
||||||
|
MultiDomainStanding: true,
|
||||||
|
CommittedCapital: true,
|
||||||
|
CommunityEndorsement: true,
|
||||||
|
}
|
||||||
|
if !signals.IsFreeholderEligible() {
|
||||||
|
t.Error("All four signals present should be Freeholder eligible (§9.1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFreeholderSignalsMissing(t *testing.T) {
|
||||||
|
signals := types.FreeholderSignals{
|
||||||
|
StashMaturity: true,
|
||||||
|
MultiDomainStanding: true,
|
||||||
|
CommittedCapital: true,
|
||||||
|
CommunityEndorsement: false, // missing
|
||||||
|
}
|
||||||
|
if signals.IsFreeholderEligible() {
|
||||||
|
t.Error("Missing one signal should NOT be Freeholder eligible (§9.1: all four must be present)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiversityBonus(t *testing.T) {
|
||||||
|
if types.ComputeDiversityBonus(3) != 0.05 {
|
||||||
|
t.Error("3 categories should give +0.05 bonus (§9.2)")
|
||||||
|
}
|
||||||
|
if types.ComputeDiversityBonus(4) != 0.10 {
|
||||||
|
t.Error("4 categories should give +0.10 bonus (§9.2)")
|
||||||
|
}
|
||||||
|
if types.ComputeDiversityBonus(5) != 0.15 {
|
||||||
|
t.Error("5+ categories should give +0.15 bonus (§9.2)")
|
||||||
|
}
|
||||||
|
if types.ComputeDiversityBonus(2) != 0.0 {
|
||||||
|
t.Error("2 categories should give 0 bonus (§9.2)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVoucherWeights(t *testing.T) {
|
||||||
|
if types.GetVoucherWeight(true, 4.0, 100) != 1.5 {
|
||||||
|
t.Error("Freeholder weight should be 1.5x (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetVoucherWeight(false, 4.6, 100) != 1.2 {
|
||||||
|
t.Error("4.5+ with 1-2 cats should be 1.2x (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetVoucherWeight(false, 4.2, 100) != 1.0 {
|
||||||
|
t.Error("4.0-4.5 should be 1.0x (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetVoucherWeight(false, 3.5, 100) != 0.5 {
|
||||||
|
t.Error("Below 4.0 should be 0.5x (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetVoucherWeight(false, 4.0, 5) != 0.3 {
|
||||||
|
t.Error("Below 10 ratings should be 0.3x (§9.2)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStandingBuckets(t *testing.T) {
|
||||||
|
if types.GetStandingBucket(4.9, 100, false) != types.BucketTop {
|
||||||
|
t.Error("4.8+ should be Top (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetStandingBucket(4.6, 100, false) != types.BucketPreferred {
|
||||||
|
t.Error("4.5-4.7 should be Preferred (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetStandingBucket(4.2, 100, false) != types.BucketTrusted {
|
||||||
|
t.Error("4.0-4.4 should be Trusted (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetStandingBucket(3.5, 100, false) != types.BucketNew {
|
||||||
|
t.Error("Below 4.0 should be New (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetStandingBucket(4.5, 5, false) != types.BucketNew {
|
||||||
|
t.Error("Below 10 ratings should be New (§9.2)")
|
||||||
|
}
|
||||||
|
if types.GetStandingBucket(4.5, 100, true) != types.BucketSlashed {
|
||||||
|
t.Error("Slashed should be Slashed bucket (§9.2)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLockedConstants(t *testing.T) {
|
||||||
|
if types.FreeholderStashMaturityDays != 90 {
|
||||||
|
t.Error("Stash maturity should be 90 days (§9.1 LOCKED)")
|
||||||
|
}
|
||||||
|
if types.FreeholderMinStandingScore != 4.5 {
|
||||||
|
t.Error("Min standing score should be 4.5 (§9.1 LOCKED)")
|
||||||
|
}
|
||||||
|
if types.FreeholderMinCategories != 3 {
|
||||||
|
t.Error("Min categories should be 3 (§9.1 LOCKED)")
|
||||||
|
}
|
||||||
|
if types.MinCounterpartiesFreeholderStatus != 30 {
|
||||||
|
t.Error("Min counterparties for Freeholder status should be 30 (§9.2)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "stash"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stash is a per-Holder sovereign storage (§5)
|
||||||
|
// The Holder owns it. No one else can access it without a Window (§10).
|
||||||
|
type Stash struct {
|
||||||
|
HolderID string `json:"holder_id" yaml:"holder_id"`
|
||||||
|
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||||
|
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||||
|
LastActive int64 `json:"last_active" yaml:"last_active"`
|
||||||
|
BalanceGrain int64 `json:"balance_grain" yaml:"balance_grain"`
|
||||||
|
IsStill bool `json:"is_still" yaml:"is_still"` // Still = Holder paused partner access (§10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StashActivity tracks Stash maturity for Freeholder signals (§9.1: 90 days, no gap >30 days)
|
||||||
|
type StashActivity struct {
|
||||||
|
StashID string `json:"stash_id" yaml:"stash_id"`
|
||||||
|
ActiveDays uint32 `json:"active_days" yaml:"active_days"`
|
||||||
|
MaxGapDays uint32 `json:"max_gap_days" yaml:"max_gap_days"` // must be ≤30 for Freeholder
|
||||||
|
LastActivityDay int64 `json:"last_activity_day" yaml:"last_activity_day"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaturityThreshold for Freeholder signal (§9.1: 90 days)
|
||||||
|
const MaturityThresholdDays = 90
|
||||||
|
|
||||||
|
// MaxGapForMaturity for Freeholder signal (§9.1: no gap > 30 days)
|
||||||
|
const MaxGapForMaturity = 30
|
||||||
|
|
||||||
|
// IsMature checks if Stash activity meets the Freeholder maturity threshold (§9.1)
|
||||||
|
func (a StashActivity) IsMature() bool {
|
||||||
|
return a.ActiveDays >= MaturityThresholdDays && a.MaxGapDays <= MaxGapForMaturity
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct{}
|
||||||
|
|
||||||
|
func DefaultParams() Params { return Params{} }
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
Stashes []Stash `json:"stashes" yaml:"stashes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams(), Stashes: []Stash{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/stash/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMaturityThreshold(t *testing.T) {
|
||||||
|
if types.MaturityThresholdDays != 90 {
|
||||||
|
t.Errorf("MaturityThresholdDays = %d, expected 90 (§9.1)", types.MaturityThresholdDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxGapForMaturity(t *testing.T) {
|
||||||
|
if types.MaxGapForMaturity != 30 {
|
||||||
|
t.Errorf("MaxGapForMaturity = %d, expected 30 (§9.1: no gap > 30 days)", types.MaxGapForMaturity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStashIsMature(t *testing.T) {
|
||||||
|
mature := types.StashActivity{ActiveDays: 90, MaxGapDays: 15}
|
||||||
|
if !mature.IsMature() {
|
||||||
|
t.Error("90 days active, 15 max gap should be mature (§9.1)")
|
||||||
|
}
|
||||||
|
|
||||||
|
notEnoughDays := types.StashActivity{ActiveDays: 89, MaxGapDays: 15}
|
||||||
|
if notEnoughDays.IsMature() {
|
||||||
|
t.Error("89 days should NOT be mature (threshold = 90)")
|
||||||
|
}
|
||||||
|
|
||||||
|
gapTooBig := types.StashActivity{ActiveDays: 100, MaxGapDays: 31}
|
||||||
|
if gapTooBig.IsMature() {
|
||||||
|
t.Error("31-day gap should NOT be mature (max gap = 30)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStashStillFlag(t *testing.T) {
|
||||||
|
stash := types.Stash{IsStill: true}
|
||||||
|
if !stash.IsStill {
|
||||||
|
t.Error("Stash with IsStill=true should reflect Still state (§10: Holder paused partner access)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "vault"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vault is a per-Stand governed storage (§5)
|
||||||
|
// Governed by the Stand Council (§19). Holds shared funds for a Stand.
|
||||||
|
type Vault struct {
|
||||||
|
VaultID string `json:"vault_id" yaml:"vault_id"`
|
||||||
|
StandID string `json:"stand_id" yaml:"stand_id"`
|
||||||
|
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||||
|
BalanceGrain int64 `json:"balance_grain" yaml:"balance_grain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultAllocation tracks sub-Vault allocations within a Stand
|
||||||
|
type VaultAllocation struct {
|
||||||
|
VaultID string `json:"vault_id" yaml:"vault_id"`
|
||||||
|
SubVaultID string `json:"sub_vault_id" yaml:"sub_vault_id"`
|
||||||
|
Percentage uint32 `json:"percentage" yaml:"percentage"`
|
||||||
|
Purpose string `json:"purpose" yaml:"purpose"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Params struct{}
|
||||||
|
|
||||||
|
func DefaultParams() Params { return Params{} }
|
||||||
|
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
Vaults []Vault `json:"vaults" yaml:"vaults"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{Params: DefaultParams(), Vaults: []Vault{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
Reference in New Issue
Block a user