# SPEC — BitPatagonia 27 MW profit-share token

Version 0.2 (2026-10-06, after implementation; see §8 for changes from 0.1) · Source of economics: `params.json`, `model/BitPatagonia_Inversores_Generacion_27MW_v7.xlsx`
(tab *Opción A — Socio BitPatagonia*; scenario blocks rows 61–180; ratchet table rows 183–194).

## 1. Parties and flows

```
Investors ──USDC──▶ Subscription ──mint 1:1──▶ BPT token (permissioned ERC-20)
                         │
                         └──USDC (raise)──▶ BitPatagonia treasury (buys generators)

BitPatagonia ──monthly USDC deposit + report hash──▶ Distribution
Distribution ──waterfall──▶ investor pool (claimable pro rata) + company remainder (returned)
```

The token represents the investor-side economics of Category B: 100 % of distributions until
1.0× recovery, then a share fixed by the ratchet. BitPatagonia's own share (Category B in kind
+ Category A) stays off-chain.

## 2. Parameters (immutable after deployment, loaded from `params.json`)

| Name | Value | Notes |
|---|---|---|
| `SOFT_CAP` | 9,720,000 USDC | minimum raise (= `params.json` raise); below it → refunds |
| `HARD_CAP` | configurable ≥ soft cap | more may be raised ("a larger project"); 1 token = 1 USDC |
| `RAISE` | `token.totalSupply()` at close | read on-chain by `Distribution`; not a constant |
| `HURDLE_MULTIPLE_WAD` | 1e18 (1.0×) | `hurdle = RAISE × multiple / 1e18` |
| `RESERVE_BPS` | 500 (5 %) | company reserve withheld from positive distributable before the pool |
| `BASE_SHARE_WAD` | 275083875385142000 (27.5083875385142 %) | share after recovery if recovery ≤ `TARGET_MONTH` |
| `MAX_SHARE_WAD` | 350000000000000000 (35 %) | cap |
| `TARGET_MONTH` | 29 | ratchet starts after this month |
| `MONTH_AT_MAX` | 36 | share reaches cap at this month |
| `MONTH_0` | timestamp of COD | informational; months are posted sequentially by the reporter |
| `HORIZON_MONTHS` | 96 | informational; contract keeps running after |

Integer form of `params.json` is generated into `deploy/params.units.json`
(`uv run python -m reference.params_units`); contracts and deploy scripts read only that file.

Rounding: all USDC math in 6 decimals, floor division; shares in 18-decimal fixed point (WAD —
basis points drift ~8 USD/month from the model). Ratchet: `share = BASE + (MAX − BASE) × k / 7`,
`k = clamp(recoveryMonth − 29, 0, 7)`, exact at both ends. Dust from pro-rata floor division stays
in the contract and is reported by `unclaimed()` (not swept; < 1e-6 USDC per holder per month).

If more than `SOFT_CAP` is raised, `investor_base_share` (= raise / post-money) must be re-derived in
`params.json` before phase-2 deployment; `DeployDistribution` refuses a mismatch unless
`ALLOW_RAISE_MISMATCH=true`.

## 3. Contracts

### 3.1 `BPToken` — permissioned ERC-20
- 6 decimals, name/symbol configurable, fixed supply after `Subscription.close()`.
- `transfer`/`transferFrom` succeed only if `registry.canTransfer(from, to, amount)` is true.
- `ComplianceRegistry`: issuer-managed whitelist with per-address fields `{kycVerified,
  jurisdiction, lockupUntil, frozen}`; `canTransfer` enforces both ends whitelisted, neither
  frozen, lock-up expired for `from`, and optional per-jurisdiction rules (table, not code).
- `forceTransfer(from, to, amount, reason)` — issuer-only, emits event; for court orders / lost keys.
- `pause()` / `unpause()` — issuer multisig.
- Snapshots: ERC-20 votes-style checkpoints (OpenZeppelin `ERC20Votes` without delegation,
  or a minimal checkpoint library) so `balanceOfAt(holder, snapshotId)` is available for claims.
- No minting outside `Subscription`; no burning except `Subscription.refund()`.

### 3.2 `Subscription`
- `subscribe(amount)` during `[openAt, closeAt]`, requires whitelist; transfers USDC in; mints
  `amount` tokens 1:1; enforces `minTicket`, `hardCap = RAISE`.
- `close()` after `closeAt` or when hard cap reached: if `totalRaised ≥ softCap` → forwards USDC
  to `treasury` and freezes minting; else → enables `refund()` for all subscribers (burns tokens).
- Events for every subscription, close, refund.

