Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c59bd48e0 | |||
| 79a72c48cf | |||
| edea9422ac | |||
| 42895a74dd | |||
| b53118693f | |||
| 0131c0cbbb | |||
| d9a8b28cd3 | |||
| 269569bfe7 | |||
| fa54501858 | |||
| 71e429da5f |
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"phase": 0,
|
"phase": 2,
|
||||||
"stage": "complete",
|
"stage": "execute",
|
||||||
"milestone": "v0.1",
|
"milestone": "v0.1",
|
||||||
"phase_role": "pre_execution",
|
"phase_role": "execution",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-08-17T00:00:00Z"
|
"updated_at": "2026-08-17T00:00:00Z"
|
||||||
}
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# Component 1: OY Chain & Mirror — Phase 1 Skeleton
|
||||||
|
|
||||||
|
## Status
|
||||||
|
v0.0.1 — skeleton with type definitions and locked-constant tests
|
||||||
|
|
||||||
|
## Vision References
|
||||||
|
- §7: Architecture — Three Layers and the Mirror
|
||||||
|
- §3: Still/Stir (pause/resume)
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
### x/watcher — Watcher Bond & Attestation Quorum
|
||||||
|
- `types.Watcher` — bonded Watcher (§7: 100,000 Bread minimum bond)
|
||||||
|
- `types.Attestation` — daily reserve snapshot signed by a Watcher
|
||||||
|
- `types.ReserveSnapshot` — off-chain reserve state (off-chain total, on-chain Bread, basket breakdown)
|
||||||
|
- `types.QuorumStatus` — QuorumMet / QuorumLost / StaleEpoch
|
||||||
|
- `types.SlashReason` — InsufficientBond / StaleAttestation / GeographicCollusion / Forgery
|
||||||
|
|
||||||
|
**Locked constants (§7):**
|
||||||
|
- `MinBond = 100000` (100,000 Bread)
|
||||||
|
- `Quorum = 6` (6-of-9)
|
||||||
|
- `MaxWatchers = 9`
|
||||||
|
- `AttestationEpochHours = 24` (daily)
|
||||||
|
|
||||||
|
### x/mirror — Reserve Ratio Publication
|
||||||
|
- `types.NavPerBread` — Net Asset Value per Bread (§4.2)
|
||||||
|
- `types.ReserveRatio` — real-time published ratio with quorum flag
|
||||||
|
|
||||||
|
### x/still — Pause/Resume (Still/Stir)
|
||||||
|
- `types.StillState` — StirActive / StillPaused (§3)
|
||||||
|
- `types.StillReason` — CouncilVote / QuorumLost / SecurityAlert
|
||||||
|
- `types.StillRecord` — tracks pause events
|
||||||
|
- Auto-still on quorum lost (§4.2: Forge/Fold blocked until quorum restored)
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
12 tests, all passing:
|
||||||
|
- Watcher: MinBond, Quorum, MaxWatchers, AttestationEpoch, DefaultParams (5)
|
||||||
|
- Still: DefaultStillState, AutoStillOnQuorumLost, StillStates (3)
|
||||||
|
- Mirror: DefaultPublishInterval, ModuleName, NavPerBreadStruct, ReserveRatioQuorum (4)
|
||||||
|
|
||||||
|
## What's NOT in this skeleton (deferred to P1 completion)
|
||||||
|
- Cosmos SDK app.go full integration (requires SDK v0.50+ dependency resolution)
|
||||||
|
- Keeper implementations (BondWatcher, SubmitAttestation, VerifyQuorum, SlashWatcher)
|
||||||
|
- CLI commands (oyd tx watcher bond, oyd query watcher quorum)
|
||||||
|
- Proto definitions (Buf-generated types)
|
||||||
|
- CometBFT consensus setup
|
||||||
|
- Genesis import/export
|
||||||
|
|
||||||
|
## Next Steps (P1 continuation)
|
||||||
|
1. Resolve Cosmos SDK v0.50 dependency tree
|
||||||
|
2. Implement keeper logic with store operations
|
||||||
|
3. Add proto definitions and code generation
|
||||||
|
4. Wire modules into app.go
|
||||||
|
5. Build oyd daemon
|
||||||
|
6. Integration test: 9 Watchers bond, submit attestations, verify quorum
|
||||||
@@ -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 }
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "mirror"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// NavPerBread is the Net Asset Value per Bread (§4.2)
|
||||||
|
type NavPerBread struct {
|
||||||
|
Value int64 `json:"value" yaml:"value"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
Attestations uint8 `json:"attestations" yaml:"attestations"`
|
||||||
|
AuditRef string `json:"audit_ref" yaml:"audit_ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReserveRatio is the real-time published reserve ratio (§7)
|
||||||
|
type ReserveRatio struct {
|
||||||
|
Ratio float64 `json:"ratio" yaml:"ratio"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
QuorumMet bool `json:"quorum_met" yaml:"quorum_met"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params for the mirror module
|
||||||
|
type Params struct {
|
||||||
|
PublishIntervalBlocks uint64 `json:"publish_interval_blocks" yaml:"publish_interval_blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{
|
||||||
|
PublishIntervalBlocks: 144, // ~daily at 10min blocks
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/mirror/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultPublishInterval(t *testing.T) {
|
||||||
|
params := types.DefaultParams()
|
||||||
|
if params.PublishIntervalBlocks != 144 {
|
||||||
|
t.Errorf("PublishIntervalBlocks = %d, expected 144 (~daily at 10min blocks)", params.PublishIntervalBlocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModuleName(t *testing.T) {
|
||||||
|
if types.ModuleName != "mirror" {
|
||||||
|
t.Errorf("ModuleName = %s, expected 'mirror'", types.ModuleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNavPerBreadStruct(t *testing.T) {
|
||||||
|
nav := types.NavPerBread{
|
||||||
|
Value: 10000,
|
||||||
|
Timestamp: 1700000000,
|
||||||
|
Attestations: 7,
|
||||||
|
AuditRef: "audit-001",
|
||||||
|
}
|
||||||
|
if nav.Attestations < 6 {
|
||||||
|
t.Error("NAV attestations should be ≥6 for quorum (§7)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReserveRatioQuorum(t *testing.T) {
|
||||||
|
ratio := types.ReserveRatio{
|
||||||
|
Ratio: 1.0,
|
||||||
|
Timestamp: 1700000000,
|
||||||
|
QuorumMet: true,
|
||||||
|
}
|
||||||
|
if !ratio.QuorumMet {
|
||||||
|
t.Error("ReserveRatio should have QuorumMet=true for valid publication (§7)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,63 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleName = "still"
|
||||||
|
StoreKey = ModuleName
|
||||||
|
RouterKey = ModuleName
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
// StillState represents the pause/resume state (§3: Still/Stir)
|
||||||
|
type StillState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StirActive — normal operation, Forge/Fold enabled
|
||||||
|
StirActive StillState = "StirActive"
|
||||||
|
// StillPaused — Forge/Fold blocked until Stir
|
||||||
|
StillPaused StillState = "StillPaused"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StillReason defines why the system was stilled
|
||||||
|
type StillReason string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StillCouncilVote StillReason = "CouncilVote"
|
||||||
|
StillQuorumLost StillReason = "QuorumLost"
|
||||||
|
StillSecurityAlert StillReason = "SecurityAlert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StillRecord tracks a pause event
|
||||||
|
type StillRecord struct {
|
||||||
|
State StillState `json:"state" yaml:"state"`
|
||||||
|
Reason StillReason `json:"reason" yaml:"reason"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
TriggeredBy string `json:"triggered_by" yaml:"triggered_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params for the still module
|
||||||
|
type Params struct {
|
||||||
|
AutoStillOnQuorumLost bool `json:"auto_still_on_quorum_lost" yaml:"auto_still_on_quorum_lost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{
|
||||||
|
AutoStillOnQuorumLost: true, // §4.2: Forge/Fold blocked until quorum restored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenesisState defines the still module genesis state
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
State *StillRecord `json:"state" yaml:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
|
State: &StillRecord{State: StirActive},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/still/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultStillState(t *testing.T) {
|
||||||
|
state := types.DefaultGenesisState()
|
||||||
|
if state.State.State != types.StirActive {
|
||||||
|
t.Errorf("Default state = %s, expected StirActive", state.State.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoStillOnQuorumLost(t *testing.T) {
|
||||||
|
params := types.DefaultParams()
|
||||||
|
if !params.AutoStillOnQuorumLost {
|
||||||
|
t.Error("AutoStillOnQuorumLost should be true (§4.2: Forge/Fold blocked until quorum restored)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStillStates(t *testing.T) {
|
||||||
|
if types.StirActive != "StirActive" {
|
||||||
|
t.Errorf("StirActive = %s, expected 'StirActive'", types.StirActive)
|
||||||
|
}
|
||||||
|
if types.StillPaused != "StillPaused" {
|
||||||
|
t.Errorf("StillPaused = %s, expected 'StillPaused'", types.StillPaused)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ModuleName is the name of the watcher module
|
||||||
|
ModuleName = "watcher"
|
||||||
|
|
||||||
|
// StoreKey is the string store key
|
||||||
|
StoreKey = ModuleName
|
||||||
|
|
||||||
|
// RouterKey is the message route for the watcher module
|
||||||
|
RouterKey = ModuleName
|
||||||
|
|
||||||
|
// QuerierRoute is the querier route for the watcher module
|
||||||
|
QuerierRoute = ModuleName
|
||||||
|
|
||||||
|
// MinBond is the minimum bond for a Watcher: 100,000 Bread (§7)
|
||||||
|
// Bread atomic unit = Grain; 1 Bread = 10,000 Grain
|
||||||
|
MinBond = 100000
|
||||||
|
|
||||||
|
// Quorum is the minimum attestation count: 6-of-9 (§7)
|
||||||
|
Quorum = 6
|
||||||
|
|
||||||
|
// MaxWatchers is the maximum active Watcher count: 9 (§7)
|
||||||
|
MaxWatchers = 9
|
||||||
|
|
||||||
|
// AttestationEpochHours is the daily attestation window (§7)
|
||||||
|
AttestationEpochHours = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parameter store keys
|
||||||
|
var (
|
||||||
|
KeyActiveWatchers = []byte("ActiveWatchers")
|
||||||
|
KeyBondAmount = []byte("BondAmount")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher represents a bonded Watcher (§7)
|
||||||
|
type Watcher struct {
|
||||||
|
WatcherID string `json:"watcher_id" yaml:"watcher_id"`
|
||||||
|
BondAmount int64 `json:"bond_amount" yaml:"bond_amount"`
|
||||||
|
Jurisdiction string `json:"jurisdiction" yaml:"jurisdiction"`
|
||||||
|
Organization string `json:"organization" yaml:"organization"`
|
||||||
|
ActiveSince int64 `json:"active_since" yaml:"active_since"`
|
||||||
|
Slashes uint32 `json:"slashes" yaml:"slashes"`
|
||||||
|
LastAttestation int64 `json:"last_attestation" yaml:"last_attestation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attestation is a daily reserve snapshot signed by a Watcher (§7)
|
||||||
|
type Attestation struct {
|
||||||
|
WatcherID string `json:"watcher_id" yaml:"watcher_id"`
|
||||||
|
Snapshot ReserveSnapshot `json:"snapshot" yaml:"snapshot"`
|
||||||
|
Signature []byte `json:"signature" yaml:"signature"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReserveSnapshot is the off-chain reserve state attested by Watchers (§7)
|
||||||
|
type ReserveSnapshot struct {
|
||||||
|
OffChainTotal int64 `json:"off_chain_total" yaml:"off_chain_total"`
|
||||||
|
OnChainBread int64 `json:"on_chain_bread" yaml:"on_chain_bread"`
|
||||||
|
BasketBreakdown []BasketAsset `json:"basket_breakdown" yaml:"basket_breakdown"`
|
||||||
|
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||||
|
AuditorRef string `json:"auditor_ref" yaml:"auditor_ref"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasketAsset represents one asset in the Root Basket (§6)
|
||||||
|
type BasketAsset struct {
|
||||||
|
AssetType string `json:"asset_type" yaml:"asset_type"`
|
||||||
|
Amount int64 `json:"amount" yaml:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuorumStatus represents the current quorum state
|
||||||
|
type QuorumStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuorumMet QuorumStatus = "QuorumMet" // ≥6 valid sigs in last epoch
|
||||||
|
QuorumLost QuorumStatus = "QuorumLost" // <6 — Forge/Fold stilled
|
||||||
|
StaleEpoch QuorumStatus = "StaleEpoch" // no attestations in 24h
|
||||||
|
)
|
||||||
|
|
||||||
|
// SlashReason defines why a Watcher was slashed
|
||||||
|
type SlashReason string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SlashInsufficientBond SlashReason = "InsufficientBond"
|
||||||
|
SlashStaleAttestation SlashReason = "StaleAttestation"
|
||||||
|
SlashGeographicCollusion SlashReason = "GeographicCollusion"
|
||||||
|
SlashForgery SlashReason = "Forgery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params defines the parameters for the watcher module
|
||||||
|
type Params struct {
|
||||||
|
MinBondAmount int64 `json:"min_bond_amount" yaml:"min_bond_amount"`
|
||||||
|
QuorumCount uint32 `json:"quorum_count" yaml:"quorum_count"`
|
||||||
|
MaxWatchers uint32 `json:"max_watchers" yaml:"max_watchers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultParams returns default watcher module parameters
|
||||||
|
func DefaultParams() Params {
|
||||||
|
return Params{
|
||||||
|
MinBondAmount: int64(MinBond) * 10000, // 100,000 Bread in Grain
|
||||||
|
QuorumCount: Quorum,
|
||||||
|
MaxWatchers: MaxWatchers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenesisState defines the watcher module genesis state
|
||||||
|
type GenesisState struct {
|
||||||
|
Params Params `json:"params" yaml:"params"`
|
||||||
|
Watchers []Watcher `json:"watchers" yaml:"watchers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGenesisState() *GenesisState {
|
||||||
|
return &GenesisState{
|
||||||
|
Params: DefaultParams(),
|
||||||
|
Watchers: []Watcher{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateGenesis(bz json.RawMessage) error { return nil }
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oy/openyield/x/watcher/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMinBond(t *testing.T) {
|
||||||
|
if types.MinBond != 100000 {
|
||||||
|
t.Errorf("MinBond = %d, expected 100000 (§7: 100,000 Bread bond)", types.MinBond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuorum(t *testing.T) {
|
||||||
|
if types.Quorum != 6 {
|
||||||
|
t.Errorf("Quorum = %d, expected 6 (§7: 6-of-9 quorum)", types.Quorum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxWatchers(t *testing.T) {
|
||||||
|
if types.MaxWatchers != 9 {
|
||||||
|
t.Errorf("MaxWatchers = %d, expected 9 (§7: 9 active Watchers)", types.MaxWatchers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttestationEpoch(t *testing.T) {
|
||||||
|
if types.AttestationEpochHours != 24 {
|
||||||
|
t.Errorf("AttestationEpochHours = %d, expected 24 (§7: daily attestations)", types.AttestationEpochHours)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultParams(t *testing.T) {
|
||||||
|
params := types.DefaultParams()
|
||||||
|
if params.QuorumCount != 6 {
|
||||||
|
t.Errorf("DefaultParams QuorumCount = %d, expected 6", params.QuorumCount)
|
||||||
|
}
|
||||||
|
if params.MaxWatchers != 9 {
|
||||||
|
t.Errorf("DefaultParams MaxWatchers = %d, expected 9", params.MaxWatchers)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user