diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index cb055f0..f23810b 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -1,5 +1,5 @@ { - "phase": 1, + "phase": 2, "stage": "execute", "milestone": "v0.1", "phase_role": "execution", diff --git a/x/bread/types/types.go b/x/bread/types/types.go new file mode 100644 index 0000000..1651c3c --- /dev/null +++ b/x/bread/types/types.go @@ -0,0 +1,76 @@ +package types + +import "encoding/json" + +const ( + ModuleName = "bread" + StoreKey = ModuleName + RouterKey = ModuleName + QuerierRoute = ModuleName + + // Bread atomic unit = Grain (§4) + // 1 Bread = 10,000 Grain + GrainsPerBread = 10000 + + // Bread scale denominations (§4) + // Grain = 1 (atomic) + // Crumb = 100 Grain + // Bread = 10,000 Grain (~$1) + // Loaf = 10 Bread + // Batch = 100 Bread + // Cake = 1,000 Bread + // Bakery = 10,000 Bread + // Granary = 100,000 Bread + // Mill = 1,000,000 Bread + // Harvest = 10,000,000 Bread + // Earth = 100,000,000 Bread +) + +// BreadScale defines the unit scale (§4) +type BreadScale struct { + Name string `json:"name" yaml:"name"` + GrainValue int64 `json:"grain_value" yaml:"grain_value"` +} + +// BreadScaleAll returns all denominations (§4) +func BreadScaleAll() []BreadScale { + return []BreadScale{ + {"Grain", 1}, + {"Crumb", 100}, + {"Bread", 10000}, + {"Loaf", 100000}, + {"Batch", 1000000}, + {"Cake", 10000000}, + {"Bakery", 100000000}, + {"Granary", 1000000000}, + {"Mill", 10000000000}, + {"Harvest", 100000000000}, + {"Earth", 1000000000000}, + } +} + +// Params for the bread module +type Params struct { + DenomGrain string `json:"denom_grain" yaml:"denom_grain"` + DenomBread string `json:"denom_bread" yaml:"denom_bread"` +} + +func DefaultParams() Params { + return Params{ + DenomGrain: "ugrain", + DenomBread: "ubread", + } +} + +// GenesisState defines the bread module genesis state +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + } +} + +func ValidateGenesis(bz json.RawMessage) error { return nil } diff --git a/x/bread/types/types_test.go b/x/bread/types/types_test.go new file mode 100644 index 0000000..3458bc3 --- /dev/null +++ b/x/bread/types/types_test.go @@ -0,0 +1,45 @@ +package types_test + +import ( + "testing" + + "github.com/oy/openyield/x/bread/types" +) + +func TestGrainsPerBread(t *testing.T) { + if types.GrainsPerBread != 10000 { + t.Errorf("GrainsPerBread = %d, expected 10000 (§4: 1 Bread = 10,000 Grain)", types.GrainsPerBread) + } +} + +func TestBreadScaleCount(t *testing.T) { + scale := types.BreadScaleAll() + if len(scale) != 11 { + t.Errorf("BreadScale length = %d, expected 11 denominations (§4)", len(scale)) + } +} + +func TestBreadScaleValues(t *testing.T) { + scale := types.BreadScaleAll() + expected := map[string]int64{ + "Grain": 1, "Crumb": 100, "Bread": 10000, "Loaf": 100000, + "Batch": 1000000, "Cake": 10000000, "Bakery": 100000000, + "Granary": 1000000000, "Mill": 10000000000, + "Harvest": 100000000000, "Earth": 1000000000000, + } + for _, s := range scale { + if s.GrainValue != expected[s.Name] { + t.Errorf("%s = %d, expected %d", s.Name, s.GrainValue, expected[s.Name]) + } + } +} + +func TestDefaultDenoms(t *testing.T) { + params := types.DefaultParams() + if params.DenomGrain != "ugrain" { + t.Errorf("DenomGrain = %s, expected 'ugrain'", params.DenomGrain) + } + if params.DenomBread != "ubread" { + t.Errorf("DenomBread = %s, expected 'ubread'", params.DenomBread) + } +} diff --git a/x/forge/types/types.go b/x/forge/types/types.go new file mode 100644 index 0000000..690332c --- /dev/null +++ b/x/forge/types/types.go @@ -0,0 +1,107 @@ +package types + +import "encoding/json" + +const ( + ModuleName = "forge" + StoreKey = ModuleName + RouterKey = ModuleName + QuerierRoute = ModuleName +) + +// RootBasketAssetType defines the asset types in the Root Basket (§6) +type RootBasketAssetType string + +const ( + TreasuryShort RootBasketAssetType = "TreasuryShort" // §6 ~35% + CorporateIG RootBasketAssetType = "CorporateIG" // §6 ~25% + TokenizedGold RootBasketAssetType = "TokenizedGold" // §6 ~20% + Bitcoin RootBasketAssetType = "Bitcoin" // §6 ~10% + OtherRwa RootBasketAssetType = "OtherRwa" // §6 ~10% +) + +// BasketComposition defines the working Root Basket (§6) +// Mission-lock: governance-adjustable within bounds — not into leverage, +// not into futures, not into money creation. +type BasketComposition struct { + TreasuryShortPct uint32 `json:"treasury_short_pct" yaml:"treasury_short_pct"` // ~35 + CorporateIGPct uint32 `json:"corporate_ig_pct" yaml:"corporate_ig_pct"` // ~25 + TokenizedGoldPct uint32 `json:"tokenized_gold_pct" yaml:"tokenized_gold_pct"` // ~20 + BitcoinPct uint32 `json:"bitcoin_pct" yaml:"bitcoin_pct"` // ~10 + OtherRwaPct uint32 `json:"other_rwa_pct" yaml:"other_rwa_pct"` // ~10 +} + +// DefaultBasketComposition returns the working Root Basket (§6) +func DefaultBasketComposition() BasketComposition { + return BasketComposition{ + TreasuryShortPct: 35, + CorporateIGPct: 25, + TokenizedGoldPct: 20, + BitcoinPct: 10, + OtherRwaPct: 10, + } +} + +// Validate checks that the basket composition sums to 100 and stays within bounds +func (b BasketComposition) Validate() bool { + total := b.TreasuryShortPct + b.CorporateIGPct + b.TokenizedGoldPct + b.BitcoinPct + b.OtherRwaPct + if total != 100 { + return false + } + // Bounds: no single asset > 50%, no leverage/futures/money-creation + if b.TreasuryShortPct > 50 || b.CorporateIGPct > 50 || + b.TokenizedGoldPct > 50 || b.BitcoinPct > 50 || b.OtherRwaPct > 50 { + return false + } + return true +} + +// ForgeInput represents a Forge operation: mint Bread against provided RWA (§6) +type ForgeInput struct { + AssetType RootBasketAssetType `json:"asset_type" yaml:"asset_type"` + Amount int64 `json:"amount" yaml:"amount"` + TargetStash string `json:"target_stash" yaml:"target_stash"` +} + +// FoldInput represents a Fold operation: burn Bread, return RWA (§6) +type FoldInput struct { + BreadAmount int64 `json:"bread_amount" yaml:"bread_amount"` + TargetAsset RootBasketAssetType `json:"target_asset" yaml:"target_asset"` + TargetStash string `json:"target_stash" yaml:"target_stash"` +} + +// ForgeError defines error states (§4.2) +type ForgeError string + +const ( + ErrAssetNotSupported ForgeError = "AssetNotSupported" + ErrStillActive ForgeError = "StillActive" + ErrBelowMinimum ForgeError = "BelowMinimum" + ErrMirrorQuorumFailed ForgeError = "MirrorQuorumFailed" +) + +// Params for the forge module +type Params struct { + Basket BasketComposition `json:"basket" yaml:"basket"` + MinForgeGrains int64 `json:"min_forge_grains" yaml:"min_forge_grains"` // min 1 Crumb +} + +func DefaultParams() Params { + return Params{ + Basket: DefaultBasketComposition(), + MinForgeGrains: 100, // 1 Crumb + } +} + +// GenesisState defines the forge module genesis state +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + } +} + +func ValidateGenesis(bz json.RawMessage) error { return nil } diff --git a/x/forge/types/types_test.go b/x/forge/types/types_test.go new file mode 100644 index 0000000..c325e1e --- /dev/null +++ b/x/forge/types/types_test.go @@ -0,0 +1,81 @@ +package types_test + +import ( + "testing" + + "github.com/oy/openyield/x/forge/types" +) + +func TestDefaultBasketComposition(t *testing.T) { + basket := types.DefaultBasketComposition() + if basket.TreasuryShortPct != 35 { + t.Errorf("TreasuryShortPct = %d, expected 35 (§6)", basket.TreasuryShortPct) + } + if basket.CorporateIGPct != 25 { + t.Errorf("CorporateIGPct = %d, expected 25 (§6)", basket.CorporateIGPct) + } + if basket.TokenizedGoldPct != 20 { + t.Errorf("TokenizedGoldPct = %d, expected 20 (§6)", basket.TokenizedGoldPct) + } + if basket.BitcoinPct != 10 { + t.Errorf("BitcoinPct = %d, expected 10 (§6)", basket.BitcoinPct) + } + if basket.OtherRwaPct != 10 { + t.Errorf("OtherRwaPct = %d, expected 10 (§6)", basket.OtherRwaPct) + } +} + +func TestBasketValidation(t *testing.T) { + basket := types.DefaultBasketComposition() + if !basket.Validate() { + t.Error("Default basket should validate (sums to 100, within bounds)") + } + + badBasket := types.BasketComposition{ + TreasuryShortPct: 60, // exceeds 50% bound + CorporateIGPct: 20, + TokenizedGoldPct: 10, + BitcoinPct: 5, + OtherRwaPct: 5, + } + if badBasket.Validate() { + t.Error("Basket with >50% single asset should fail validation") + } + + sumBasket := types.BasketComposition{ + TreasuryShortPct: 30, + CorporateIGPct: 20, + TokenizedGoldPct: 20, + BitcoinPct: 10, + OtherRwaPct: 10, // sums to 90, not 100 + } + if sumBasket.Validate() { + t.Error("Basket not summing to 100 should fail validation") + } +} + +func TestRootBasketAssetTypes(t *testing.T) { + assets := []types.RootBasketAssetType{ + types.TreasuryShort, types.CorporateIG, types.TokenizedGold, + types.Bitcoin, types.OtherRwa, + } + if len(assets) != 5 { + t.Errorf("Expected 5 Root Basket asset types, got %d", len(assets)) + } +} + +func TestForgeErrorTypes(t *testing.T) { + if types.ErrAssetNotSupported != "AssetNotSupported" { + t.Error("ErrAssetNotSupported mismatch") + } + if types.ErrMirrorQuorumFailed != "MirrorQuorumFailed" { + t.Error("ErrMirrorQuorumFailed mismatch (§4.2: Forge/Fold blocked until restored)") + } +} + +func TestMinForgeGrains(t *testing.T) { + params := types.DefaultParams() + if params.MinForgeGrains != 100 { + t.Errorf("MinForgeGrains = %d, expected 100 (1 Crumb)", params.MinForgeGrains) + } +}