Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: contracts
# The sandbox tests build the contracts with `cargo near` (near-workspaces compile_project).
- name: Install cargo-near 0.22.0
run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/near/cargo-near/releases/download/cargo-near-v0.22.0/cargo-near-installer.sh | sh
- run: cargo check --workspace --all-targets
- run: cargo test --workspace

Expand Down
21 changes: 14 additions & 7 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,21 @@ Everything in a vault belongs to its owner. There is no split and no shared bala
### 3.2 Factory
```rust
pub struct Factory {
admin: AccountId, // can publish new vault code hashes only (see B12)
admin: AccountId, // uploads vault code for FUTURE vaults only; no path to funds (I5)
keeper: AccountId,
usdc: AccountId,
vault_code: VaultCode, // embedded vault wasm — regular deploy (KD-8)
vault_code: LazyOption<Vec<u8>>, // vault wasm uploaded once by the admin; regular deploy (KD-8)
vaults: LookupMap<AccountId, AccountId>,
created_today: (u64 /*day*/, u32 /*count*/),
daily_cap: u32,
}
```
| Method | Caller | Behaviour |
|---|---|---|
| `create_vault_for(owner)` | keeper | One per owner; daily cap; create sub-account; deploy code; `vault.new(owner, keeper, usdc)`; `storage_deposit` on USDC for the vault |
| `get_vault(owner)` | view | |
| `set_vault_code()` | admin | Raw wasm as the call input (not JSON). Emits `vault_code_updated {sha256, size}`. Affects only vaults created afterwards |
| `create_vault_for(owner)` | keeper | One per owner; daily cap. Batch: create `v<sha256(owner)[0..16]>.<factory>` → fund (code storage + 0.1 NEAR) → deploy → `new(owner, keeper, usdc)`. Then `on_vault_deployed`: on failure forget the vault and free the day's slot; on success `storage_deposit` the vault on USDC (registration_only) → `on_vault_registered` emits `vault_created` or `vault_registration_failed` |
| `retry_registration(owner)` | keeper | Re-run the USDC registration for an existing vault |
| `get_vault(owner)` · `get_vault_code_hash()` · `get_config()` | view | Code hash lets anyone compare it with a vault's on-chain code hash |

### 3.3 Vault
```rust
Expand All @@ -104,9 +106,9 @@ pub struct Vault {
| `set_plan(target_asset, destination_address, amount_per_run, interval_s, max_cost_bps)` | owner | **1 yocto** | Validates ranges; emits `plan_updated` |
| `resume()` | owner | **1 yocto** | Requires a complete plan |
| `pause()` | owner | – | Always available |
| `execute_buy(quote_ref, deposit_address, amount)` | keeper | storage deposit if needed | Requires `!paused`, plan set, `now ≥ last_run_at_s + interval_s`, `amount ≤ amount_per_run`. Sets `last_run_at_s = now`; registers `deposit_address` on USDC if needed **[verify whether 1Click pre-registers]**; `ft_transfer(deposit_address, amount)`; callback: on failure restore `last_run_at_s`, emit `buy_failed` |
| `execute_buy(quote_ref, deposit_address, amount)` | keeper | storage deposit if needed | Requires `!paused`, plan set, `now ≥ last_run_at_s + interval_s`, `amount ≤ amount_per_run`. Sets `last_run_at_s = now`. If the keeper attaches NEAR, first `storage_deposit(deposit_address, registration_only)` with it (refunded if already registered); with no deposit, an unregistered address makes the transfer fail cleanly. Then `ft_transfer(deposit_address, amount)`; callback `on_buy_transfer`: success → `buy_sent`; failure → restore `last_run_at_s`, `buy_failed`. **[verify whether 1Click pre-registers, K0.9]** |
| `withdraw(amount)` | owner | **1 yocto** | `ft_transfer(owner, amount)` USDC |
| `recover_token(token, amount)` | owner | **1 yocto** | Send any other NEP-141 to the owner |
| `recover_token(token, amount)` | owner | **1 yocto** | Send any other NEP-141 to the owner; refuses USDC (use `withdraw`) |
| `get_state()` | view | – | Plan, paused, `next_run_at_s` |

Balance is read with `ft_balance_of(vault)` by the app; the contract doesn't track it. An `execute_buy` larger than the balance simply fails in the USDC contract and the callback restores state.
Expand All @@ -121,7 +123,12 @@ Balance is read with `ft_balance_of(vault)` by the app; the contract doesn't tra
- I7. A new vault is paused with no plan.

### 3.5 Events (NEP-297)
`vault_created`, `deposited`, `plan_updated`, `paused`, `resumed`, `buy_sent`, `buy_failed`, `withdrawn`, `token_recovered`.
Vault (`standard: stack_vault`): `deposited`, `plan_updated`, `paused`, `resumed`, `buy_sent`, `buy_failed`, `withdrawn`, `withdraw_failed`, `token_recovered`.
Factory (`standard: stack_factory`): `vault_code_updated`, `vault_created`, `vault_create_failed`, `vault_registration_failed`.

Plan limits enforced on-chain: `interval_s ≥ 60`, `0 < max_cost_bps ≤ 5000`, `amount_per_run > 0`, string fields ≤ 128 chars.

**Callback rule:** a callback awaiting a call that returns data (e.g. `storage_deposit` → StorageBalance) must read `env::promise_result_checked` instead of `#[callback_result] Result<(), _>`, which would panic on deserialization.

