myco-bonding-curve/src/commitments/labor.py

173 lines
6.2 KiB
Python

"""Proof-of-contribution labor issuance channel.
Non-financial minting: verified labor/contribution units are converted
to $MYCO at a governed rate, subject to rate limits (flow dampening).
Design:
- An attestation system (oracle, DAO vote, or peer review) reports
"contribution units" for a contributor
- Each unit converts to $MYCO at a governed rate (tokens_per_unit)
- Rate limits prevent gaming: max units per period, cooldown between claims
- Contribution types have different conversion rates (code, governance, community)
This is the "subscription-based bonding curve" concept from the MycoFi paper,
extended to general labor contributions.
"""
import numpy as np
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ContributionType:
"""A type of contribution with its conversion rate."""
name: str
tokens_per_unit: float # $MYCO per contribution unit
max_units_per_period: float # Rate limit per period
period_length: float # Length of rate-limit period (time units)
cooldown: float = 0.0 # Minimum time between claims
decay_rate: float = 0.0 # Annual decay of unclaimed units (use-it-or-lose-it)
@dataclass
class ContributorState:
"""State for a single contributor."""
address: str
unclaimed_units: dict[str, float] = field(default_factory=dict)
claimed_this_period: dict[str, float] = field(default_factory=dict)
period_start: dict[str, float] = field(default_factory=dict)
last_claim_time: dict[str, float] = field(default_factory=dict)
total_claimed: float = 0.0
@dataclass
class LaborIssuanceSystem:
"""The labor issuance channel."""
contribution_types: dict[str, ContributionType]
global_rate_multiplier: float = 1.0 # Governance can scale all rates
total_minted: float = 0.0
max_total_mint: float = float("inf") # Cap on total labor-minted tokens
def attest_contribution(
system: LaborIssuanceSystem,
contributor: ContributorState,
contribution_type: str,
units: float,
current_time: float,
) -> ContributorState:
"""Record a contribution attestation.
Called by the oracle/attestation layer when a contribution is verified.
Does NOT mint tokens — just records units for later claiming.
"""
assert contribution_type in system.contribution_types
assert units > 0
ct = system.contribution_types[contribution_type]
# Apply decay to existing unclaimed units
if contribution_type in contributor.unclaimed_units:
last_time = contributor.last_claim_time.get(contribution_type, current_time)
elapsed = current_time - last_time
if ct.decay_rate > 0 and elapsed > 0:
decay = np.exp(-ct.decay_rate * elapsed)
contributor.unclaimed_units[contribution_type] *= decay
# Add new units
current = contributor.unclaimed_units.get(contribution_type, 0.0)
contributor.unclaimed_units[contribution_type] = current + units
return contributor
def claim_tokens(
system: LaborIssuanceSystem,
contributor: ContributorState,
contribution_type: str,
current_time: float,
units_to_claim: float | None = None,
) -> tuple[LaborIssuanceSystem, ContributorState, float]:
"""Claim $MYCO for verified contributions.
Returns (updated_system, updated_contributor, tokens_minted).
"""
assert contribution_type in system.contribution_types
ct = system.contribution_types[contribution_type]
# Check cooldown
last_claim = contributor.last_claim_time.get(contribution_type, -float("inf"))
if current_time - last_claim < ct.cooldown:
return system, contributor, 0.0
# Available units
available = contributor.unclaimed_units.get(contribution_type, 0.0)
if available <= 0:
return system, contributor, 0.0
# Check rate limit (reset period if needed)
period_start = contributor.period_start.get(contribution_type, current_time)
if current_time - period_start >= ct.period_length:
# New period
contributor.claimed_this_period[contribution_type] = 0.0
contributor.period_start[contribution_type] = current_time
claimed_so_far = contributor.claimed_this_period.get(contribution_type, 0.0)
remaining_allowance = ct.max_units_per_period - claimed_so_far
# Determine how many units to claim
if units_to_claim is not None:
to_claim = min(units_to_claim, available, remaining_allowance)
else:
to_claim = min(available, remaining_allowance)
if to_claim <= 0:
return system, contributor, 0.0
# Compute tokens
tokens = to_claim * ct.tokens_per_unit * system.global_rate_multiplier
# Check global cap
tokens = min(tokens, system.max_total_mint - system.total_minted)
if tokens <= 0:
return system, contributor, 0.0
# Update state
contributor.unclaimed_units[contribution_type] -= to_claim
contributor.claimed_this_period[contribution_type] = claimed_so_far + to_claim
contributor.last_claim_time[contribution_type] = current_time
contributor.total_claimed += tokens
system.total_minted += tokens
return system, contributor, tokens
def create_default_system() -> LaborIssuanceSystem:
"""Create a default labor issuance system with common contribution types."""
types = {
"code": ContributionType(
name="Code Contribution",
tokens_per_unit=10.0, # 10 MYCO per merged PR equivalent
max_units_per_period=50.0, # Max 50 units per period
period_length=30.0, # 30-day periods
cooldown=1.0, # 1-day cooldown between claims
),
"governance": ContributionType(
name="Governance Participation",
tokens_per_unit=5.0,
max_units_per_period=20.0,
period_length=30.0,
cooldown=7.0, # Weekly claims
),
"community": ContributionType(
name="Community Building",
tokens_per_unit=3.0,
max_units_per_period=100.0,
period_length=30.0,
cooldown=1.0,
decay_rate=0.1, # Unclaimed units decay
),
}
return LaborIssuanceSystem(contribution_types=types)