### 3.3 `Waterfall` (pure library, no storage) — the business math
```
struct State { uint256 cumulativePaid; uint32 recoveryMonth; uint64 lockedShareWad; }

function step(State s, uint32 month, int256 distributable, Params p)
    returns (State s2, uint256 investorPayout, uint256 companyRemainder, uint256 reserve)
{
    uint256 positive = distributable > 0 ? uint256(distributable) : 0;
    reserve  = positive * RESERVE_BPS / 10_000;
    uint256 pool = positive - reserve;
    uint256 need = max(0, RAISE * HURDLE_MULTIPLE - s.cumulativePaid);
    uint256 pref = min(pool, need);
    uint256 excess = pool - pref;
    if (s.recoveryMonth == 0 && pool >= need) {            // recovery happens this month
        s2.recoveryMonth = month;
        k = min(max(0, month - TARGET_MONTH), MONTH_AT_MAX - TARGET_MONTH);
        s2.lockedShareWad = BASE_SHARE_WAD + (MAX_SHARE_WAD - BASE_SHARE_WAD) * k / (MONTH_AT_MAX - TARGET_MONTH);
    }
    uint64 share = s2.recoveryMonth == 0 ? BASE_SHARE_WAD : s2.lockedShareWad;  // irrelevant while excess == 0
    investorPayout   = pref + excess * share / 1e18;
    companyRemainder = excess - excess * share / 1e18;
    s2.cumulativePaid = s.cumulativePaid + investorPayout;
}
```
Notes: `need` uses cumulative **investor payouts** (nominal, no interest). A negative
`distributable` (e.g. the month-49 overhaul) yields zero payout and does not create a
carry-forward: the company absorbs negatives from its reserve, exactly as in the model
(row *Caja retenida en la compañía*). The 4- and 8-year exit residuals in the model (rows 35-37;
8-year = 10 % × generators + land = USD 1,722,000 added to month 96) are valuation exercises and
are **not** implemented on-chain. The scenario CSVs in `test_vectors/` include the 8-year residual in
their month-96 payout; the tests add it explicitly. `base_case.csv` does not include it.

### 3.4 `Distribution`
- `deposit(month, distributable, usdcAmount, reportHash)` — callable by `reporter` (multisig).
  `usdcAmount` must equal the `investorPayout` computed by `Waterfall.step` for that month
  (the company only deposits the investors' part; reserve and company remainder never touch the
  chain, but both are emitted in the event for transparency). Months must be deposited in order,
  once each.
- On deposit: snapshot = the deposit block (`BPToken` keeps per-account block checkpoints);
  claims open from the next block. A holder's claim = `investorPayout × balanceOfAt / totalSupplyAt`.
- `claim(month)` / `claimMany(months[])` / `claimRange(from, to)` — whitelist (`canReceive`) checked.
- `preview(distributable)` — what the next deposit must be; used by the reporting CLI.
- `state()` — exposes `cumulativePaid`, `recoveryMonth`, `lockedShareWad`, months deposited.
- Unclaimed balances stay claimable; no expiry. Token pause does **not** block claims.

### 3.5 Admin / roles
- `DEFAULT_ADMIN_ROLE` on every contract = OpenZeppelin `TimelockController` (48 h min delay);
  the issuer Safe is its proposer and executor. Role changes therefore wait 48 h.
- `ISSUER_ROLE` (issuer Safe): pause, unpause, forceTransfer.
- `REGISTRAR_ROLE` (KYC operations key): registry entries, freezes, lock-ups, jurisdiction tables.
- `REPORTER_ROLE` (2-of-3 Safe: CFO, Nick, external auditor): monthly deposits.
- `treasury`: receives the raise. `Subscription` has no admin at all.
- Chain: Base, native USDC. No proxy, no upgrade path; migration = new deployment + snapshot.

## 4. Off-chain components
- `reference/waterfall.py` — the same math in Python; `pytest` reproduces every file in
  `test_vectors/` (base case + 10 hashprice scenarios) and the headline numbers in
  `expected_results.json` (recovery month and applied share must match exactly; totals to the cent).
- `reporting/` — CLI that takes the month's operating figures (MWh, mining revenue, profit
  share, costs) as a signed JSON, computes `distributable`, runs the waterfall, prints the deposit
  amount and the report hash to post on-chain, and appends to a public ledger (JSON/CSV).
- `dashboard/` — static page: raise status, months paid, cumulative vs. hurdle, locked share,
  per-holder claimable (connect wallet), link to each month's report.

## 5. Tests (Foundry + pytest)
1. Vector tests: for each scenario CSV, feed `distributable` month by month into `Waterfall.step`
   and assert `investor_payout` and `investor_cumulative` (±1 USDC unit).
2. Ratchet: recovery at month 29 → 2,751 bps; 32 → ~3,072; 36+ → 3,500; share never changes after lock.
3. Hurdle: `cumulativePaid` never exceeds `RAISE` before the share applies; pref + excess == pool.
4. Subscription: hard cap, soft cap failure → refunds restore every balance; whitelist enforced.
5. Distribution: sum of claims == deposited (− dust); out-of-order or duplicate month reverts; snapshot
   isolates transfers made after deposit; paused token blocks transfers but not claims (decided).
6. Fuzz/invariants: no path pays investors more than `pref + share × excess`; no negative state.

## 6. Deliverables
- `contracts/` (Foundry project), `reference/` (Python), `reporting/`, `dashboard/`,
  `deploy/` (script reading `params.json`, writing an immutable `deployment.json`), `README.md`
  with a plain-language description of the waterfall for investors (Spanish + English).
- Gas report and a self-review checklist for an external audit.

## 7. Explicitly out of scope (until counsel signs off)
Secondary market / DEX listing, bridging, staking, governance tokens, any form of leverage,
automated on-chain oracle for mining revenue (reports are signed off-chain by design).

## 8. Changes from 0.1 (implementation, 2026-10-06)
- Shares in WAD instead of bps; ratchet written in the exact-endpoint form (§2, §3.3).
- Soft cap / hard cap split; `RAISE` is the supply actually sold and the hurdle is derived on-chain (§2).
- Snapshot at the deposit block, claims from the next block; `claimRange`, `preview` added (§3.4).
- Pause does not block claims (§3.4, §5.5). Dust reported, not swept (§2).
- Admin = 48 h `TimelockController`; registrar role split from issuer (§3.5).
- Month-96 residual documented and excluded; CSV precision caveats in the tests (§3.3).