### 3.6 Known trust assumption (README)
The keeper chooses `deposit_address`. A stolen keeper key could redirect at most one `amount_per_run` per interval per vault until the owner pauses. Off-chain: the worker verifies the quote signature and that its recipient/asset match `get_state()` before calling `execute_buy`. Stretch: verify the signature in the contract.
Expand Down
7 changes: 4 additions & 3 deletions ARCHITECTURE_ESSENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Separate repo from **Settle** (track 08). Never import Settle code; README names
- **KD-5** No vault upgrades: vaults are immutable; owners withdraw and open a new vault instead
- **KD-6** Targets: BTC `nep141:btc.omft.near`, ETH `nep141:eth.omft.near`, NEAR `nep141:wrap.near`
- **KD-7** Name: Stack
- **KD-8** Regular deploy: factory embeds the vault wasm (~1.1 NEAR locked per vault). Never a global contract by account
- **KD-8** Regular deploy: admin uploads the vault wasm to the factory once (`set_vault_code`); ~1.5 NEAR locked per 137 KB vault. Never a global contract by account

## ⛔ Pending — don't build until the human decides
_None._
Expand All @@ -41,9 +41,10 @@ Rust near-sdk =5.30.0 + cargo-near 0.22 + near-workspaces =0.24.0 · Next.js 15.

## Contract rules (non-negotiable)
1. USDC leaves only via `withdraw` (to owner) or `execute_buy` (bounded).
2. `execute_buy`: keeper · not paused · plan set · once per `interval_s` · `amount ≤ amount_per_run` · register deposit address before `ft_transfer` · callback restores `last_run_at_s` on failure.
2. `execute_buy`: keeper · not paused · plan set · once per `interval_s` · `amount ≤ amount_per_run` · keeper attaches ~0.013 NEAR so the deposit address gets registered before `ft_transfer` · callback restores `last_run_at_s` on failure.
3. `set_plan`, `resume`, `withdraw`, `recover_token`: owner + 1 yocto. `pause`: owner, no deposit.
4. `create_vault_for`: keeper only · one per owner · daily cap.
4. `create_vault_for`: keeper only · one per owner · daily cap · vault code uploaded once by the admin (`set_vault_code`, future vaults only) · USDC registration chained after deploy.
6. Callbacks on calls that return data read `env::promise_result_checked`, never `Result<(), _>`.
5. No admin path to funds. Checked math. Events for every state change.

## 1Click rules
Expand Down
29 changes: 15 additions & 14 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
|---|---|---|---|
| D. Design & docs | Nov 4 | 100% | ✅ done |
| 0. Setup & verification | Nov 4–5 | 80% | in progress (K0.9 blocked on K0.8; K0.8, K0.14 👤) |
| 1. Contracts | Nov 5–7 | 0% | in progress (state drafted) |
| 1. Contracts | Nov 5–7 | 92% | in progress (K1.13 deploy is 👤) |
| 2. Core logic (`lib/core`) | Nov 6–7 | 0% | in progress (drafted) |
| 3. Integrations (1Click, NEAR, DB) | Nov 7–8 | 0% | in progress (schema drafted) |
| 4. API routes | Nov 8–9 | 0% | not started |
Expand All @@ -27,7 +27,7 @@
| 10. README & public repo | Nov 12–13 | 14% | in progress |
| 11. Launch: video, post, submission | Nov 13–14 | 0% | not started |
| 12. After submission | Nov 14–25 | 0% | not started |
| **Overall (MVP)** | | **~13%** | |
| **Overall (MVP)** | | **~20%** | |

Phase % = done tasks ÷ non-dropped tasks in that phase. Overall = average of all 14 rows (D + phases 0–12). (Earlier entries over-estimated the overall %; recalculated 2026-11-04.)

Expand Down Expand Up @@ -61,7 +61,7 @@ Format: `- [task id] what blocks it · who can unblock · since date`
| KD-5 [#2](https://github.com/MFrancis-dev/stack/issues/2) | Owner-approved vault upgrades in MVP? → **No.** Vaults are immutable; an owner can always withdraw and open a new vault | ✅ decided 2026-11-04 |
| KD-6 [#3](https://github.com/MFrancis-dev/stack/issues/3) | MVP target assets → **BTC (native) `nep141:btc.omft.near`, ETH (Ethereum) `nep141:eth.omft.near`, NEAR `nep141:wrap.near`** | ✅ decided 2026-11-04 |
| KD-7 [#4](https://github.com/MFrancis-dev/stack/issues/4) | Working name → **"Stack" is final** | ✅ decided 2026-11-04 |
| KD-8 [#106](https://github.com/MFrancis-dev/stack/issues/106) | Vault deploy → **regular deploy** (factory embeds the vault wasm; ~1.1 NEAR locked per vault, recoverable). Never a global contract by account | ✅ decided 2026-11-04 |
| KD-8 [#106](https://github.com/MFrancis-dev/stack/issues/106) | Vault deploy → **regular deploy** (admin uploads the vault wasm to the factory once; ~1.5 NEAR locked per 137 KB vault, recoverable). Never a global contract by account | ✅ decided 2026-11-04 |

---

Expand All @@ -85,18 +85,18 @@ Format: `- [task id] what blocks it · who can unblock · since date`

## Phase 1 — Contracts (Nov 5–7) — **critical path**

- [~] **K1.1** [#20](https://github.com/MFrancis-dev/stack/issues/20) (2026-11-04: `Vault` + `VaultView` structs and `new` written; `get_state` stubbed) Vault state + `new` + `get_state` (paused, no plan)
- [ ] **K1.2** [#21](https://github.com/MFrancis-dev/stack/issues/21) `set_plan` (1 yocto, range checks), `resume` (1 yocto, plan required), `pause` (owner)
- [ ] **K1.3** [#22](https://github.com/MFrancis-dev/stack/issues/22) `ft_on_transfer` (accept any NEP-141, event)
- [ ] **K1.4** [#23](https://github.com/MFrancis-dev/stack/issues/23) `execute_buy`: keeper, bounds, deposit-address storage registration, `ft_transfer`, failure callback restoring `last_run_at_s`
- [ ] **K1.5** [#24](https://github.com/MFrancis-dev/stack/issues/24) `withdraw`, `recover_token` (owner, 1 yocto)
- [ ] **K1.6** [#25](https://github.com/MFrancis-dev/stack/issues/25) NEP-297 events for every state change
- [~] **K1.7** [#26](https://github.com/MFrancis-dev/stack/issues/26) (2026-11-04: `Factory` state, `new`, `get_vault` written; `create_vault_for` stubbed) Factory: `create_vault_for` (keeper, one per owner, daily cap), deploy (global or embedded), USDC storage for the vault, `get_vault` · 2026-11-04: USDC storage registration for the vault is **mandatory**: Intents gives no refund to an unregistered receiver (§12). Deploy method per KD-8
- [ ] **K1.8** [#27](https://github.com/MFrancis-dev/stack/issues/27) near-workspaces setup with mock USDC
- [ ] **K1.9** [#28](https://github.com/MFrancis-dev/stack/issues/28) Tests: I1–I7 one each
- [ ] **K1.10** [#29](https://github.com/MFrancis-dev/stack/issues/29) Tests: failed transfer callback, insufficient balance, stray token recovery, daily cap, unauthorized callers
- [x] **K1.1** [#20](https://github.com/MFrancis-dev/stack/issues/20) (2026-11-04: `Vault` + `VaultView` structs and `new` written; `get_state` stubbed) Vault state + `new` + `get_state` (paused, no plan) · 2026-11-04: state, `new`, `get_state` (next_run_at_s) + unit tests
- [x] **K1.2** [#21](https://github.com/MFrancis-dev/stack/issues/21) `set_plan` (1 yocto, range checks), `resume` (1 yocto, plan required), `pause` (owner) · 2026-11-04: range checks (interval ≥ 60 s, 0 < cost ≤ 5000 bps), set_plan never unpauses
- [x] **K1.3** [#22](https://github.com/MFrancis-dev/stack/issues/22) `ft_on_transfer` (accept any NEP-141, event) · 2026-11-04: accepts any NEP-141, emits `deposited`
- [x] **K1.4** [#23](https://github.com/MFrancis-dev/stack/issues/23) `execute_buy`: keeper, bounds, deposit-address storage registration, `ft_transfer`, failure callback restoring `last_run_at_s` · 2026-11-04: bounds, optional storage_deposit for the deposit address, `on_buy_transfer` restores schedule on failure
- [x] **K1.5** [#24](https://github.com/MFrancis-dev/stack/issues/24) `withdraw`, `recover_token` (owner, 1 yocto) · 2026-11-04: `withdraw` (always available), `recover_token` (refuses USDC), both with result events
- [x] **K1.6** [#25](https://github.com/MFrancis-dev/stack/issues/25) NEP-297 events for every state change · 2026-11-04: NEP-297 `stack_vault` + `stack_factory` events
- [x] **K1.7** [#26](https://github.com/MFrancis-dev/stack/issues/26) (2026-11-04: `Factory` state, `new`, `get_vault` written; `create_vault_for` stubbed) Factory: `create_vault_for` (keeper, one per owner, daily cap), deploy (global or embedded), USDC storage for the vault, `get_vault` · 2026-11-04: USDC storage registration for the vault is **mandatory**: Intents gives no refund to an unregistered receiver (§12). Deploy method per KD-8 · 2026-11-04: admin uploads code once (`set_vault_code`, future vaults only), keeper `create_vault_for`, rollback on failed deploy, chained USDC registration + `retry_registration`
- [x] **K1.8** [#27](https://github.com/MFrancis-dev/stack/issues/27) near-workspaces setup with mock USDC · 2026-11-04: near-workspaces sandbox + `compile_project` + `mock-ft` crate
- [x] **K1.9** [#28](https://github.com/MFrancis-dev/stack/issues/28) Tests: I1–I7 one each · 2026-11-04: I1–I7 + private callbacks, all passing on the sandbox
- [x] **K1.10** [#29](https://github.com/MFrancis-dev/stack/issues/29) Tests: failed transfer callback, insufficient balance, stray token recovery, daily cap, unauthorized callers · 2026-11-04: deposit-address registration, insufficient balance, factory rules, recovery, withdraw, non-owner — 49 contract tests total (24 vault unit, 9 factory unit, 16 sandbox)
- [-] **K1.11** [#30](https://github.com/MFrancis-dev/stack/issues/30) If KD-5 = yes: owner-approved `upgrade_to(hash)` + tests · 2026-11-04: dropped: KD-5 = no upgrades in MVP
- [ ] **K1.12** [#31](https://github.com/MFrancis-dev/stack/issues/31) `contracts/README.md`: methods, build, test
- [x] **K1.12** [#31](https://github.com/MFrancis-dev/stack/issues/31) `contracts/README.md`: methods, build, test · 2026-11-04: `contracts/README.md` build/test/deploy order; method tables in ARCHITECTURE §3
- [ ] **K1.13** [#32](https://github.com/MFrancis-dev/stack/issues/32) 👤 Approve deployment; deploy factory (+ global vault code if used); record account ids

## Phase 2 — Core logic `lib/core` (Nov 6–7)
Expand Down Expand Up @@ -214,6 +214,7 @@ Format: `- [task id] what blocks it · who can unblock · since date`
## Update log
One line per contribution, newest first: `YYYY-MM-DD · who/agent · what changed · tasks`

- 2026-11-04 · agent · Phase 1 contracts: vault + factory implemented, mock-ft crate, 49 contract tests passing (unit + near-workspaces sandbox); wasm vault 137 KB / factory 146 KB · K1.1–K1.10, K1.12
- 2026-11-04 · agent · Decisions accepted by the human: KD-5 no upgrades, KD-6 BTC/ETH/NEAR, KD-7 name Stack, KD-8 regular deploy; K1.11 dropped; update-log actor made neutral · K0.1, K0.10, K1.11
- 2026-11-04 · agent · CI green on main (web, contracts, gitleaks) · K0.7
- 2026-11-04 · agent · Phase 0 verification: live `/v0/tokens` + dry quotes (cost data), SDK status enum, NEAR docs (global contracts, near-connect); added near-connect + near-api-js; db client + check-env + test; react-server condition for worker/scripts; CI workflow; ARCHITECTURE §12 + ESSENTIALS updated; new decision KD-8 · K0.6, K0.7, K0.9, K0.10, K0.11, K0.12, K0.13, K1.7, K4.5, K6.3, K8.2
Expand Down
Loading
Loading