From 20b851ed9319ddbce0723b6344bb3c58b0301068 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:05:45 +0000 Subject: [PATCH 1/7] Initial plan From fa2b43d6bf7c024c9b4c1775ed33b5a7fe1dd81a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:06:41 +0000 Subject: [PATCH 2/7] chore: plan mp bridge history fix --- .agents/skills/gooddollar/CONTRIBUTING.md | 121 ++ .agents/skills/gooddollar/SKILL.md | 215 ++++ .../contracts/BuyGDCloneFactory.abi.yaml | 205 ++++ .../BuyGDCloneFactory.selectors.yaml | 27 + .../contracts/BuyGDCloneV2.abi.yaml | 282 +++++ .../contracts/BuyGDCloneV2.selectors.yaml | 36 + .../contracts/CFAv1Forwarder.abi.yaml | 237 ++++ .../contracts/CFAv1Forwarder.selectors.yaml | 19 + .../ConstantFlowAgreementV1.abi.yaml | 560 +++++++++ .../ConstantFlowAgreementV1.selectors.yaml | 33 + .../contracts/GoodDollarOFTAdapter.abi.yaml | 100 ++ .../GoodDollarOFTAdapter.selectors.yaml | 12 + .../GooddollarSavingsStream.abi.yaml | 210 ++++ .../GooddollarSavingsStream.selectors.yaml | 27 + .../contracts/GovernanceStakingV2.abi.yaml | 202 ++++ .../GovernanceStakingV2.selectors.yaml | 26 + .../references/contracts/IdentityV3.abi.yaml | 390 +++++++ .../contracts/IdentityV3.selectors.yaml | 49 + .../references/contracts/IdentityV4.abi.yaml | 424 +++++++ .../contracts/IdentityV4.selectors.yaml | 51 + .../references/contracts/InvitesV2.abi.yaml | 333 ++++++ .../contracts/InvitesV2.selectors.yaml | 41 + .../references/contracts/MentoBroker.abi.yaml | 259 +++++ .../contracts/MentoBroker.selectors.yaml | 24 + .../contracts/MessagePassingBridge.abi.yaml | 542 +++++++++ .../MessagePassingBridge.selectors.yaml | 52 + .../references/contracts/NameService.abi.yaml | 104 ++ .../contracts/NameService.selectors.yaml | 11 + .../references/contracts/SuperToken.abi.yaml | 1033 +++++++++++++++++ .../contracts/SuperToken.selectors.yaml | 99 ++ .../references/contracts/Superfluid.abi.yaml | 757 ++++++++++++ .../contracts/Superfluid.selectors.yaml | 51 + .../references/contracts/UBISchemeV2.abi.yaml | 305 +++++ .../contracts/UBISchemeV2.selectors.yaml | 40 + .../contracts/_rich-abi-yaml-format.md | 93 ++ .../deep-researches/faucet-flows.md | 42 + .../fuse-to-celo-staking-migration.md | 38 + .../gooddao-daostack-surface.md | 37 + .../deep-researches/how-ubi-is-minted.md | 68 ++ .../inviter-invitee-reward-model.md | 58 + .../mento-reserve-economics.md | 27 + .../deep-researches/on-off-ramp-service.md | 69 ++ .../gooddollar/references/guides/bridge.md | 187 +++ .../references/guides/check-identity.md | 87 ++ .../gooddollar/references/guides/claim.md | 83 ++ .../gooddollar/references/guides/faucet.md | 76 ++ .../gooddollar/references/guides/gooddocs.md | 37 + .../gooddollar/references/guides/goodsdks.md | 110 ++ .../references/guides/hypersync-hyperrpc.md | 110 ++ .../references/guides/invite-bounties.md | 115 ++ .../migrate-fuse-staking-to-celo-savings.md | 116 ++ .../references/guides/on-off-ramp.md | 77 ++ .../gooddollar/references/guides/save.md | 93 ++ .../gooddollar/references/guides/stream.md | 135 +++ .../gooddollar/references/guides/swap.md | 83 ++ .../references/subgraphs/_query-patterns.md | 35 + .../subgraphs/goodcollective-guide.md | 54 + .../subgraphs/goodcollective.graphql | 200 ++++ .../subgraphs/gooddollar-celo-guide.md | 104 ++ .../subgraphs/gooddollar-celo.graphql | 113 ++ .../subgraphs/reserve-celo-guide.md | 41 + .../references/subgraphs/reserve-celo.graphql | 90 ++ skills-lock.json | 11 + 63 files changed, 9266 insertions(+) create mode 100644 .agents/skills/gooddollar/CONTRIBUTING.md create mode 100644 .agents/skills/gooddollar/SKILL.md create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/NameService.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/NameService.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml create mode 100644 .agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/faucet-flows.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md create mode 100644 .agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md create mode 100644 .agents/skills/gooddollar/references/guides/bridge.md create mode 100644 .agents/skills/gooddollar/references/guides/check-identity.md create mode 100644 .agents/skills/gooddollar/references/guides/claim.md create mode 100644 .agents/skills/gooddollar/references/guides/faucet.md create mode 100644 .agents/skills/gooddollar/references/guides/gooddocs.md create mode 100644 .agents/skills/gooddollar/references/guides/goodsdks.md create mode 100644 .agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md create mode 100644 .agents/skills/gooddollar/references/guides/invite-bounties.md create mode 100644 .agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md create mode 100644 .agents/skills/gooddollar/references/guides/on-off-ramp.md create mode 100644 .agents/skills/gooddollar/references/guides/save.md create mode 100644 .agents/skills/gooddollar/references/guides/stream.md create mode 100644 .agents/skills/gooddollar/references/guides/swap.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/_query-patterns.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective.graphql create mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql create mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md create mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql create mode 100644 skills-lock.json diff --git a/.agents/skills/gooddollar/CONTRIBUTING.md b/.agents/skills/gooddollar/CONTRIBUTING.md new file mode 100644 index 00000000..ab855d38 --- /dev/null +++ b/.agents/skills/gooddollar/CONTRIBUTING.md @@ -0,0 +1,121 @@ +# Contributing to GoodSkills + +This repository is an AI skill pack. The goal of each update is to make agent behavior more reliable, more explicit, and easier to audit. + +## Update workflow + +1. Define the user-facing problem first. +2. For contract-related updates, add or update Rich ABI first (`references/contracts/*.abi.yaml`), then refresh selectors. +3. Decide the remaining artifact types: + - `references/guides/` for "what to do" + - `references/deep-researches/` for "why it works this way" + - `scripts/` for deterministic and repeatable execution +4. Update `SKILL.md` routing so the new artifact is discoverable. +5. Validate consistency (paths, naming, links, selectors, assumptions). + +If a change touches contract behavior, treat Rich ABI update/add as mandatory first step before guides, deep-research, or scripts. + +## Add or update a guide + +Use guides for execution playbooks and operator workflows. + +Required structure: + +- title and one-line usage trigger +- `## Goal` +- `## Required inputs` +- `## Execution flow` as numbered steps +- deterministic snippet when execution is non-trivial +- failure handling and output contract + +Guide rules: + +- prefer explicit pre-checks before state-changing actions +- include only one primary workflow per file +- use [GoodProtocol `releases/deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) as the **only** source for contract addresses (rich ABI `meta.deployments` mirrors those rows); use GoodDocs for behavior and UX, not for resolving addresses; use on-chain `NameService.getAddress` only when the deployment documents the string key +- avoid implementation-deep theory; keep that in deep-research files + +After adding a guide: + +- add it to `SKILL.md` in `Guides` +- add an entry in `Use-case to guide map` + +## Add or update a deep-research note + +Use deep-research files for architecture, rationale, tradeoffs, and root-cause logic. + +Deep-research rules: + +- explain causality, not only API surfaces +- distinguish current behavior from legacy behavior +- link source contracts/docs for traceability +- keep language natural and decision-oriented + +Do not turn deep-research files into step-by-step runbooks; move operational steps into guides. + +## Add or update Rich ABI YAML + +Location: `references/contracts/`. + +For each contract: + +- create or update `Foo.abi.yaml` +- generate or refresh `Foo.selectors.yaml` +- include function-level notes for non-obvious behavior +- when `meta.deployments` lists concrete addresses, add **`creationBlock`** next to each **`address`** (placement: `references/contracts/_rich-abi-yaml-format.md`; using it as **`fromBlock`** for log or HyperSync fetches: `references/guides/hypersync-hyperrpc.md`) + +Minimum ABI documentation quality: + +- correct mutability, inputs, outputs +- access pattern (`owner`, `avatar`, `anyone`, etc.) where relevant +- emitted events and practical errors +- notes for routing/edge-case semantics + +Source-of-truth policy: + +- prefer canonical contract repos (GoodProtocol, GoodBridge, mento-core) +- avoid inferred behavior when source is unclear +- update notes when protocol behavior changed + +Selector generation: + +```bash +node scripts/selectors.mjs generate Foo.abi.yaml +``` + +## Add or update scripts + +Location: `scripts/`. + +Use scripts when: + +- a workflow is repeated +- deterministic output is needed +- manual querying is error-prone + +Script standards: + +- require inputs through env vars or explicit args +- fail loudly with actionable messages +- print structured output for easy reuse +- keep script intent narrow + +When a script supports a guide: + +- reference it from that guide +- document expected inputs and outputs in the guide + +## Naming and organization + +- use lowercase kebab-case for guides and deep-research files +- keep one topic per file +- avoid duplicate guidance across files +- prefer updating existing files over creating near-duplicates + +## Update checklist before merge + +- `SKILL.md` routing updated +- links resolve and point to public sources +- guides and deep-research files respect "what" vs "why" separation +- ABI + selectors pairs are in sync +- new behavior is reflected in notes where needed diff --git a/.agents/skills/gooddollar/SKILL.md b/.agents/skills/gooddollar/SKILL.md new file mode 100644 index 00000000..2d59116b --- /dev/null +++ b/.agents/skills/gooddollar/SKILL.md @@ -0,0 +1,215 @@ +--- +name: gooddollar +description: > + Knowledge base for GoodProtocol action execution and GoodDollar (G$) integrations. + Use this skill BEFORE ad-hoc web search for claim, save/stake, swap, bridge, + stream, and identity tasks. Prefer GoodDocs (https://docs.gooddollar.org/) for + narrative; contract addresses only from GoodProtocol releases/deployment.json. +metadata: + version: 1.0.0 +license: MIT +--- + +# GoodDollar Skill Pack + +Routing index for GoodProtocol. This repo complements [GoodDocs](https://docs.gooddollar.org/) for behavior and user flows. **Contract addresses** come only from [GoodProtocol/releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (and `meta.deployments` in `references/contracts/*.abi.yaml`, which mirror those rows)—not from GoodDocs pages. + +Repository maintenance and update process is documented in `CONTRIBUTING.md`. + +## Protocol snapshot (from GoodDocs) + +- G$ is reserve-backed; issuance and pricing tie to the reserve and bonding-curve mechanics described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). +- The stack is multi-chain; which contracts exist per environment is defined only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `GoodDollar`, `Identity`, `NameService`, `UBIScheme`, Mento keys, `MpbBridge`, and related entries under `production`, `production-celo`, and `production-xdc`). +- UBI is daily for verified users; identity verification and connected accounts are documented under [user guides](https://docs.gooddollar.org/user-guides). + +## Guides (single location for action playbooks) + +All task-specific instructions live under `references/guides/`. + +- `references/guides/claim.md` — daily UBI (`claim` / UBIScheme). +- `references/guides/save.md` — stake, rewards, unstake. +- `references/guides/swap.md` — buy or sell G$ (Mento on supported chains). +- `references/guides/bridge.md` — MessagePassingBridge (GoodDocs); optional OFT path via ABI refs. +- `references/guides/stream.md` — Superfluid streams (Celo-oriented in GoodDocs). +- `references/guides/check-identity.md` — whitelist and connected-address semantics. +- `references/guides/goodsdks.md` — SDK-first integration routing for GoodSDKs packages. +- `references/guides/gooddocs.md` — hub links to [GoodDocs](https://docs.gooddollar.org/). +- `references/guides/hypersync-hyperrpc.md` — Envio HyperSync/HyperRPC data-source routing for high-volume historical reads. +- `references/guides/faucet.md` — Faucet gas top-up execution flow and preflight checks. +- `references/guides/on-off-ramp.md` — stable-token ramp service flow into and out of G$. +- `references/guides/invite-bounties.md` — verify and execute inviter-invitee bounty payouts. +- `references/guides/migrate-fuse-staking-to-celo-savings.md` — migrate Fuse governance stake to CELO savings flow. + +## Subgraphs (indexed chain history) + +Use this folder with the same pattern as the protocol subgraph references: one `*-guide.md` plus one companion `.graphql` per deployment. + +For historical on-chain data, **start with the subgraph**: confirm the deployment covers the question (entities and fields in the guide, freshness via `_meta`). If the subgraph does not work for the request—missing schema coverage, stale or lagging indexing, query limits, or endpoint errors—**then** move to **HyperSync** or **HyperRPC** using `references/guides/hypersync-hyperrpc.md`. + +- `references/subgraphs/_query-patterns.md` — cross-cutting query discipline. +- `references/subgraphs/reserve-celo-guide.md` + `references/subgraphs/reserve-celo.graphql` — reserve pricing and swap history. +- `references/subgraphs/gooddollar-celo-guide.md` + `references/subgraphs/gooddollar-celo.graphql` — GoodDollar Celo schema discovery and starter probes. +- `references/subgraphs/goodcollective-guide.md` + `references/subgraphs/goodcollective.graphql` — GoodCollective schema discovery and starter probes. + +For Superfluid protocol subgraphs (streams, pools, vesting schedulers), see [Superfluid documentation](https://docs.superfluid.finance/) and [subgraph endpoints](https://subgraph-endpoints.superfluid.dev/). + +## Historical data routing policy (strict) + +1. Query subgraphs first for all historical/indexed requests. +2. Validate required entities and fields against the target subgraph schema and guide before declaring a gap. +3. Use **HyperSync** or **HyperRPC** fallback only when at least one of these is true: + - required entities or fields are not available in subgraph schema + - indexing lag makes subgraph data stale for the requested range + - query limits or endpoint instability block reliable retrieval +4. Do not start with HyperSync or HyperRPC when subgraph data is available and fresh. +5. HyperRPC fallback requires a valid Envio API key; if missing, **explicitly ask the user** to provide `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or paste a full `HYPERRPC_URL`); do not treat anonymous HyperRPC as production. +6. When **HyperSync** is the best option for the query and no Envio API token is available (`ENVIO_API_TOKEN` or equivalent per `references/guides/hypersync-hyperrpc.md`), **explicitly ask the user** to provide the token before proceeding; do not silently substitute anonymous HyperSync usage. +7. When fallback is used, report reason explicitly (schema gap, lag, or reliability issue). + +## Data source decision table + +| Query type | Primary source | Secondary source | Notes | +|---|---|---|---| +| Current on-chain state (latest balances, allowances, config, flags, view calls) | RPC | None | Use direct contract RPC reads for latest state. | +| Historical indexed entity data (time-series, aggregates, protocol entities, event-derived analytics) | Subgraph | HyperSync/HyperRPC | Prefer subgraph first; fall back when it cannot answer. | +| Historical raw on-chain data when subgraph is missing fields/entities or stale | HyperSync | HyperRPC | Prefer HyperSync for bulk scans and data pipelines. | +| Historical data for existing JSON-RPC integrations | HyperRPC | HyperSync | Use HyperRPC when strict JSON-RPC compatibility is required. | + +Decision rule: + +1. If request is current state -> use RPC. +2. If request is historical/indexed -> query subgraph first. +3. If subgraph cannot satisfy request -> fallback to HyperSync or HyperRPC per compatibility and scale needs. +4. HyperRPC fallback requires Envio API key credentials. +5. HyperSync client usage requires an Envio API token; if HyperSync is chosen and the token is missing, explicitly ask the user to provide it (see `references/guides/hypersync-hyperrpc.md`). + +## Mapping data retrieval rule + +Solidity mappings are not iterable on-chain by keyspace scan. Do not assume full-key enumeration is possible from RPC alone. + +When data is stored in mapping-like structures: + +1. Check contract source and ABI for key-discovery paths first: + - events emitted on set or update + - arrays, counters, linked lists, or index getters storing keys + - dedicated pagination or enumerable view functions +2. If key discovery exists, reconstruct key set from those sources and then read mapping entries. +3. If key discovery does not exist, report that complete iteration is not possible from chain state alone. +4. For historical reconstruction, prefer subgraph indexing first; if unavailable, use HyperSync or HyperRPC log scans with explicit limitations. + +## Use-case to guide map + +- Claim requests -> `references/guides/claim.md` +- Eligibility or connected-address questions -> `references/guides/check-identity.md` +- Stake, save, unstake -> `references/guides/save.md` +- Buy or sell G$ against reserve rails -> `references/guides/swap.md` +- Cross-chain bridge -> `references/guides/bridge.md` +- Stream management -> `references/guides/stream.md` +- SDK app integration tasks -> `references/guides/goodsdks.md` +- Bulk historical reads or data-engineering fetches -> `references/guides/hypersync-hyperrpc.md` +- Faucet top-up tasks -> `references/guides/faucet.md` +- On-/off-ramp service flow tasks -> `references/guides/on-off-ramp.md` +- Invite bounty eligibility and payout tasks -> `references/guides/invite-bounties.md` +- Fuse to CELO staking migration tasks -> `references/guides/migrate-fuse-staking-to-celo-savings.md` +- Indexed history, analytics, or GraphQL against GoodDollar subgraphs -> `references/subgraphs/_query-patterns.md` +- Historical on-chain fetch when subgraph data is insufficient -> subgraphs first, then HyperSync or HyperRPC per `references/guides/hypersync-hyperrpc.md`; if HyperSync is best and `ENVIO_API_TOKEN` is missing, ask the user for it explicitly. + +## Ambiguous prompts and incomplete inputs + +Stop and **ask the user** whenever the task is underspecified or required facts are missing. List what you need in short, concrete questions (for example chain, contract, address, amount, account, RPC or signer access, time or block range, prior tx hash, approval scope). + +- **Ambiguous** means the goal, environment, contract surface, or acceptance criteria are not clear enough to choose a safe path. +- **Incomplete** means you lack inputs that would change what you build, call, or sign next. + +**Do not invent** chain, address, amount, or policy details that affect correctness, funds, or eligibility. For **information-only** work you may state a single explicit assumption, label it, and ask the user to confirm or correct it before going further. + +**Execution work** (writing or editing runnable code, sending transactions, migrations, or anything that can move funds or alter on-chain state) has **no guessing**: settle every required input with the user, then implement or run. + +## Execution rules + +1. Collect missing required inputs before sending transactions. +2. Run pre-checks first (allowance, whitelist, quotes, bridge **amount** limits, peer wiring when using OFT paths). +3. If a pre-check fails, stop and return the exact corrective action. +4. Return tx hash and key output values. +5. Never fabricate addresses, amounts, or ABI behavior. +6. Resolve decimals and units per chain as in [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (for example 18 decimals on Celo, 2 on Fuse and Ethereum where applicable). + +## Pre-check matrix + +- Claim: verify identity whitelist status before `claim()`. +- Save or stake: verify balance and allowance before `stake()`. +- Swap: fetch quote, apply slippage bounds, verify allowance; confirm Mento contract keys for the active chain exist in `deployment.json` (for example `MentoBroker` under `production-celo` or `production-xdc`). +- Bridge (MessagePassingBridge): on the **source** chain approve G$ to the bridge; optionally preflight `canBridge(from, amount)` on that same contract (outbound `_bridgeTo` does not call it internally). For LZ use `estimateSendFee` with the **normalized** burn amount per `references/guides/bridge.md`, then `bridgeToWithLz` with nonzero `msg.value` for the **cross-chain transport** fee only (distinct from destination **`bridgeFees`** on minted G$; see **Bridge fee context** in that guide). Read **`bridgeLimits`** / daily trackers when debugging **amount** caps; see **Bridge amount limit context** in that guide. Respect `isClosed`, `LZ_FEE`, `MISSING_FEE`, and `UNSUPPORTED_CHAIN`. **Destination** mint applies `_enforceLimits` and can still revert. Use **Axelar** only when `toAxelarChainId` returns a route (implementation maps 1, 5, 42220, 44787); for Fuse or XDC style targets prefer LZ unless mapping is extended on-chain. +- Bridge (OFT adapter path): verify peer wiring and `quoteSend` fee data. +- Stream: confirm Celo (or documented Superfluid network) and correct Super Token and forwarder or host addresses. +- Identity: resolve Identity from NameService; remember connected addresses do not multiply daily claims ([connect wallet guide](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity)). + +## Output format requirements + +For any state-changing action return: + +- network and key contract addresses used +- normalized input amounts and min or max guards +- tx hash +- key post-state output when available +- follow-up action if user intervention is required + +## Rich contract ABI references + +Convention: each `Foo.abi.yaml` has a companion `Foo.selectors.yaml` (function, event, and custom error selectors). Schema: `references/contracts/_rich-abi-yaml-format.md`. + +GoodDollar / Mento: + +- `references/contracts/NameService.abi.yaml` +- `references/contracts/IdentityV3.abi.yaml` +- `references/contracts/IdentityV4.abi.yaml` +- `references/contracts/InvitesV2.abi.yaml` +- `references/contracts/BuyGDCloneFactory.abi.yaml` +- `references/contracts/BuyGDCloneV2.abi.yaml` +- `references/contracts/GovernanceStakingV2.abi.yaml` +- `references/contracts/GooddollarSavingsStream.abi.yaml` (Ubeswap Superfluid stream savings; Celo deployment) +- `references/contracts/UBISchemeV2.abi.yaml` +- `references/contracts/MentoBroker.abi.yaml` +- `references/contracts/MessagePassingBridge.abi.yaml` +- `references/contracts/GoodDollarOFTAdapter.abi.yaml` +- `references/contracts/CFAv1Forwarder.abi.yaml` +- `references/contracts/ConstantFlowAgreementV1.abi.yaml` +- `references/contracts/Superfluid.abi.yaml` +- `references/contracts/SuperToken.abi.yaml` + +Superfluid (CFA, CFAv1Forwarder, Host, full ABI library): use [Superfluid docs](https://docs.superfluid.finance/), npm packages such as `@superfluid-finance/ethereum-contracts` and `@sfpro/sdk`, and contract ABIs published with those packages. + +## Deep researches + +- `references/deep-researches/on-off-ramp-service.md` +- `references/deep-researches/how-ubi-is-minted.md` +- `references/deep-researches/inviter-invitee-reward-model.md` +- `references/deep-researches/mento-reserve-economics.md` +- `references/deep-researches/gooddao-daostack-surface.md` +- `references/deep-researches/faucet-flows.md` +- `references/deep-researches/fuse-to-celo-staking-migration.md` + +## Revert debugging quick map + +- Identity or eligibility errors -> Identity and UBIScheme ABIs; live addresses from `deployment.json` only; GoodDocs for whitelist and claim behavior. +- Approval or transfer failures -> token approvals and balances; see integration guide for `transferAndCall` vs `approve` plus `transferFrom`. +- Swap bound failures -> quote freshness and slippage settings. +- MessagePassingBridge failures -> `canBridge`; **`BRIDGE_LIMITS`** (amount caps, whitelist, **`closed`**, and related policy strings); transport `msg.value` (`MISSING_FEE`, `LZ_FEE`) vs destination protocol fee (`bridgeFees`, `feeRecipient`); correct `bridgeTo` arguments; [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). +- OFT path failures -> peer wiring and `quoteSend` fee data. +- Stream failures -> CFA forwarder or host agreement calls, buffer and flow-rate limits per Superfluid docs linked from GoodDocs. +- Faucet top-up failures -> `canTop`, `onlyAuthorized`, daily or weekly caps; `references/deep-researches/faucet-flows.md`. +- DAO-gated reverts -> caller is not avatar; scheme not registered; `references/deep-researches/gooddao-daostack-surface.md`. + +## Library usage discipline + +1. Open `references/guides/gooddocs.md` when unsure which GoodDocs page applies. +2. Start at this file to classify intent. +3. Open one guide under `references/guides/` unless the user requests a multi-step workflow. For subgraph or indexed-data tasks, start at `references/subgraphs/_query-patterns.md`. +4. Read only the ABI references and matching `.selectors.yaml` files needed for the chosen action. +5. Prefer GoodDocs for documented behavior; use only `deployment.json` (and rich ABI `meta.deployments` aligned with it) for contract addresses—never infer addresses from GoodDocs. +6. For large historical reads, prefer `references/guides/hypersync-hyperrpc.md` and choose HyperSync over HyperRPC unless strict JSON-RPC compatibility is required. +7. Historical data routing is strict: subgraphs first; HyperSync or HyperRPC only with an explicit fallback reason. +8. HyperRPC usage requires Envio API key credentials; when absent, **explicitly ask the user** for `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or a full `HYPERRPC_URL`) and do not attempt anonymous production flow. +9. When HyperSync is the best historical-data path and no Envio API token is available, explicitly ask the user to provide `ENVIO_API_TOKEN` (or the token your client expects) before continuing; see `references/guides/hypersync-hyperrpc.md`. +10. For subgraph tasks, validate field availability from the relevant `references/subgraphs/*-guide.md` and companion `.graphql` before guessing alternate entities. +11. For local shells repeating HyperRPC log pulls (for example last N whitelist events), from the **GoodSkills repository root** run `scripts/fetch-whitelist-events-hyperrpc.mjs` per `references/guides/hypersync-hyperrpc.md` instead of re-deriving JSON-RPC setup each time; that script ships with **defaults for production Celo** (HyperRPC host + `Identity` contract from `deployment.json`) and URL composition from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` unless you override `CONTRACT_ADDRESS` / `HYPERRPC_URL`. HyperSync remains a separate client install path documented in the same guide. diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml new file mode 100644 index 00000000..40226b3b --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml @@ -0,0 +1,205 @@ +meta: + name: BuyGDCloneFactory + version: "1" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol + note: > + Deterministic clone factory for BuyGDCloneV2 and DonateGDClone. Used to create per-owner + executors and optional create+swap flows for ramp services. + deployments: + mainnet: + production-celo: + BuyGDFactoryV2: + networkId: 42220 + address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" + creationBlock: 22909812 + BuyGDFactory: + networkId: 42220 + address: "0x00e533B7d6255D05b7f15034B1c989c21F51b91C" + creationBlock: 21006324 + related: + - references/deep-researches/on-off-ramp-service.md + - references/guides/on-off-ramp.md + +create: + notes: + - "Deploys deterministic BuyGDCloneV2 clone for owner and initializes it." + - "Difference vs createAndSwap: this only deploys; no swap is executed." + mutability: nonpayable + access: anyone + inputs: + - owner: address + outputs: + - clone: address + +createDonation: + notes: + - "Deploys deterministic DonateGDClone with donation target and call payload." + - "Difference vs create: deploys donation-capable implementation instead of plain swap clone." + mutability: nonpayable + access: anyone + inputs: + - owner: address + - donateOrExecTo: address + - callData: bytes + outputs: + - clone: address + +createAndSwap: + notes: + - "One-transaction helper: create BuyGDCloneV2 then immediately call clone.swap(minAmount, msg.sender)." + - "Difference vs create: bundles deployment and first swap for faster ramp UX." + mutability: nonpayable + access: anyone + inputs: + - owner: address + - minAmount: uint256 + outputs: + - clone: address + +createDonationAndSwap: + notes: + - "One-transaction helper for DonateGDClone: deploy, then execute donation flow with optional pre-swap." + - "Difference vs createDonation: can execute immediate swap and/or target call instead of deployment only." + mutability: nonpayable + access: anyone + inputs: + - owner: address + - donateOrExecTo: address + - withSwap: bool + - minAmount: uint256 + - callData: bytes + outputs: + - clone: address + +predict: + notes: + - "Computes deterministic address for create(owner) salt." + mutability: view + inputs: + - owner: address + outputs: + - clone: address + +predictDonation: + notes: + - "Computes deterministic address for createDonation(owner, donateOrExecTo, callData) salt." + mutability: view + inputs: + - owner: address + - donateOrExecTo: address + - callData: bytes + outputs: + - clone: address + +getBaseFee: + mutability: view + inputs: [] + outputs: + - baseFee: uint256 + +quoter: + mutability: pure + inputs: [] + outputs: + - addr: address + +CUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +celo: + mutability: pure + inputs: [] + outputs: + - token: address + +USDC: + mutability: pure + inputs: [] + outputs: + - token: address + +GLOUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +PERIOD: + mutability: pure + inputs: [] + outputs: + - seconds: uint24 + +impl: + mutability: view + inputs: [] + outputs: + - addr: address + +donateImpl: + mutability: view + inputs: [] + outputs: + - addr: address + +gd: + mutability: view + inputs: [] + outputs: + - token: address + +stable: + mutability: view + inputs: [] + outputs: + - token: address + +oracle: + mutability: view + inputs: [] + outputs: + - addr: address + +router: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoBroker: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeProvider: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeId: + mutability: view + inputs: [] + outputs: + - id: bytes32 + +events: + GDSwapToCusd: + indexed: [] + data: + - from: address + - to: address + - amountIn: uint256 + - amountOut: uint256 + - note: bytes + +errors: + NOT_GD_TOKEN: "onTokenTransfer caller is not G$ token." + INVALID_TWAP: "TWAP validation failed." + RECIPIENT_ZERO: "Recipient cannot be zero address." + ZERO_MINAMOUNT: "Minimum amount cannot be zero." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml new file mode 100644 index 00000000..16b13f22 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml @@ -0,0 +1,27 @@ +# Generated by scripts/selectors.mjs +functions: + create(address): 0x9ed93318 + createDonation(address,address,bytes): 0x48199e6b + createAndSwap(address,uint256): 0x89643a29 + createDonationAndSwap(address,address,bool,uint256,bytes): 0xeb5621b8 + predict(address): 0x901b96e7 + predictDonation(address,address,bytes): 0x3895de30 + getBaseFee(): 0x15e812ad + quoter(): 0xc6bbd5a7 + CUSD(): 0x758316c9 + celo(): 0x051ed8ef + USDC(): 0x89a30271 + GLOUSD(): 0x4b5b02d6 + PERIOD(): 0xb4d1d795 + impl(): 0x8abf6077 + donateImpl(): 0x21fc2eef + gd(): 0xa5e598fc + stable(): 0x22be3de1 + oracle(): 0x7dc0d1d0 + router(): 0xf887ea40 + mentoBroker(): 0x7b89f117 + mentoExchangeProvider(): 0x4f62feec + mentoExchangeId(): 0xd373b333 +events: + GDSwapToCusd(address,address,uint256,uint256,bytes): 0x252bc23e3fb01f9986fa157af621236fb8a706ea12622da296e7f2f30d4f1a56 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml new file mode 100644 index 00000000..5f35ee91 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml @@ -0,0 +1,282 @@ +meta: + name: BuyGDCloneV2 + version: "2" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol + inherits: + - Initializable + note: > + Clone implementation used by BuyGDCloneFactory for deterministic per-owner swap executors. + Supports CELO and cUSD flows, with cUSD route selection between Uniswap and Mento when configured. + deployments: + mainnet: + production-celo: + BuyGDFactoryV2: + networkId: 42220 + address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" + creationBlock: 22909812 + related: + - references/deep-researches/on-off-ramp-service.md + - references/guides/on-off-ramp.md + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _owner: address + outputs: [] + +getSwapPath: + mutability: pure + inputs: + - tokens: address[] + - fees: uint24[] + outputs: + - path: bytes + +swap: + notes: + - "Dispatcher entrypoint: if clone holds native CELO balance it executes CELO route, otherwise uses cUSD route." + - "Difference vs swapCelo/swapCusd: this function chooses route by contract balances; specialized functions force a specific asset path." + mutability: payable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + outputs: + - bought: uint256 + emits: [Bought] + errors: [NO_BALANCE] + +swapCelo: + notes: + - "CELO-only convenience path using the default preconfigured Uniswap route." + - "Difference vs swapCeloWithPath: same Uniswap execution, but caller cannot override token path and fee tiers." + - "Difference vs cUSD paths: CELO path does not use Mento route selection." + mutability: payable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + outputs: + - bought: uint256 + emits: [BoughtFromUniswap] + errors: [REFUND_FAILED] + +swapCeloWithPath: + notes: + - "CELO-only path with caller-supplied Uniswap multi-hop route." + - "Difference vs swapCelo: allows custom path optimization when default route is not preferred." + - "Difference vs cUSD paths: still Uniswap-only and does not compare against Mento." + mutability: payable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + - _path: (address[],uint24[]) + outputs: + - bought: uint256 + emits: [BoughtFromUniswap] + errors: [REFUND_FAILED] + +swapCusd: + notes: + - "cUSD-only convenience path that compares expected output from default-path Uniswap and Mento, then executes the better quote." + - "Difference vs swapCusdWithPath: this uses hardcoded default Uniswap path for comparison." + - "Difference vs CELO paths: includes dual-route best-execution logic with optional Mento." + mutability: nonpayable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + outputs: + - bought: uint256 + emits: [BoughtFromMento, BoughtFromUniswap] + errors: [MENTO_NOT_CONFIGURED] + +swapCusdWithPath: + notes: + - "cUSD path with caller-supplied Uniswap route; still compares custom Uniswap quote against Mento quote and picks larger expected output." + - "Difference vs swapCusd: custom path changes only the Uniswap side of the comparison." + - "Difference vs swapCeloWithPath: this function performs route competition (Uniswap vs Mento), not only route customization." + mutability: nonpayable + access: anyone + inputs: + - _minAmount: uint256 + - refundGas: address + - _path: (address[],uint24[]) + outputs: + - bought: uint256 + emits: [BoughtFromMento, BoughtFromUniswap] + errors: [MENTO_NOT_CONFIGURED] + +getExpectedReturnFromUniswapPath: + notes: + - "Quote helper for Uniswap path expected output used by cUSD route selection and preflight checks." + mutability: nonpayable + inputs: + - amountIn: uint256 + - _path: (address[],uint24[]) + outputs: + - expectedReturn: uint256 + +getExpectedReturnFromMento: + notes: + - "Quote helper for Mento expected output for cUSD->G$ used in best-route decision." + mutability: view + inputs: + - cusdAmount: uint256 + outputs: + - expectedReturn: uint256 + errors: [MENTO_NOT_CONFIGURED] + +minAmountByTWAP: + mutability: view + inputs: + - baseAmount: uint256 + - baseToken: address + - period: uint32 + outputs: + - minTwap: uint256 + - quote: uint256 + +recover: + mutability: nonpayable + access: anyone + inputs: + - token: address + outputs: [] + errors: [REFUND_FAILED] + +router: + mutability: view + inputs: [] + outputs: + - addr: address + +celo: + mutability: pure + inputs: [] + outputs: + - token: address + +CUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +USDC: + mutability: pure + inputs: [] + outputs: + - token: address + +GLOUSD: + mutability: pure + inputs: [] + outputs: + - token: address + +GD_FEE_TIER: + mutability: pure + inputs: [] + outputs: + - tier: uint24 + +CUSD_STABLE_FEE_TIER: + mutability: pure + inputs: [] + outputs: + - tier: uint24 + +CELO_STABLE_FEE_TIER: + mutability: pure + inputs: [] + outputs: + - tier: uint24 + +twapPeriod: + mutability: view + inputs: [] + outputs: + - period: uint32 + +stable: + mutability: view + inputs: [] + outputs: + - token: address + +gd: + mutability: view + inputs: [] + outputs: + - token: address + +oracle: + mutability: view + inputs: [] + outputs: + - addr: address + +quoter: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoBroker: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeProvider: + mutability: view + inputs: [] + outputs: + - addr: address + +mentoExchangeId: + mutability: view + inputs: [] + outputs: + - id: bytes32 + +owner: + mutability: view + inputs: [] + outputs: + - addr: address + +CUSD_GAS_COSTS: + mutability: pure + inputs: [] + outputs: + - amount: uint256 + +events: + Bought: + indexed: [] + data: + - inToken: address + - inAmount: uint256 + - outAmount: uint256 + BoughtFromMento: + indexed: [] + data: + - inToken: address + - inAmount: uint256 + - outAmount: uint256 + BoughtFromUniswap: + indexed: [] + data: + - inToken: address + - inAmount: uint256 + - outAmount: uint256 + +errors: + REFUND_FAILED: "Refund call failed." + NO_BALANCE: "No CELO or cUSD available on clone." + MENTO_NOT_CONFIGURED: "Mento broker path not configured." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml new file mode 100644 index 00000000..1476ae35 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml @@ -0,0 +1,36 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address): 0xc4d66de8 + getSwapPath(address[],uint24[]): 0xf0036d5d + swap(uint256,address): 0xd3986f08 + swapCelo(uint256,address): 0xaa6bfa9d + swapCeloWithPath(uint256,address,(address[],uint24[])): 0x81b7d2e0 + swapCusd(uint256,address): 0xb1a5fa9f + swapCusdWithPath(uint256,address,(address[],uint24[])): 0x170ba915 + getExpectedReturnFromUniswapPath(uint256,(address[],uint24[])): 0x12194320 + getExpectedReturnFromMento(uint256): 0xdbb15eb2 + minAmountByTWAP(uint256,address,uint32): 0x821dc910 + recover(address): 0x0cd865ec + router(): 0xf887ea40 + celo(): 0x051ed8ef + CUSD(): 0x758316c9 + USDC(): 0x89a30271 + GLOUSD(): 0x4b5b02d6 + GD_FEE_TIER(): 0xe00e8fdd + CUSD_STABLE_FEE_TIER(): 0xba428926 + CELO_STABLE_FEE_TIER(): 0x60e4bf4b + twapPeriod(): 0xf6207326 + stable(): 0x22be3de1 + gd(): 0xa5e598fc + oracle(): 0x7dc0d1d0 + quoter(): 0xc6bbd5a7 + mentoBroker(): 0x7b89f117 + mentoExchangeProvider(): 0x4f62feec + mentoExchangeId(): 0xd373b333 + owner(): 0x8da5cb5b + CUSD_GAS_COSTS(): 0x32f90ac3 +events: + Bought(address,uint256,uint256): 0xa9a40dec7a304e5915d11358b968c1e8d365992abf20f82285d1df1b30c8e24c + BoughtFromMento(address,uint256,uint256): 0x8e2ac24d7ef5662ee242823a19dbd1c952b3e96ae127228f4bbce83e2816e3fb + BoughtFromUniswap(address,uint256,uint256): 0xdb1f2a6cbbfd964f19c648b140a35992aa80f482df8f519d3211d0bd86c9f335 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml new file mode 100644 index 00000000..32ba0de5 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml @@ -0,0 +1,237 @@ +# CFAv1Forwarder — convenience wrapper for ConstantFlowAgreementV1 +# Allows direct interaction with CFA functions without manually routing through +# the Host's batchCall / forwardBatchCall. Operates as a trusted forwarder, +# preserving msg.sender via EIP-2771. +# +# Each forwarder call is a standalone transaction — forwarder calls cannot be +# combined in a Host.batchCall. To batch multiple operations atomically (e.g. +# wrap tokens + create stream), use Host.batchCall with the raw CFA agreement +# (operationType 201). See Superfluid.abi.yaml for batch operation details. +# Tradeoff: forwarder calls produce human-readable descriptions in wallets +# (e.g. "setFlowrate(token, receiver, flowrate)"), while Host.batchCall +# shows encoded tuples that are difficult for users to verify. +# +# This contract has no events — all events are emitted by the underlying CFA. +# Errors from the CFA (and Host) propagate through to the caller. + +meta: + name: CFAv1Forwarder + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/CFAv1Forwarder.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/ForwarderBase.sol + inherits: [ForwarderBase] + deployments: + # Same address on all networks except avalanche-fuji + mainnet: + eth-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + polygon-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + xdai-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + base-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + optimism-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + arbitrum-one: "0xcfA132E353cB4E398080B9700609bb008eceB125" + bsc-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + avalanche-c: "0xcfA132E353cB4E398080B9700609bb008eceB125" + celo-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + scroll-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" + degenchain: "0xcfA132E353cB4E398080B9700609bb008eceB125" + testnet: + avalanche-fuji: "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D" + base-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + eth-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + optimism-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + scroll-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" + deploymentCreationBlocks: + mainnet: + celo-mainnet: 17404156 + +# == Glossary == +# flowrate — token transfer rate in wad/second (int96) +# flowOperator — account authorized to create/update/delete flows on behalf of another +# buffer — deposit locked as solvency collateral while a flow is active +# permissions — bitmask of create | update | delete rights for an operator +# flowrateAllowance — max flowrate an operator may set per individual flow + +# == Flow Management == +# High-level functions that automatically create, update, or delete flows as needed. + +setFlowrate: + # Smart setter: creates a new flow if none exists, updates if flowrate changed, + # deletes if flowrate is zero. No-ops if the current rate already matches. + notes: + - "Gotcha: Negative flowrate values revert with CFA_FWD_INVALID_FLOW_RATE." + mutability: nonpayable + access: anyone # flow from msg.sender + inputs: + - token: address + - receiver: address + - flowrate: int96 # must be >= 0 + outputs: + - bool + errors: [CFA_FWD_INVALID_FLOW_RATE] + +setFlowrateFrom: + # Same as setFlowrate but can be called by a flow operator on behalf of `sender`. + # msg.sender must have sufficient operator permissions and flowrateAllowance. + mutability: nonpayable + access: sender | operator + inputs: + - token: address + - sender: address + - receiver: address + - flowrate: int96 + outputs: + - bool + errors: [CFA_FWD_INVALID_FLOW_RATE] + +# == Low-Level Flow Operations == +# Direct wrappers around CFA functions. These give full control (including userData) +# but require the caller to know whether a flow already exists. +# If sender != msg.sender, the *ByOperator variant is used internally. + +createFlow: + # Create a new flow. Reverts if a flow already exists between sender and receiver. + mutability: nonpayable + access: sender | operator + inputs: + - token: address + - sender: address + - receiver: address + - flowrate: int96 + - userData: bytes + outputs: + - bool + +updateFlow: + # Update an existing flow's rate. Reverts if no flow exists. + mutability: nonpayable + access: sender | operator + inputs: + - token: address + - sender: address + - receiver: address + - flowrate: int96 + - userData: bytes + outputs: + - bool + +deleteFlow: + # Delete an existing flow. Can be called by sender, receiver, or an operator. + # If msg.sender is neither sender nor receiver, deleteFlowByOperator is used. + mutability: nonpayable + access: sender | receiver | operator + inputs: + - token: address + - sender: address + - receiver: address + - userData: bytes + outputs: + - bool + +# == ACL (Operator Permissions) == + +grantPermissions: + # Grant full create/update/delete permissions with max flowrateAllowance to an operator. + # Convenience wrapper — equivalent to updateFlowOperatorPermissions with full control. + mutability: nonpayable + access: anyone # grants on msg.sender's flows + inputs: + - token: address + - flowOperator: address + outputs: + - bool + +revokePermissions: + # Revoke all permissions from an operator. Does not affect existing flows. + mutability: nonpayable + access: anyone # revokes on msg.sender's flows + inputs: + - token: address + - flowOperator: address + outputs: + - bool + +updateFlowOperatorPermissions: + # Set granular operator permissions and flowrate allowance. + notes: + - "Gotcha: flowrateAllowance limits per-flow rate, NOT aggregate net flow." + mutability: nonpayable + access: anyone # grants on msg.sender's flows + inputs: + - token: address + - flowOperator: address + - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete + - flowrateAllowance: int96 + outputs: + - bool + +# == Flow Queries == + +getFlowrate: + mutability: view + inputs: + - token: address + - sender: address + - receiver: address + outputs: + - flowrate: int96 + +getFlowInfo: + mutability: view + inputs: + - token: address + - sender: address + - receiver: address + outputs: + - lastUpdated: uint256 + - flowrate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +getBufferAmountByFlowrate: + # Returns the deposit/buffer required for a given flowrate. + notes: + - "Gotcha: This value is governance-configurable and may change over time. Changes only affect newly created/updated flows." + mutability: view + inputs: + - token: address + - flowrate: int96 + outputs: + - bufferAmount: uint256 + +getAccountFlowrate: + # Net aggregate flowrate for an account (incoming minus outgoing). + mutability: view + inputs: + - token: address + - account: address + outputs: + - flowrate: int96 + +getAccountFlowInfo: + mutability: view + inputs: + - token: address + - account: address + outputs: + - lastUpdated: uint256 + - flowrate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +# == ACL Queries == + +getFlowOperatorPermissions: + mutability: view + inputs: + - token: address + - sender: address + - flowOperator: address + outputs: + - permissions: uint8 + - flowrateAllowance: int96 + +# == Errors == + +errors: + - CFA_FWD_INVALID_FLOW_RATE # flowrate argument was negative diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml new file mode 100644 index 00000000..f98e21ea --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml @@ -0,0 +1,19 @@ +# Generated by scripts/selectors.mjs +functions: + setFlowrate(address,address): 0x2f57fb6f + setFlowrateFrom(address,address,address,int96): 0xc5ad5c1a + createFlow(address,address,address,int96,bytes): 0xe15536b6 + updateFlow(address,address,address,int96,bytes): 0x0c033991 + deleteFlow(address,address,address,bytes): 0xb4b333c6 + grantPermissions(address,address): 0x7243fb93 + revokePermissions(address,address): 0x0bd0728d + updateFlowOperatorPermissions(address,address): 0x42294caf + getFlowrate(address,address,address): 0x1d8b6526 + getFlowInfo(address,address,address): 0x2860fd93 + getBufferAmountByFlowrate(address,int96): 0x09f0b495 + getAccountFlowrate(address,address): 0x22c904d9 + getAccountFlowInfo(address,address): 0x0f1ac495 + getFlowOperatorPermissions(address,address,address): 0x4d3f60f9 +events: +{} +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml new file mode 100644 index 00000000..afaec5dc --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml @@ -0,0 +1,560 @@ +# Superfluid Constant Flow Agreement (CFA) v1 +# Manages continuous per-second token streams between accounts. +# +# NOTE: emits/errors mappings are traced from source code — verify against implementation. +# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID) +# are omitted — they belong to the UUPSProxiable / AgreementBase layer. +# Pure helpers addPermissions/removePermissions are omitted — they were made public +# for testability only and are not part of the protocol interface. + +meta: + name: ConstantFlowAgreementV1 + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/AgreementBase.sol + implements: [IConstantFlowAgreementV1, ISuperAgreement] + inherits: [AgreementBase] + deployments: + mainnet: + eth-mainnet: "0x2844c1BBdA121E9E43105630b9C8310e5c72744b" + polygon-mainnet: "0x6EeE6060f715257b970700bc2656De21dEdF074C" + xdai-mainnet: "0xEbdA4ceF883A7B12c4E669Ebc58927FBa8447C7D" + base-mainnet: "0x19ba78B9cDB05A877718841c574325fdB53601bb" + optimism-mainnet: "0x204C6f131bb7F258b2Ea1593f5309911d8E458eD" + arbitrum-one: "0x731FdBB12944973B500518aea61942381d7e240D" + bsc-mainnet: "0x49c38108870e74Cb9420C0991a85D3edd6363F75" + avalanche-c: "0x6946c5B38Ffea373b0a2340b4AEf0De8F6782e58" + celo-mainnet: "0x9d369e78e1a682cE0F8d9aD849BeA4FE1c3bD3Ad" + scroll-mainnet: "0xB3bcD6da1eeB6c97258B3806A853A6dcD3B6C00c" + degenchain: "0x82cc052d1b17aC554a22A88D5876B56c6b51e95c" + testnet: + avalanche-fuji: "0x16843ac25Ccc58Aa7960ba05f61cBB17b36b130A" + base-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" + eth-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" + optimism-sepolia: "0x8a3170AdbC67233196371226141736E4151e7C26" + scroll-sepolia: "0xbc46B4Aa41c055578306820013d4B65fff42711E" + deploymentCreationBlocks: + mainnet: + celo-mainnet: 16393492 + +# == Abbreviations == +# 3Ps — Three Periods: liquidation, patrician, and pleb (solvency model) +# ACL — Access Control List (flow operator permissions) +# CFA — Constant Flow Agreement +# ctx — context (Superfluid call context bytes, carries msg.sender and userData) +# GDA — General Distribution Agreement (the other Superfluid agreement) +# PPP — Patrician-Pleb-Pirate (the 3Ps solvency periods) + +# == Glossary == +# flow / stream — continuous per-second token transfer, used interchangeably +# flowRate — tokens per second (int96, wei-denominated); positive = outgoing +# deposit — buffer locked when opening a flow; protects against insolvency +# owed deposit — portion of deposit owed back to the sender by a Super App receiver +# flow operator — an address authorized to create/update/delete flows on behalf of a sender +# flow rate allowance — maximum net flow rate an operator may allocate (int96); decrements on use +# permissions bitmask — uint8: bit 0 = create (1), bit 1 = update (2), bit 2 = delete (4) +# liquidation — closing an insolvent sender's flow; rewards go to the liquidator or bond +# patrician period — grace window after insolvency where reward goes to the bond account +# pleb period — window after patrician where reward goes to the liquidator +# pirate / bailout — state where total deposit cannot cover the deficit; protocol bails out +# Super App — a contract registered with the Host that receives agreement callbacks +# app credit — deposit credit enabling "zero-balance Super Apps" to relay flows +# without pre-funded tokens. 1:1 relay (one in, one out at same rate) +# always works at zero balance. Fan-out (1:N) needs the app to hold +# tokens. Credit is settled as "owed deposit" on the original sender. +# critical — an account whose available balance is negative (eligible for liquidation) +# jailed — a Super App penalized for violating protocol rules; loses callbacks +# +# == Time Conventions == +# Conventional seconds-per-period used in Superfluid apps for flow rate conversion: +# month: 2628000 (365.25 / 12 * 86400) +# year: 31536000 (365 * 86400) +# Flow rate from a monthly amount: flowRate = monthlyAmount / 2628000 +# Flow rate from a yearly amount: flowRate = yearlyAmount / 31536000 + +# == Flow Management == +# Core operations for creating, updating, and deleting token streams. +# Functions with ctx are called through the Host — either via +# Host.callAgreement (single op) or Host.batchCall (operationType 201). +# For batchCall, the data field is abi.encode(callData, userData) where +# callData is the full ABI-encoded function call with an empty ctx ("0x") +# as placeholder. The Host replaces the placeholder with the real context. +# The access field reflects who can initiate the call, not the direct caller. + +createFlow: + # Start a new stream from ctx.msgSender to receiver. + # The deposit amount depends on the governance-configured liquidation period + # and minimum deposit. The lower 32 bits of the deposit are clipped (rounded up). + notes: + - "Gotcha: A deposit is taken as a safety margin for solvency agents." + mutability: nonpayable + access: anyone # flow from ctx.msgSender + inputs: + - token: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] + +updateFlow: + # Change the flow rate of an existing stream from ctx.msgSender to receiver. + # Note: owedDeposit accumulates when a Super App receiver uses app credit to + # open outgoing streams. The sender bears the deposit cost for those streams. + notes: + - "Gotcha: Deposit is adjusted to match the new flow rate but never refunds owed deposit — that adjustment happens separately via the app credit system." + mutability: nonpayable + access: anyone # flow from ctx.msgSender + inputs: + - token: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] + +deleteFlow: + # Stop a stream between sender and receiver. + # During liquidation the reward distribution depends on the solvency period: + # patrician → bond account gets reward; pleb → liquidator gets reward; + # pirate → liquidator gets full single deposit, protocol covers the bailout. + notes: + - "Gotcha: Third-party callers (not sender, receiver, or operator) only succeed if the sender is critical (negative available balance) or either party is jailed." + mutability: nonpayable + access: sender | receiver | operator | anyone(if-critical-or-jailed) + inputs: + - token: address + - sender: address + - receiver: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] + +# == Operator Flow Management == +# Act on behalf of another account. Requires ACL permissions granted by the sender. +# The operator's flow rate allowance is decremented on create/update. +# Functions with ctx are called through the Host. + +createFlowByOperator: + # Create a flow on behalf of sender. Consumes flow rate allowance. + notes: + - "Gotcha: Reverts if ctx.msgSender IS the sender — use createFlow instead." + mutability: nonpayable + access: operator + inputs: + - token: address + - sender: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ACL_NO_SENDER_CREATE, CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] + +updateFlowByOperator: + # Update a flow on behalf of sender. Only consumes allowance if flow rate increases. + # If flowRateAllowance is type(int96).max, it is treated as unlimited. + notes: + - "Gotcha: If the new rate is lower than the old rate, no allowance is consumed." + mutability: nonpayable + access: operator + inputs: + - token: address + - sender: address + - receiver: address + - flowRate: int96 # must be > 0 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ACL_NO_SENDER_UPDATE, CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] + +deleteFlowByOperator: + # Delete a flow on behalf of sender. Does not consume flow rate allowance. + mutability: nonpayable + access: operator + inputs: + - token: address + - sender: address + - receiver: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] + errors: [CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS, CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] + +# == Flow Queries == + +realtimeBalanceOf: + # Compute the real-time CFA balance contribution for an account. + # Returns the dynamic balance delta (flowRate * elapsed), total deposit held, + # and total owed deposit. This is called by the token's realtimeBalanceOfNow + # to assemble the full balance across all agreements. + # To get an account's actual token balance, use SuperToken.realtimeBalanceOfNow + # (or SuperToken.balanceOf for ERC-20 compatible, clamped to zero). + # It can be deeply negative for net-outflow accounts as time progresses. + notes: + - "Gotcha: Returns only the CFA portion of the balance, not the total." + - "Gotcha: The returned dynamicBalance is a delta, not an absolute balance." + mutability: view + inputs: + - token: address + - account: address + - time: uint256 + outputs: + - dynamicBalance: int256 # flowRate * (time - lastUpdate); can be negative + - deposit: uint256 # total deposit locked across all outflows + - owedDeposit: uint256 # total owed deposit from Super App receivers + +getFlow: + # Get flow data between a specific sender-receiver pair. + mutability: view + inputs: + - token: address + - sender: address + - receiver: address + outputs: + - timestamp: uint256 # last update time + - flowRate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +getFlowByID: + # Get flow data using the agreement ID (keccak256 of sender and receiver). + mutability: view + inputs: + - token: address + - flowId: bytes32 # keccak256(abi.encode(sender, receiver)) + outputs: + - timestamp: uint256 + - flowRate: int96 + - deposit: uint256 + - owedDeposit: uint256 + +getAccountFlowInfo: + # Aggregated flow state for an account across all its CFA flows. + mutability: view + inputs: + - token: address + - account: address + outputs: + - timestamp: uint256 # last time any flow was updated for this account + - flowRate: int96 # net flow rate (inflows - outflows) + - deposit: uint256 # sum of deposits across all outflows + - owedDeposit: uint256 # sum of owed deposits across all outflows + +getNetFlow: + # Net flow rate for an account (sum of inflows minus outflows). + mutability: view + inputs: + - token: address + - account: address + outputs: + - flowRate: int96 + +# == ACL Management == +# Manage flow operator permissions and flow rate allowances. +# The caller (ctx.msgSender) is always the permission granter — they control +# who can operate on their own flows. +# Functions with ctx are called through the Host. + +updateFlowOperatorPermissions: + # Set exact permissions and flow rate allowance for a flow operator. + mutability: nonpayable + access: anyone # grants on ctx.msgSender's flows + inputs: + - token: address + - flowOperator: address + - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete + - flowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +authorizeFlowOperatorWithFullControl: + # Grant all permissions (create+update+delete) with unlimited allowance. + # Shorthand for updateFlowOperatorPermissions(token, op, 7, type(int96).max, ctx). + mutability: nonpayable + access: anyone # grants on ctx.msgSender's flows + inputs: + - token: address + - flowOperator: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +revokeFlowOperatorWithFullControl: + # Revoke all permissions and set allowance to zero. + # Shorthand for updateFlowOperatorPermissions(token, op, 0, 0, ctx). + mutability: nonpayable + access: anyone # revokes on ctx.msgSender's flows + inputs: + - token: address + - flowOperator: address + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +increaseFlowRateAllowance: + # Increase flow rate allowance for an operator by a delta. + # Delegates to increaseFlowRateAllowanceWithPermissions with permissionsToAdd=0. + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - addedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +decreaseFlowRateAllowance: + # Decrease flow rate allowance for an operator by a delta. + # Delegates to decreaseFlowRateAllowanceWithPermissions with permissionsToRemove=0. + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - subtractedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +increaseFlowRateAllowanceWithPermissions: + # Increase flow rate allowance and add permission bits in one call. + # permissionsToAdd is OR'd with existing permissions. + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - permissionsToAdd: uint8 # bitmask OR'd onto existing permissions + - addedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +decreaseFlowRateAllowanceWithPermissions: + # Decrease flow rate allowance and remove permission bits in one call. + # permissionsToRemove is AND-NOT'd from existing permissions. + notes: + - "Gotcha: Reverts if the resulting allowance would go negative." + mutability: nonpayable + access: anyone # ctx.msgSender's allowance + inputs: + - token: address + - flowOperator: address + - permissionsToRemove: uint8 # bitmask AND-NOT'd from existing permissions + - subtractedFlowRateAllowance: int96 + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowOperatorUpdated] + errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] + +# == ACL Queries == + +getFlowOperatorData: + # Get permissions and allowance for a flow operator. + mutability: view + inputs: + - token: address + - sender: address # the permission granter + - flowOperator: address # the permission grantee + outputs: + - flowOperatorId: bytes32 # keccak256(abi.encode("flowOperator", sender, flowOperator)) + - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete + - flowRateAllowance: int96 + +getFlowOperatorDataByID: + # Get permissions and allowance using the pre-computed operator ID. + mutability: view + inputs: + - token: address + - flowOperatorId: bytes32 + outputs: + - permissions: uint8 + - flowRateAllowance: int96 + +# == Solvency Queries == +# Check whether an account is in the patrician period (grace window for liquidation). + +isPatricianPeriodNow: + # Check patrician period status using the Host's current timestamp. + mutability: view + inputs: + - token: address + - account: address + outputs: + - isCurrentlyPatricianPeriod: bool + - timestamp: uint256 # the Host's block.timestamp used for the check + +isPatricianPeriod: + # Check patrician period status at a specific timestamp. + mutability: view + inputs: + - token: address + - account: address + - timestamp: uint256 + outputs: + - bool + +# == Deposit Helpers == + +getMaximumFlowRateFromDeposit: + # Calculate the maximum flow rate achievable with a given deposit. + notes: + - "Gotcha: Deposit is clipped (lower 32 bits zeroed) and rounded down." + mutability: view + inputs: + - token: address # needed to look up liquidation period from governance + - deposit: uint256 + outputs: + - flowRate: int96 + errors: [CFA_DEPOSIT_TOO_BIG] + +getDepositRequiredForFlowRate: + # Calculate the deposit required for a given flow rate. + # Returns max(minimumDeposit, flowRate * liquidationPeriod) with rounding. + mutability: view + inputs: + - token: address # needed to look up liquidation period and minimum deposit + - flowRate: int96 + outputs: + - deposit: uint256 + errors: [CFA_INVALID_FLOW_RATE, CFA_FLOW_RATE_TOO_BIG] + +# == Protocol Constants == + +agreementType: + # Returns keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") + mutability: pure + outputs: + - bytes32 + +DEFAULT_MINIMUM_DEPOSIT: + # Minimum deposit floor: uint96(1 << 32) ≈ 4.29 * 10^9 wei. + # Governance may set a higher per-token minimum; this is the absolute floor. + mutability: view + outputs: + - uint256 + +MAXIMUM_DEPOSIT: + # Maximum deposit cap: type(int96).max + mutability: view + outputs: + - uint256 + +MAXIMUM_FLOW_RATE: + # Maximum flow rate: type(int96).max + mutability: view + outputs: + - uint256 + +CFA_HOOK_GAS_LIMIT: + # Gas limit for external hook calls (Super App callbacks): 250,000 + mutability: view + outputs: + - uint64 + +# == Events == + +events: + FlowUpdated: + # Emitted on every create, update, and delete flow operation. + # totalSenderFlowRate and totalReceiverFlowRate are the NET flow rates + # after the operation, not the individual flow rate. + notes: + - "Gotcha: Always emitted together with FlowUpdatedExtension." + indexed: + - token: address + - sender: address + - receiver: address + data: + - flowRate: int96 # new rate for this specific flow (0 on delete) + - totalSenderFlowRate: int256 # sender's net flow rate after this operation + - totalReceiverFlowRate: int256 + - userData: bytes + + FlowUpdatedExtension: + # Companion event to FlowUpdated. Carries the operator and new deposit. + # Emitted immediately after FlowUpdated so indexers can correlate them. + indexed: + - flowOperator: address # ctx.msgSender who initiated the operation + data: + - deposit: uint256 # new deposit for this specific flow + + FlowOperatorUpdated: + # Emitted when operator permissions or flow rate allowance change. + indexed: + - token: address + - sender: address # the permission granter + - flowOperator: address # the permission grantee + data: + - permissions: uint8 # updated bitmask + - flowRateAllowance: int96 # updated allowance + + # Inherited events (from AgreementBase / UUPSProxiable): + # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) + # Initialized — emitted on proxy initialization (version) + +# == Errors == + +errors: + # Flow validation + - CFA_FLOW_ALREADY_EXISTS # createFlow when flow exists + - CFA_FLOW_DOES_NOT_EXIST # update/delete when no flow + - CFA_INVALID_FLOW_RATE # flowRate <= 0 + - CFA_NO_SELF_FLOW # sender == receiver + - CFA_ZERO_ADDRESS_SENDER # sender is address(0) + - CFA_ZERO_ADDRESS_RECEIVER # receiver is address(0) + # Solvency + - CFA_INSUFFICIENT_BALANCE # sender cannot cover deposit + - CFA_NON_CRITICAL_SENDER # third-party delete but sender is solvent + - CFA_DEPOSIT_TOO_BIG # deposit > MAXIMUM_DEPOSIT + - CFA_FLOW_RATE_TOO_BIG # flowRate * liquidationPeriod overflows + # ACL + - CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED # operator exceeds granted allowance + - CFA_ACL_NO_NEGATIVE_ALLOWANCE # resulting allowance would be < 0 + - CFA_ACL_NO_SENDER_CREATE # operator cannot be the sender (use createFlow) + - CFA_ACL_NO_SENDER_FLOW_OPERATOR # cannot set yourself as your own operator + - CFA_ACL_NO_SENDER_UPDATE # operator cannot be the sender (use updateFlow) + - CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS + - CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS + - CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS + - CFA_ACL_UNCLEAN_PERMISSIONS # permission bits outside valid range + # Super App / Host + - CFA_HOOK_OUT_OF_GAS # Super App callback exceeded gas limit + - APP_RULE: # Super App rule violation + inputs: + - _code: uint256 + - AGREEMENT_BASE_ONLY_HOST # call not routed through the Host contract + # SafeCast (inherited from OpenZeppelin) + - SafeCastOverflowedIntToUint: # int256 value overflows on cast to uint256 + inputs: + - value: int256 + - SafeCastOverflowedUintToInt: # uint256 value overflows on cast to int256 + inputs: + - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml new file mode 100644 index 00000000..a929e06c --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml @@ -0,0 +1,33 @@ +# Generated by scripts/selectors.mjs +functions: + createFlow(address,address): 0x7deac86a + updateFlow(address,address): 0xb6214437 + deleteFlow(address,address,address,bytes): 0xb4b333c6 + createFlowByOperator(address,address,address): 0x97b0b745 + updateFlowByOperator(address,address,address): 0xaea4dc29 + deleteFlowByOperator(address,address,address,bytes): 0x4c8b181f + realtimeBalanceOf(address,address,uint256): 0x9b2e48bc + getFlow(address,address,address): 0xe6a1e888 + getFlowByID(address): 0x4eccd8ac + getAccountFlowInfo(address,address): 0x0f1ac495 + getNetFlow(address,address): 0xe8e7e2d1 + updateFlowOperatorPermissions(address,address): 0x42294caf + authorizeFlowOperatorWithFullControl(address,address,bytes): 0x54b770e3 + revokeFlowOperatorWithFullControl(address,address,bytes): 0x062e56ec + increaseFlowRateAllowance(address,address,int96,bytes): 0xac5f5d00 + decreaseFlowRateAllowance(address,address,int96,bytes): 0x5f51fb23 + increaseFlowRateAllowanceWithPermissions(address,address): 0x5e6a4dc3 + decreaseFlowRateAllowanceWithPermissions(address,address): 0x8de39d9b + getFlowOperatorData(address): 0x5f13dbbc + getFlowOperatorDataByID(address,bytes32): 0x09d256ef + isPatricianPeriodNow(address,address): 0x4fe9c291 + isPatricianPeriod(address,address,uint256): 0x4b839e0b + getMaximumFlowRateFromDeposit(): 0x4ba52c34 + getDepositRequiredForFlowRate(): 0xcc111b9e + CFA_HOOK_GAS_LIMIT(): 0xbf3fbc28 +events: + FlowUpdated(address,address,address,int256,bytes): 0x8e3b8f31fe09d2ca20fa7f76ec574bc9fea49f16a91f1ca828154ea76e76a20a + FlowUpdatedExtension(): 0x9bf0b3e0199b411ca09607d040b7672430af5dae9d159b715c9f8004c876065f + FlowOperatorUpdated(address): 0x401d2cb05b3db80617d70aabf976f52d2a5dac64298eadeed7a81ad4bb5d4fdd + # Inherited events (from AgreementBase / UUPSProxiable)(): 0xcabfd4ad95e3bb803739ac513717fccee724fc0c5741e01cf84319a96120eefa +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml new file mode 100644 index 00000000..c60d4985 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml @@ -0,0 +1,100 @@ +# GoodDollarOFTAdapter — LayerZero V2 OFT adapter pattern bridging underlying G$ +# Exact bytecode lives in the GoodDollar OFT deployment; this documents the surface agents call. + +meta: + name: GoodDollarOFTAdapter + version: LayerZero-OFT-adapter (generic) + source: + - https://docs.layerzero.network/ + - https://github.com/LayerZero-Labs/devtools + note: > + Flow: optional peers(dstEid) check -> approve underlying to minterBurner if + MessagingFee, refundAddress) paying nativeFee in msg.value when applicable. + SendParam mirrors LayerZero OFT: dstEid, to (bytes32 padded recipient), + amountLD, minAmountLD, extraOptions, composeMsg, oftCmd. + Verify tuple field order and OFT revision against your deployed artifact before mainnet use. + Event names and topic0 hashes vary by OFT package version — pull from the deployment ABI. + related: + - references/guides/bridge.md + +token: + mutability: view + inputs: [] + outputs: + - underlyingToken: address + +minterBurner: + notes: + - "Underlying G$ must approve this spender when burning for cross-chain send." + mutability: view + inputs: [] + outputs: + - minterBurner: address + +oftVersion: + mutability: view + inputs: [] + outputs: + - interfaceId: bytes4 + - version: uint64 + +peers: + notes: + - "Returns bytes32 peer address configured for destination endpoint id." + mutability: view + inputs: + - dstEid: uint32 + outputs: + - peer: bytes32 + +endpoint: + mutability: view + inputs: [] + outputs: + - lzEndpoint: address + +owner: + mutability: view + inputs: [] + outputs: + - account: address + +quoteSend: + notes: + - "Simulates messaging + bridge fee; use nativeFee as msg.value on send when paying in native gas token." + mutability: view + inputs: + - sendParam: SendParam + - payInLzToken: bool + outputs: + - nativeFee: uint256 + - lzTokenFee: uint256 + errors: + - NO_PEER + - LZ_INVALID_OPTIONS + +send: + notes: + - "Payable: include MessagingFee.nativeFee in msg.value when fee is native." + - "Consumes sendParam.amountLD from sender on source chain and emits cross-chain message to dstEid peer." + - "sendParam.to must be destination receiver encoded as bytes32." + mutability: payable + access: sender + inputs: + - sendParam: SendParam + - fee: MessagingFee + - refundAddress: address + outputs: + - msgReceipt: MessageReceipt + - oftReceipt: OFTReceipt + errors: + - NO_PEER + - SLIPPAGE_OR_AMOUNT + - LZ_INVALID_OPTIONS + +events: {} + +errors: + NO_PEER: "Destination peer not configured for dstEid." + LZ_INVALID_OPTIONS: "Composer/options payload rejected by LayerZero." + SLIPPAGE_OR_AMOUNT: "Bridged amount violates minAmount or available balance." diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml new file mode 100644 index 00000000..e5615a1e --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml @@ -0,0 +1,12 @@ +# Generated by scripts/selectors.mjs +functions: + token(): 0xfc0c546a + minterBurner(): 0x2ef8c5a4 + approvalRequired(): 0x9f68b964 + oftVersion(): 0x156a0d0f + peers(uint32): 0xbb0b6a53 + endpoint(): 0x5e280f11 + owner(): 0x8da5cb5b +events: +{} +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml new file mode 100644 index 00000000..d736bc19 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml @@ -0,0 +1,210 @@ +# GooddollarSavingsStream (Ubeswap) — G$ native Super Token stake with Superfluid GDA reward stream + +meta: + name: GooddollarSavingsStream + version: "1" + source: + - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/GooddollarSavingsStream.sol + - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/interfaces/IGooddollarSavingsStream.sol + - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/StakingVault.sol + inherits: + - IGooddollarSavingsStream + - Ownable + - ERC2771Context + - ReentrancyGuard + note: > + Celo production savings. Staked principal sits in StakingVault; this contract holds reward + balance and streams via a Superfluid GDA distribution pool (`distributeFlow`). Effective rate is + min(rewardRate, maxRewardRatePerToken * totalSupply / 1e18), throttled when reward balance is low. + Staking token is the G$ native Super Token (`superToken`). Fuse->CELO migration uses `stakeFor` + after bridge. Ubeswap lists this as Celo mainnet savings (streaming). + deployments: + mainnet: + production-celo: + GooddollarSavingsStream: + networkId: 42220 + address: "0x059ee811414230d1Fb157878D2b491240F4D8d3B" + creationBlock: 66685884 + related: + - https://github.com/Ubeswap/gooddollar-contracts/blob/main/README.md + - https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B + - references/contracts/SuperToken.abi.yaml + - references/guides/migrate-fuse-staking-to-celo-savings.md + - references/deep-researches/fuse-to-celo-staking-migration.md + +totalSupply: + mutability: view + inputs: [] + outputs: + - supply: uint256 + +balanceOf: + mutability: view + inputs: + - account: address + outputs: + - balance: uint256 + +getDailyRewards: + mutability: view + inputs: [] + outputs: + - daily: uint256 + +getEffectiveFlowRate: + notes: + - "Returns 0 when totalSupply is 0, rewardRate is 0, or reward balance cannot sustain MIN_STREAM_BUFFER_SECONDS." + - "Otherwise min(global rewardRate, APR cap) as int96 for Superfluid distributeFlow." + mutability: view + inputs: [] + outputs: + - flowRate: int96 + +periodFinish: + notes: + - "Estimated timestamp when current reward balance is drained at getEffectiveFlowRate; 0 if not streaming." + mutability: view + inputs: [] + outputs: + - finish: uint256 + +getUnits: + mutability: view + inputs: + - account: address + outputs: + - units: uint128 + +getTotalUnits: + mutability: view + inputs: [] + outputs: + - units: uint128 + +stake: + notes: + - "Pulls G$ Super Token from msg.sender, deposits principal to StakingVault, updates GDA pool units, syncFlowRate." + - "Requires prior superToken.approve(this, amount)." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + outputs: [] + emits: [Staked] + +stakeFor: + notes: + - "Migrator path: caller funds transfer; stake credits `_balances[recipient]` and pool units for recipient." + - "recipient must not be zero, this, superToken, or vault." + - "Used after Fuse->CELO bridge when backend wallet holds bridged G$." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + - recipient: address + outputs: [] + emits: [Staked] + +withdraw: + notes: + - "Reduces stake and pool units, syncFlowRate, then vault.withdraw to msg.sender." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + outputs: [] + emits: [Withdrawn] + +exit: + notes: + - "Full withdraw of caller principal via withdraw(balance)." + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + emits: [Withdrawn] + +addToReward: + notes: + - "Pulls reward Super Token from caller; re-syncs flow rate when balance was throttling the stream." + mutability: nonpayable + access: anyone + inputs: + - reward: uint256 + outputs: [] + emits: [RewardAdded] + +syncFlowRate: + notes: + - "Permissionless; call when tokens were sent directly to the contract so distributeFlow picks up balance." + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + emits: [FlowRateUpdated] + +setDailyRewards: + mutability: nonpayable + access: owner + inputs: + - _dailyRewards: uint256 + outputs: [] + emits: [DailyRewardsUpdated] + +setMaxRewardRatePerToken: + mutability: nonpayable + access: owner + inputs: + - _value: uint256 + outputs: [] + emits: [MaxRewardRateUpdated] + +recoverERC20: + notes: + - "Cannot recover the G$ Super Token (staking/reward asset)." + mutability: nonpayable + access: owner + inputs: + - tokenAddress: address + - tokenAmount: uint256 + outputs: [] + emits: [Recovered] + +events: + Staked: + indexed: + - user: address + data: + - amount: uint256 + Withdrawn: + indexed: + - user: address + data: + - amount: uint256 + RewardAdded: + data: + - reward: uint256 + DailyRewardsUpdated: + data: + - rewardRate: uint256 + - givenDailyRewards: uint256 + MaxRewardRateUpdated: + data: + - newMaxRate: uint256 + FlowRateUpdated: + data: + - newFlowRate: int96 + Recovered: + indexed: + - token: address + data: + - amount: uint256 + - receiver: address + +errors: + - CannotStakeZero + - CannotWithdrawZero + - InsufficientStake + - InvalidAddress + - NoRewardToAdd + - CannotRecoverStakingToken diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml new file mode 100644 index 00000000..f04d75a4 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml @@ -0,0 +1,27 @@ +# Generated by scripts/selectors.mjs +functions: + totalSupply(): 0x18160ddd + balanceOf(address): 0x70a08231 + getDailyRewards(): 0x68527008 + getEffectiveFlowRate(): 0xa2efe857 + periodFinish(): 0xebe2b12b + getUnits(address): 0x0fefbc09 + getTotalUnits(): 0xa754a702 + stake(uint256): 0xa694fc3a + stakeFor(uint256,address): 0x51746bb2 + withdraw(uint256): 0x2e1a7d4d + exit(): 0xe9fad8ee + addToReward(uint256): 0x2ce618fa + syncFlowRate(): 0x0e83284a + setDailyRewards(uint256): 0x4adef718 + setMaxRewardRatePerToken(uint256): 0x6a03a9e2 + recoverERC20(address,uint256): 0x8980f11f +events: + Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d + Withdrawn(address,uint256): 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5 + RewardAdded(uint256): 0xde88a922e0d3b88b24e9623efeb464919c6bf9f66857a65e2bfcf2ce87a9433d + DailyRewardsUpdated(uint256,uint256): 0x54f5b7ef058007cebca11af0127cd80c0bac4e968788eabb3c1e70d1bfb78edd + MaxRewardRateUpdated(uint256): 0x9041b23d05af9bceefc73becffefd1887907ad55c3d6c6655230b37ef87dbb07 + FlowRateUpdated(int96): 0xd02fcf6b96acb60ac68942c7d5075e11a8a0b38cc865c1916934f4e1f129ded0 + Recovered(address,uint256,address): 0xb197f0a554c4d7840105e6ae65f0e275e9e8605a969dffa8caa7f1f118a2e1f5 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml new file mode 100644 index 00000000..5adccb32 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml @@ -0,0 +1,202 @@ +# GovernanceStakingV2 (Fuse) — G$ staking share token with GDAO reward accrual +# Deployment key in GoodProtocol release metadata is GovernanceStakingV2. + +meta: + name: GovernanceStakingV2 + version: "2" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/governance/GovernanceStaking.sol + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/releases/deployment.json + inherits: + - ERC20Upgradeable + - MultiBaseGovernanceShareField + - DAOUpgradeableContract + - ReentrancyGuardUpgradeable + note: > + Fuse governance staking contract where users stake G$ and receive staking shares (`sG$`) with + GDAO-style rewards minted on reward withdrawal paths. `withdrawStake(0)` means full unstake. + In GoodProtocol `releases/deployment.json` this address is listed as GovernanceStakingV2 on Fuse `production`. + This is the old staking contract in Fuse->CELO migration flows. + deployments: + mainnet: + production: + GovernanceStakingV2: + networkId: 122 + address: "0xB7C3e738224625289C573c54d402E9Be46205546" + creationBlock: 15956809 + related: + - references/guides/migrate-fuse-staking-to-celo-savings.md + - references/deep-researches/fuse-to-celo-staking-migration.md + +getChainBlocksPerMonth: + mutability: pure + inputs: [] + outputs: + - blocks: uint256 + +stake: + notes: + - "Requires prior G$ ERC20 approval to staking contract." + - "Mints staking share token and updates reward productivity before event emission." + mutability: nonpayable + access: anyone + inputs: + - _amount: uint256 + outputs: [] + emits: [Staked] + +withdrawStake: + notes: + - "If _amount is 0, contract interprets it as full user unstake." + - "Burns staking shares, updates productivity, mints pending rewards, then transfers G$ out." + mutability: nonpayable + access: anyone + inputs: + - _amount: uint256 + outputs: [] + emits: [StakeWithdraw] + +withdrawRewards: + notes: + - "Claims rewards without changing staked principal." + mutability: nonpayable + access: anyone + inputs: [] + outputs: + - minted: uint256 + emits: [ReputationEarned] + +setMonthlyRewards: + notes: + - "DAO-controlled update of rewardsPerBlock schedule via avatar authorization." + mutability: nonpayable + access: avatar + inputs: + - _monthlyAmount: uint256 + outputs: [] + +getRewardsPerBlock: + mutability: view + inputs: [] + outputs: + - amount: uint256 + +getProductivity: + mutability: view + inputs: + - _user: address + outputs: + - productivity: uint256 + - rewardDebt: uint256 + +getUserPendingReward: + mutability: view + inputs: + - _user: address + outputs: + - pending: uint256 + +users: + notes: + - "UserInfo getter for internal staking productivity record." + mutability: view + inputs: + - _user: address + outputs: + - amount: uint256 + - rewardDebt: uint256 + +totalRewardsPerShare: + mutability: view + inputs: [] + outputs: + - value: uint256 + +decimals: + mutability: view + inputs: [] + outputs: + - d: uint8 + +totalSupply: + mutability: view + inputs: [] + outputs: + - supply: uint256 + +balanceOf: + mutability: view + inputs: + - account: address + outputs: + - balance: uint256 + +allowance: + mutability: view + inputs: + - owner: address + - spender: address + outputs: + - amount: uint256 + +approve: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - amount: uint256 + outputs: + - ok: bool + emits: [Approval] + +transfer: + mutability: nonpayable + access: anyone + inputs: + - to: address + - amount: uint256 + outputs: + - ok: bool + emits: [Transfer] + +transferFrom: + mutability: nonpayable + access: anyone + inputs: + - from: address + - to: address + - amount: uint256 + outputs: + - ok: bool + emits: [Transfer, Approval] + +events: + ReputationEarned: + indexed: + - staker: address + data: + - amount: uint256 + Staked: + indexed: + - staker: address + data: + - amount: uint256 + StakeWithdraw: + indexed: + - staker: address + data: + - amount: uint256 + Transfer: + indexed: + - from: address + - to: address + data: + - value: uint256 + Approval: + indexed: + - owner: address + - spender: address + data: + - value: uint256 + +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml new file mode 100644 index 00000000..37388ba0 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml @@ -0,0 +1,26 @@ +# Generated by scripts/selectors.mjs +functions: + getChainBlocksPerMonth(): 0x213b329e + stake(uint256): 0xa694fc3a + withdrawStake(uint256): 0x25d5971f + withdrawRewards(): 0xc7b8981c + setMonthlyRewards(uint256): 0xc76279a2 + getRewardsPerBlock(): 0x0c1cd7f3 + getProductivity(address): 0x28e964e9 + getUserPendingReward(address): 0xc6710629 + users(address): 0xa87430ba + totalRewardsPerShare(): 0xbf8e9b6e + decimals(): 0x313ce567 + totalSupply(): 0x18160ddd + balanceOf(address): 0x70a08231 + allowance(address,address): 0xdd62ed3e + approve(address,uint256): 0x095ea7b3 + transfer(address,uint256): 0xa9059cbb + transferFrom(address,address,uint256): 0x23b872dd +events: + ReputationEarned(address,uint256): 0x43848d0574703c28d68ae8958e0571521618f60c4bcacfb094cff2156eaae0f1 + Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d + StakeWithdraw(address,uint256): 0x1248d48e2de900a1010c7fce73506969ecec243600bfc08b641b158f26d857cd + Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml new file mode 100644 index 00000000..aea2efe9 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml @@ -0,0 +1,390 @@ +# IdentityV3 — whitelist, blacklist, DID, and connected-account graph for GoodDollar +# UBI eligibility uses getWhitelistedRoot; connectAccount links extra wallets to a root. + +meta: + name: IdentityV3 + version: v3 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV3.sol + inherits: + - DAOUpgradeableContract + - AccessControlUpgradeable + - PausableUpgradeable + - EIP712Upgradeable + note: > + isWhitelisted enforces authenticationPeriod against dateAuthenticated and status==1, + with fallback to oldIdentity. getWhitelistedRoot returns the root address for a + connected wallet or the wallet itself when directly whitelisted. + deployments: + mainnet: + production: + Identity: + networkId: 122 + address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" + creationBlock: 22022901 + IdentityOld: + networkId: 122 + address: "0xFa8d865A962ca8456dF331D78806152d3aC5B84F" + creationBlock: 6246324 + production-celo: + Identity: + networkId: 42220 + address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" + creationBlock: 17237952 + production-xdc: + Identity: + networkId: 50 + address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" + creationBlock: 95143058 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/identity + - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _owner: address + - _oldIdentity: address + outputs: [] + +initDAO: + notes: + - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." + mutability: nonpayable + access: DEFAULT_ADMIN_ROLE + inputs: + - _ns: address + outputs: [] + errors: [ALREADY_INITIALIZED] + +setAuthenticationPeriod: + mutability: nonpayable + access: avatar + inputs: + - period: uint256 + outputs: [] + errors: [WHEN_NOT_PAUSED_REVERT] + +authenticate: + notes: + - "Public wrapper; forwards to authenticateWithTimestamp(account, block.timestamp)." + mutability: nonpayable + inputs: + - account: address + outputs: [] + +authenticateWithTimestamp: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - timestamp: uint256 + outputs: [] + emits: [WhitelistedAuthenticated] + errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedAdded] + errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDIDAndChain: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + - orgChain: uint256 + - dateAuthenticated: uint256 + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDID: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +renounceWhitelisted: + mutability: nonpayable + access: whitelisted sender + inputs: [] + outputs: [] + emits: [WhitelistedRemoved] + errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] + +isWhitelisted: + notes: + - "Combines local Identity record with optional oldIdentity.isWhitelisted try/catch." + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +lastAuthenticated: + mutability: view + inputs: + - account: address + outputs: + - ts: uint256 + +addBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistAdded, WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +removeBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +addContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractAdded] + errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +isDAOContract: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +isBlacklisted: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +connectAccount: + notes: + - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." + mutability: nonpayable + access: whitelisted sender + inputs: + - account: address + outputs: [] + emits: [AccountConnected] + errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] + +disconnectAccount: + notes: + - "Caller must be either the linked root or the connected wallet." + mutability: nonpayable + inputs: + - connected: address + outputs: [] + emits: [AccountDisconnected] + errors: [UNAUTHORIZED_DISCONNECT] + +getWhitelistedRoot: + notes: + - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." + mutability: view + inputs: + - account: address + outputs: + - whitelisted: address + +pause: + mutability: nonpayable + access: PAUSER_ROLE + inputs: + - toPause: bool + outputs: [] + +setDID: + mutability: nonpayable + inputs: + - account: address + - did: string + outputs: [] + errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] + +addrToDID: + mutability: view + inputs: + - account: address + outputs: + - did: string + +getWhitelistedOnChainId: + mutability: view + inputs: + - account: address + outputs: + - chainId: uint256 + +isRegistered: + notes: + - "Compatibility shim; always returns true." + mutability: pure + inputs: [] + outputs: + - ok: bool + +IDENTITY_ADMIN_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +PAUSER_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +TYPED_STRUCTURE: + mutability: view + inputs: [] + outputs: + - schema: string + +whitelistedCount: + mutability: view + inputs: [] + outputs: + - n: uint256 + +whitelistedContracts: + mutability: view + inputs: [] + outputs: + - n: uint256 + +authenticationPeriod: + mutability: view + inputs: [] + outputs: + - seconds: uint256 + +identities: + mutability: view + inputs: + - account: address + outputs: + - dateAuthenticated: uint256 + - dateAdded: uint256 + - did: string + - whitelistedOnChainId: uint256 + - status: uint8 + +didHashToAddress: + mutability: view + inputs: + - hash: bytes32 + outputs: + - account: address + +connectedAccounts: + mutability: view + inputs: + - account: address + outputs: + - root: address + +oldIdentity: + mutability: view + inputs: [] + outputs: + - addr: address + +events: + BlacklistAdded: + indexed: + - account: address + data: [] + BlacklistRemoved: + indexed: + - account: address + data: [] + WhitelistedAdded: + indexed: + - account: address + data: [] + WhitelistedRemoved: + indexed: + - account: address + data: [] + WhitelistedAuthenticated: + indexed: + - account: address + data: + - timestamp: uint256 + ContractAdded: + indexed: + - account: address + data: [] + ContractRemoved: + indexed: + - account: address + data: [] + AccountConnected: + indexed: + - connected: address + - to: address + data: [] + AccountDisconnected: + indexed: + - disconnected: address + - from: address + data: [] + +errors: + ALREADY_INITIALIZED: "already initialized — initDAO twice." + NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." + NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." + NOT_WHITELISTED_FOR_DID: "not whitelisted — setDID internal." + ALREADY_HAS_STATUS: "already has status — _addWhitelisted." + DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." + DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." + NOT_CONTRACT: "Given address is not a contract — addContract." + INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." + ALREADY_CONNECTED: "already connected — connectAccount." + UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." + NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." + DID_EMPTY: "did empty — _setDID." + WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml new file mode 100644 index 00000000..9b53f001 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml @@ -0,0 +1,49 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,address): 0x485cc955 + initDAO(address): 0x2b14dda8 + setAuthenticationPeriod(uint256): 0xfff930d4 + authenticate(address): 0x08e0d29d + authenticateWithTimestamp(address,uint256): 0x96a1ef79 + addWhitelisted(address): 0x10154bad + addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a + addWhitelistedWithDID(address,string): 0x1b027099 + removeWhitelisted(address): 0x291d9549 + renounceWhitelisted(): 0xd6cd9473 + isWhitelisted(address): 0x3af32abf + lastAuthenticated(address): 0xe1e360ba + addBlacklisted(address): 0x188efc16 + removeBlacklisted(address): 0xc6a276c2 + addContract(address): 0x5f539d69 + removeContract(address): 0xc375c2ef + isDAOContract(address): 0xc73cc4ae + isBlacklisted(address): 0xfe575a87 + connectAccount(address): 0xd21685f4 + disconnectAccount(address): 0x37a1a987 + getWhitelistedRoot(address): 0x2d0e9b46 + pause(bool): 0x02329a29 + setDID(address,string): 0xd3262816 + addrToDID(address): 0x54f9f7a3 + getWhitelistedOnChainId(address): 0xa061922d + isRegistered(): 0x22366844 + IDENTITY_ADMIN_ROLE(): 0x1aaff63c + PAUSER_ROLE(): 0xe63ab1e9 + TYPED_STRUCTURE(): 0x2cec5330 + whitelistedCount(): 0xb2a1de22 + whitelistedContracts(): 0xb30f7e7f + authenticationPeriod(): 0x31b376e2 + identities(address): 0xf653b81e + didHashToAddress(bytes32): 0x67c75937 + connectedAccounts(address): 0x61320040 + oldIdentity(): 0x4125f0f2 +events: + BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 + BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 + WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f + WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 + WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 + ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 + ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 + AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c + AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml new file mode 100644 index 00000000..485254d7 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml @@ -0,0 +1,424 @@ +# IdentityV4 — whitelist, blacklist, DID, connected accounts, and reverification schedule +# isWhitelisted uses shouldReverify against reverifyDaysOptions and authCount; oldIdentity fallback preserved. + +meta: + name: IdentityV4 + version: v4 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV4.sol + inherits: + - DAOUpgradeableContract + - AccessControlUpgradeable + - PausableUpgradeable + - EIP712Upgradeable + note: > + authenticationPeriod() returns the largest reverify interval in days (last entry of reverifyDaysOptions), + not seconds. isWhitelisted is false when shouldReverify is true for the current elapsed days since + dateAuthenticated. Post-upgrade, accounts with dateAuthenticated before 1772697574 are treated as + having authCount at the last schedule step for reverify logic. + deployments: + mainnet: + production: + Identity: + networkId: 122 + address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" + creationBlock: 22022901 + production-celo: + Identity: + networkId: 42220 + address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" + creationBlock: 17237952 + production-xdc: + Identity: + networkId: 50 + address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" + creationBlock: 95143058 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/identity + - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _owner: address + - _oldIdentity: address + outputs: [] + +initDAO: + notes: + - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." + mutability: nonpayable + access: DEFAULT_ADMIN_ROLE + inputs: + - _ns: address + outputs: [] + errors: [ALREADY_INITIALIZED] + +setReverifyDaysOptions: + notes: + - "Replaces the full schedule; values must be strictly ascending uint8 days; non-empty array." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - options: uint8[] + outputs: [] + errors: [EMPTY_REVERIFY_OPTIONS, OPTIONS_NOT_ASCENDING, WHEN_NOT_PAUSED_REVERT] + +authenticate: + notes: + - "Forwards to authenticateWithTimestamp(account, block.timestamp); requires IDENTITY_ADMIN_ROLE on the inner call." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] + +authenticateWithTimestamp: + notes: + - "Refreshes authentication timestamp for status-1 account and advances authCount when reverification threshold is reached." + - "Legacy accounts before cutoff are evaluated at last schedule step for reverify math." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - timestamp: uint256 + outputs: [] + emits: [WhitelistedAuthenticated] + errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedAdded] + errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDIDAndChain: + notes: + - "Adds whitelisted account and stores DID plus originating chain id and authentication timestamp." + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + - orgChain: uint256 + - dateAuthenticated: uint256 + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +addWhitelistedWithDID: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + - did: string + outputs: [] + emits: [WhitelistedAdded] + errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeWhitelisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +renounceWhitelisted: + mutability: nonpayable + access: whitelisted sender + inputs: [] + outputs: [] + emits: [WhitelistedRemoved] + errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] + +shouldReverify: + notes: + - "Compares daysSinceAuth against current reverifyDaysOptions step selected by authCount." + mutability: view + inputs: + - account: address + - daysSinceAuth: uint256 + outputs: + - needed: bool + +isWhitelisted: + notes: + - "Local status 1 with shouldReverify false; else oldIdentity.isWhitelisted try/catch." + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +lastAuthenticated: + notes: + - "Returns local dateAuthenticated or falls back to oldIdentity when local record has no timestamp." + mutability: view + inputs: + - account: address + outputs: + - ts: uint256 + +addBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistAdded, WhitelistedRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +removeBlacklisted: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [BlacklistRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +addContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractAdded] + errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] + +removeContract: + mutability: nonpayable + access: IDENTITY_ADMIN_ROLE + inputs: + - account: address + outputs: [] + emits: [ContractRemoved] + errors: [WHEN_NOT_PAUSED_REVERT] + +isDAOContract: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +isBlacklisted: + mutability: view + inputs: + - account: address + outputs: + - ok: bool + +connectAccount: + notes: + - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." + mutability: nonpayable + access: whitelisted sender + inputs: + - account: address + outputs: [] + emits: [AccountConnected] + errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] + +disconnectAccount: + notes: + - "Caller must be either the linked root or the connected wallet." + mutability: nonpayable + inputs: + - connected: address + outputs: [] + emits: [AccountDisconnected] + errors: [UNAUTHORIZED_DISCONNECT] + +getWhitelistedRoot: + notes: + - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." + mutability: view + inputs: + - account: address + outputs: + - whitelisted: address + +pause: + mutability: nonpayable + access: PAUSER_ROLE + inputs: + - toPause: bool + outputs: [] + +setDID: + notes: + - "Sets DID for account by admin or account itself when whitelisted; enforces uniqueness across local and old identity." + mutability: nonpayable + inputs: + - account: address + - did: string + outputs: [] + errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] + +addrToDID: + mutability: view + inputs: + - account: address + outputs: + - did: string + +getWhitelistedOnChainId: + notes: + - "Returns chain id recorded during whitelisting for cross-chain eligibility checks." + mutability: view + inputs: + - account: address + outputs: + - chainId: uint256 + +isRegistered: + notes: + - "Compatibility shim; always returns true." + mutability: pure + inputs: [] + outputs: + - ok: bool + +IDENTITY_ADMIN_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +PAUSER_ROLE: + mutability: view + inputs: [] + outputs: + - role: bytes32 + +TYPED_STRUCTURE: + mutability: view + inputs: [] + outputs: + - schema: string + +whitelistedCount: + mutability: view + inputs: [] + outputs: + - n: uint256 + +whitelistedContracts: + mutability: view + inputs: [] + outputs: + - n: uint256 + +authenticationPeriod: + notes: + - "Returns reverifyDaysOptions[last] in days, not seconds." + mutability: view + inputs: [] + outputs: + - days: uint256 + +reverifyDaysOptions: + mutability: view + inputs: + - index: uint256 + outputs: + - days: uint32 + +identities: + mutability: view + inputs: + - account: address + outputs: + - dateAuthenticated: uint256 + - dateAdded: uint256 + - did: string + - whitelistedOnChainId: uint256 + - status: uint8 + - authCount: uint32 + +didHashToAddress: + mutability: view + inputs: + - hash: bytes32 + outputs: + - account: address + +connectedAccounts: + mutability: view + inputs: + - account: address + outputs: + - root: address + +oldIdentity: + mutability: view + inputs: [] + outputs: + - addr: address + +events: + BlacklistAdded: + indexed: + - account: address + data: [] + BlacklistRemoved: + indexed: + - account: address + data: [] + WhitelistedAdded: + indexed: + - account: address + data: [] + WhitelistedRemoved: + indexed: + - account: address + data: [] + WhitelistedAuthenticated: + indexed: + - account: address + data: + - timestamp: uint256 + ContractAdded: + indexed: + - account: address + data: [] + ContractRemoved: + indexed: + - account: address + data: [] + AccountConnected: + indexed: + - connected: address + - to: address + data: [] + AccountDisconnected: + indexed: + - disconnected: address + - from: address + data: [] + +errors: + ALREADY_INITIALIZED: "already initialized — initDAO twice." + EMPTY_REVERIFY_OPTIONS: "empty options — setReverifyDaysOptions." + OPTIONS_NOT_ASCENDING: "options not in ascending order — setReverifyDaysOptions." + NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." + NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." + NOT_WHITELISTED_FOR_DID: "not whitelisted — _setDID." + ALREADY_HAS_STATUS: "already has status — _addWhitelisted." + DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." + DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." + NOT_CONTRACT: "Given address is not a contract — addContract." + INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." + ALREADY_CONNECTED: "already connected — connectAccount." + UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." + NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." + DID_EMPTY: "did empty — _setDID." + WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml new file mode 100644 index 00000000..3440a5ec --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml @@ -0,0 +1,51 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,address): 0x485cc955 + initDAO(address): 0x2b14dda8 + setReverifyDaysOptions(uint8[]): 0x925e8c1a + authenticate(address): 0x08e0d29d + authenticateWithTimestamp(address,uint256): 0x96a1ef79 + addWhitelisted(address): 0x10154bad + addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a + addWhitelistedWithDID(address,string): 0x1b027099 + removeWhitelisted(address): 0x291d9549 + renounceWhitelisted(): 0xd6cd9473 + shouldReverify(address,uint256): 0x4a03813f + isWhitelisted(address): 0x3af32abf + lastAuthenticated(address): 0xe1e360ba + addBlacklisted(address): 0x188efc16 + removeBlacklisted(address): 0xc6a276c2 + addContract(address): 0x5f539d69 + removeContract(address): 0xc375c2ef + isDAOContract(address): 0xc73cc4ae + isBlacklisted(address): 0xfe575a87 + connectAccount(address): 0xd21685f4 + disconnectAccount(address): 0x37a1a987 + getWhitelistedRoot(address): 0x2d0e9b46 + pause(bool): 0x02329a29 + setDID(address,string): 0xd3262816 + addrToDID(address): 0x54f9f7a3 + getWhitelistedOnChainId(address): 0xa061922d + isRegistered(): 0x22366844 + IDENTITY_ADMIN_ROLE(): 0x1aaff63c + PAUSER_ROLE(): 0xe63ab1e9 + TYPED_STRUCTURE(): 0x2cec5330 + whitelistedCount(): 0xb2a1de22 + whitelistedContracts(): 0xb30f7e7f + authenticationPeriod(): 0x31b376e2 + reverifyDaysOptions(uint256): 0xcadef5ac + identities(address): 0xf653b81e + didHashToAddress(bytes32): 0x67c75937 + connectedAccounts(address): 0x61320040 + oldIdentity(): 0x4125f0f2 +events: + BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 + BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 + WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f + WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 + WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 + ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 + ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 + AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c + AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml new file mode 100644 index 00000000..d69e3d60 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml @@ -0,0 +1,333 @@ +# InvitesV2 — invite codes, inviter levels, and G$ bounties (UUPS upgradeable) +# Bounty amounts on Level use G$ cents (2 decimals). Invitee receives half the inviter bounty on payout. + +meta: + name: InvitesV2 + version: "2.4" + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/invite/InvitesV2.sol + inherits: + - DAOUpgradeableContract + note: > + join binds invite code and optional inviter; bounty eligibility uses minimumDays, minimumClaims, + whitelist on invitee (and inviter when present), and same-chain whitelisting via Identity + getWhitelistedOnChainId. collectBounties and bountyFor pay invitee bountyToPay/2 and inviter + bountyToPay when applicable. Campaign inviter is address(this) when inviter code maps to contract. + deployments: + mainnet: + production: + Invites: + networkId: 122 + address: "0xCa2F09c3ccFD7aD5cB9276918Bd1868f2b922ea0" + creationBlock: 8853311 + production-celo: + Invites: + networkId: 42220 + address: "0x36829D1Cda92FFF5782d5d48991620664FC857d3" + creationBlock: 18003063 + production-xdc: + Invites: + networkId: 50 + address: "0x6bd698566632bf2e81e2278f1656CB24aAF06D2e" + creationBlock: 95144756 + related: + - references/deep-researches/inviter-invitee-reward-model.md + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _ns: address + - _level0Bounty: uint256 + - _owner: address + outputs: [] + +updateAvatar: + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + +nativeToken: + mutability: view + inputs: [] + outputs: + - token: address + +dao: + mutability: view + inputs: [] + outputs: + - controller: address + +avatar: + mutability: view + inputs: [] + outputs: + - addr: address + +nameService: + mutability: view + inputs: [] + outputs: + - ns: address + +upgradeTo: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - newImplementation: address + outputs: [] + +upgradeToAndCall: + mutability: payable + access: ownerOrAvatar + inputs: + - newImplementation: address + - data: bytes + outputs: [] + +proxiableUUID: + mutability: view + inputs: [] + outputs: + - slot: bytes32 + +setLevelExpirationEnabled: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _isEnabled: bool + outputs: [] + +getIdentity: + mutability: view + inputs: [] + outputs: + - identity: address + +join: + notes: + - "Registers invite code; may set inviter; may trigger _bountyFor when canCollectBountyFor is already true." + mutability: nonpayable + access: anyone + inputs: + - _myCode: bytes32 + - _inviterCode: bytes32 + outputs: [] + emits: [InviteeJoined] + errors: [NOT_ACTIVE, INVITE_CODE_IN_USE, SELF_INVITE, USER_ALREADY_JOINED] + +canCollectBountyFor: + notes: + - "Primary eligibility gate: checks active state, whitelist, minimumDays, minimumClaims, unpaid bounty, and chain constraints." + mutability: view + inputs: + - _invitee: address + outputs: + - ok: bool + +getInvitees: + mutability: view + inputs: + - _inviter: address + outputs: + - invitees: address[] + +getPendingInvitees: + mutability: view + inputs: + - _inviter: address + outputs: + - pending: address[] + +getPendingBounties: + mutability: view + inputs: + - _inviter: address + outputs: + - count: uint256 + +bountyFor: + notes: + - "Single-invitee payout path; revalidates eligibility and transfers bounty shares to inviter/invitee on success." + mutability: nonpayable + access: anyone + inputs: + - _invitee: address + outputs: + - bounty: uint256 + emits: [InviterBounty] + errors: [NOT_ACTIVE, NOT_ELIGIBLE_BOUNTY] + +collectBounties: + notes: + - "Batch payout path over caller pending invitees; useful for claiming multiple matured bounties in one tx." + mutability: nonpayable + access: anyone + inputs: [] + outputs: [] + emits: [InviterBounty] + errors: [NOT_ACTIVE] + +setLevel: + notes: + - "Configures inviter level progression rule and bounty amount (bounty units are G$ cents)." + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _lvl: uint256 + - _toNext: uint256 + - _bounty: uint256 + - _daysToComplete: uint256 + outputs: [] + +setActive: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _active: bool + outputs: [] + +setCampaignCode: + notes: + - "Maps a campaign code to contract-address inviter flow (address(this)) used by join." + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _code: bytes32 + outputs: [] + +setMinimums: + notes: + - "Sets minimumClaims and minimumDays thresholds used by canCollectBountyFor." + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _minClaims: uint8 + - _minDays: uint8 + outputs: [] + +end: + mutability: nonpayable + access: ownerOrAvatar + inputs: [] + outputs: [] + errors: [NOT_ACTIVE] + +setOwner: + mutability: nonpayable + access: ownerOrAvatar + inputs: + - _owner: address + outputs: [] + +version: + mutability: pure + inputs: [] + outputs: + - v: string + +codeToUser: + mutability: view + inputs: + - code: bytes32 + outputs: + - user: address + +users: + notes: + - "Public getter omits dynamic invitees/pending arrays; use getInvitees and getPendingInvitees." + mutability: view + inputs: + - user: address + outputs: + - invitedBy: address + - inviteCode: bytes32 + - bountyPaid: bool + - level: uint256 + - levelStarted: uint256 + - totalApprovedInvites: uint256 + - totalEarned: uint256 + - joinedAt: uint256 + - bountyAtJoin: uint256 + +levels: + notes: + - "bounty is in G$ cents (2 decimals). Reserved struct slots are not exposed on the getter." + mutability: view + inputs: + - lvl: uint256 + outputs: + - toNext: uint256 + - bounty: uint256 + - daysToComplete: uint256 + +owner: + mutability: view + inputs: [] + outputs: + - addr: address + +goodDollar: + mutability: view + inputs: [] + outputs: + - token: address + +active: + mutability: view + inputs: [] + outputs: + - ok: bool + +stats: + notes: + - "Reserved Stats struct slots are not exposed on the getter." + mutability: view + inputs: [] + outputs: + - totalApprovedInvites: uint256 + - totalBountiesPaid: uint256 + - totalInvited: uint256 + +levelExpirationEnabled: + mutability: view + inputs: [] + outputs: + - ok: bool + +minimumClaims: + mutability: view + inputs: [] + outputs: + - n: uint8 + +minimumDays: + mutability: view + inputs: [] + outputs: + - n: uint8 + +events: + InviteeJoined: + indexed: + - inviter: address + - invitee: address + data: [] + InviterBounty: + indexed: + - inviter: address + - invitee: address + data: + - bountyPaid: uint256 + - inviterLevel: uint256 + - earnedLevel: bool + +errors: + NOT_ACTIVE: "not active — isActive modifier." + ONLY_OWNER_OR_AVATAR: "Only owner or avatar can perform this action — ownerOrAvatar." + INVITE_CODE_IN_USE: "invite code already in use — join." + SELF_INVITE: "self invite — join." + USER_ALREADY_JOINED: "user already joined — join." + NOT_ELIGIBLE_BOUNTY: "user not elligble for bounty yet — bountyFor." diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml new file mode 100644 index 00000000..d7179550 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml @@ -0,0 +1,41 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,uint256,address): 0xc350a1b5 + updateAvatar(): 0x1b3c90a8 + nativeToken(): 0xe1758bd8 + dao(): 0x4162169f + avatar(): 0x5aef7de6 + nameService(): 0x3e6326fc + upgradeTo(address): 0x3659cfe6 + upgradeToAndCall(address,bytes): 0x4f1ef286 + proxiableUUID(): 0x52d1902d + setLevelExpirationEnabled(bool): 0x21132aad + getIdentity(): 0x36afc6fa + join(bytes32,bytes32): 0x5b419a65 + canCollectBountyFor(address): 0x6d619ef8 + getInvitees(address): 0xe9881a5e + getPendingInvitees(address): 0xe951a3aa + getPendingBounties(address): 0x41155d5e + bountyFor(address): 0xb6567cd5 + collectBounties(): 0xaf6346b0 + setLevel(uint256,uint256,uint256,uint256): 0xb9fb2d18 + setActive(bool): 0xacec338a + setCampaignCode(bytes32): 0xf14b8649 + setMinimums(uint8,uint8): 0x47826c82 + end(): 0xefbe1c1c + setOwner(address): 0x13af4035 + version(): 0x54fd4d50 + codeToUser(bytes32): 0xba6f5680 + users(address): 0xa87430ba + levels(uint256): 0xb2596a67 + owner(): 0x8da5cb5b + goodDollar(): 0x119e5bf3 + active(): 0x02fb0c5e + stats(): 0xd80528ae + levelExpirationEnabled(): 0xa1df6fd3 + minimumClaims(): 0xc121cb92 + minimumDays(): 0xdf0dca04 +events: + InviteeJoined(address,address): 0xd8c638d8979e2ba5dba1f0d66246ee4b1c54b838f0e0a2b601365345eb23b051 + InviterBounty(address,address,uint256,uint256,bool): 0x6081787cd1bd02ab1576c52f03e8710d792d460e7881c3155d77d23893f3768b +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml new file mode 100644 index 00000000..f6f47f46 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml @@ -0,0 +1,259 @@ +# Mento Broker (IBroker + IBrokerAdmin) — swap router and trading-limit gate for GoodDollar Mento rails +# Interface definitions are vendored in GoodProtocol; implementation lives in mento-core. + +meta: + name: Broker + version: IBroker / IBrokerAdmin + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/MentoInterfaces.sol + - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/swap/Broker.sol + - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/interfaces/IBroker.sol + implements: [IBroker, IBrokerAdmin] + note: > + swapIn is exact-input; swapOut is exact-output. Quotes route through the + configured exchangeProvider + exchangeId. Reverts use require strings in + Broker.sol plus TradingLimits library checks ("amountOutMin not met", + "amountInMax exceeded", trading-limit errors). + deployments: + mainnet: + production-celo: + MentoBroker: + networkId: 42220 + address: "0x88de45906D4F5a57315c133620cfa484cB297541" + creationBlock: 31415857 + production-xdc: + MentoBroker: + networkId: 50 + address: "0x88de45906D4F5a57315c133620cfa484cB297541" + creationBlock: 100091095 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/mentobroker + - references/guides/swap.md + +initialize: + mutability: nonpayable + access: initializer + inputs: + - _exchangeProviders: address[] + - _reserves: address[] + outputs: [] + +setReserves: + notes: + - "Updates reserve address per already-listed provider without changing provider registration." + mutability: nonpayable + access: owner + inputs: + - _exchangeProviders: address[] + - _reserves: address[] + outputs: [] + emits: [ReserveSet] + errors: + - EXCHANGE_PROVIDER_MISSING + - RESERVE_ZERO + +addExchangeProvider: + mutability: nonpayable + access: owner + inputs: + - exchangeProvider: address + - reserve: address + outputs: + - index: uint256 + emits: [ExchangeProviderAdded, ReserveSet] + errors: + - PROVIDER_ALREADY_LISTED + - PROVIDER_ZERO + - RESERVE_ZERO + +removeExchangeProvider: + mutability: nonpayable + access: owner + inputs: + - exchangeProvider: address + - index: uint256 + outputs: [] + emits: [ExchangeProviderRemoved] + errors: + - INDEX_MISMATCH + +getAmountIn: + notes: + - "Exact-output quote path: returns required tokenIn for target amountOut." + - "Pre-checks collateral reserve balance when tokenOut is collateral." + mutability: view + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountOut: uint256 + outputs: + - amountIn: uint256 + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + +getAmountOut: + notes: + - "Exact-input quote path: returns expected tokenOut for fixed amountIn." + - "Pre-checks collateral reserve balance when tokenOut is collateral." + mutability: view + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountIn: uint256 + outputs: + - amountOut: uint256 + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + +swapIn: + notes: + - "Transfers tokenIn from msg.sender then delivers tokenOut; enforces amountOut >= amountOutMin after exchange + limits." + mutability: nonpayable + access: nonReentrant + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountIn: uint256 + - amountOutMin: uint256 + outputs: + - amountOut: uint256 + emits: [Swap] + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + - AMOUNT_OUT_MIN_NOT_MET + - TRADING_LIMIT_OR_SAFE_TRANSFER + +swapOut: + notes: + - "Computes required tokenIn then pulls up to amountInMax; reverts if amountIn would exceed cap." + mutability: nonpayable + access: nonReentrant + inputs: + - exchangeProvider: address + - exchangeId: bytes32 + - tokenIn: address + - tokenOut: address + - amountOut: uint256 + - amountInMax: uint256 + outputs: + - amountIn: uint256 + emits: [Swap] + errors: + - EXCHANGE_PROVIDER_MISSING + - INSUFFICIENT_RESERVE_BALANCE + - AMOUNT_IN_MAX_EXCEEDED + - TRADING_LIMIT_OR_SAFE_TRANSFER + +burnStableTokens: + notes: + - "Owner-maintained escape hatch burning stables from msg.sender via reserve plumbing." + mutability: nonpayable + access: owner + inputs: + - token: address + - amount: uint256 + outputs: + - ok: bool + errors: + - COLLATERAL_TRANSFER_FAILED + +configureTradingLimit: + notes: + - "Sets per (exchangeId, token) limit config used by swapIn/swapOut guardTradingLimits checks." + mutability: nonpayable + access: owner + inputs: + - exchangeId: bytes32 + - token: address + - config: TradingLimits.Config + outputs: [] + emits: [TradingLimitConfigured] + +getExchangeProviders: + notes: + - "Discovery helper for clients that need the active provider set before quoting/swapping." + mutability: view + inputs: [] + outputs: + - providers: address[] + +exchangeProviders: + mutability: view + inputs: + - index: uint256 + outputs: + - exchangeProvider: address + +isExchangeProvider: + mutability: view + inputs: + - exchangeProvider: address + outputs: + - ok: bool + +tradingLimitsState: + mutability: view + inputs: + - eid: bytes32 + outputs: + - state: TradingLimits.State + +tradingLimitsConfig: + mutability: view + inputs: + - eid: bytes32 + outputs: + - config: TradingLimits.Config + +events: + Swap: + indexed: + - exchangeId: bytes32 + - trader: address + - tokenIn: address + data: + - exchangeProvider: address + - tokenOut: address + - amountIn: uint256 + - amountOut: uint256 + TradingLimitConfigured: + indexed: [] + data: + - exchangeId: bytes32 + - token: address + - config: TradingLimits.Config + ExchangeProviderAdded: + indexed: + - exchangeProvider: address + data: [] + ExchangeProviderRemoved: + indexed: + - exchangeProvider: address + data: [] + ReserveSet: + indexed: + - newAddress: address + - prevAddress: address + data: [] + +errors: + EXCHANGE_PROVIDER_MISSING: "ExchangeProvider does not exist" + EXCHANGE_PROVIDER_EXISTS: "ExchangeProvider already exists in the list" + PROVIDER_ZERO: "ExchangeProvider address can't be 0" + RESERVE_ZERO: "Reserve address can't be 0" + INDEX_MISMATCH: "index doesn't match provider" + INSUFFICIENT_RESERVE_BALANCE: "Insufficient balance in reserve" + AMOUNT_OUT_MIN_NOT_MET: "amountOutMin not met" + AMOUNT_IN_MAX_EXCEEDED: "amountInMax exceeded" + COLLATERAL_TRANSFER_FAILED: "Transfer of the collateral asset failed" + AMOUNT_TOO_LARGE: "amountIn too large / amountOut too large — uint256 to int256 safety checks" + TRADING_LIMIT_OR_SAFE_TRANSFER: "TradingLimits library reverts or ERC20 safety failures during swap" diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml new file mode 100644 index 00000000..249edce4 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml @@ -0,0 +1,24 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address[],address[]): 0x73cf25f8 + setReserves(address[],address[]): 0xddeb9dd2 + addExchangeProvider(address,address): 0x770d0a34 + removeExchangeProvider(address,uint256): 0x04710d53 + getAmountIn(address,bytes32,address,address,uint256): 0x04e45640 + getAmountOut(address,bytes32,address,address,uint256): 0xa20f2305 + swapIn(address,bytes32,address,address,uint256,uint256): 0xddbbe850 + swapOut(address,bytes32,address,address,uint256,uint256): 0xd163b135 + burnStableTokens(address,uint256): 0x131cab2a + configureTradingLimit(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0xa868d140 + getExchangeProviders(): 0x2cac2568 + exchangeProviders(uint256): 0xc4454fdc + isExchangeProvider(address): 0xd1d786b1 + tradingLimitsState(bytes32): 0xf01ecd17 + tradingLimitsConfig(bytes32): 0x821a816c +events: + Swap(bytes32,address,address,address,address,uint256,uint256): 0x46e6aeaebfb8f1b6a9be6403b5fc420d9827172046f365e786acb9d5b56c9409 + TradingLimitConfigured(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0x3ed50aa123e1e547aa90fcf88624ef8278d253e252bb1feac75eda93e1905bb6 + ExchangeProviderAdded(address): 0x2ee2cb0721ec60b86190cae5c48e25064b69b35abad32452a4ec99d232033de2 + ExchangeProviderRemoved(address): 0x29e92ab2e30f4f74283034c28c451b6faac986b554f1808101eb6418bdba19d4 + ReserveSet(address,address): 0xb69e1c416d8be92ac92c8e97e77c4626fba5e6ab50161099f659ea3303479e50 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml new file mode 100644 index 00000000..d4bb1f2d --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml @@ -0,0 +1,542 @@ +meta: + name: MessagePassingBridge + version: Axelar + LayerZero message passing + source: + - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/MessagePassingBridge.sol + - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/IMessagePassingBridge.sol + note: > + Bridge implementation that burns source tokens and mints destination tokens, + transporting requests through Axelar or LayerZero. + Cross-chain transport uses msg.value on the source (LayerZero estimateSendFee path, Axelar gas prepay); destination mint applies bridgeFees bps to the delivered G$ amount. See references/guides/bridge.md "Bridge fee context". + deployments: + mainnet: + production: + MpbBridge: + networkId: 122 + address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" + creationBlock: 25464921 + production-celo: + MpbBridge: + networkId: 42220 + address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" + creationBlock: 21473545 + production-xdc: + MpbBridge: + networkId: 50 + address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" + creationBlock: 95254417 + related: + - references/guides/bridge.md + +initialize: + notes: + - "Initial bootstrap for DAO wiring and bridge policy state." + - "Sets guardian to msg.sender and uses NameService UBISCHEME as feeRecipient fallback to avatar when missing." + - "Preloads default LZ mappings for Ethereum, Celo, Fuse, and XDC using this contract as trusted remote." + mutability: nonpayable + access: initializer + inputs: + - nameService: address + - limits: (uint256,uint256,uint256,uint256,bool) + - fees: (uint256,uint256,uint256) + outputs: [] + +upgrade: + notes: + - "Reinitializer for migration path; re-applies default LZ mappings through internal add function." + - "Used when upgrading existing proxy deployments to mapping-aware version." + mutability: nonpayable + access: reinitializer + inputs: [] + outputs: [] + +addLzChainSupport: + notes: + - "Administrative chain wiring method for LayerZero transport." + - "Writes both forward (chainId->lzChainId) and reverse (lzChainId->chainId) mappings." + - "Also updates trustedRemoteLookup to enforce source-contract authenticity on receive." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - lzChainId: uint16 + - chainId: uint256 + - remote: address + outputs: [] + +approveRequest: + notes: + - "Manual override for stuck/exceptional inbound requests." + - "When approved, destination _bridgeFrom skips limits enforcement for that request id." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - id: uint256 + outputs: [] + +preventRequest: + notes: + - "Emergency/manual block for a specific request id." + - "Prevents execution by pre-marking executedRequests[id]=true." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - id: uint256 + outputs: [] + +setFeeRecipient: + notes: + - "Sets destination fee mint recipient." + - "If feeRecipient is zero, fee minting is skipped in _bridgeFrom." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - recipient: address + outputs: [] + +setBridgeLimits: + notes: + - "Updates operational throttles: min amount, tx cap, account/day cap, global/day cap, whitelist mode." + - "These limits are checked by canBridge and enforced in _enforceLimits." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - limits: (uint256,uint256,uint256,uint256,bool) + outputs: [] + +setBridgeFees: + notes: + - "Updates bridge fee parameters in basis points with min/max clamps." + - "Reverts when fee exceeds 10000 bps." + - "Effective fee deduction is computed in _takeFee during destination execution." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - fees: (uint256,uint256,uint256) + outputs: [] + +setDisabledBridges: + notes: + - "Toggles allowlist state for source bridge combinations." + - "Key format is keccak256(abi.encode(sourceChainId, BridgeService))." + - "Checked in _bridgeFrom before request execution." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - bridgeKeys: bytes32[] + - disabled: bool[] + outputs: [] + +setFaucet: + notes: + - "Configures optional faucet top-up callback used after successful destination processing." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - _faucet: address + outputs: [] + +setGuardian: + notes: + - "Rotates guardian authority used by owner/avatar/guardian admin gate." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - _guardian: address + outputs: [] + +withdraw: + notes: + - "Avatar-only rescue function for arbitrary token balances held by bridge." + - "Amount 0 means full balance withdrawal." + mutability: nonpayable + access: avatar + inputs: + - token: address + - amount: uint256 + outputs: [] + +pauseBridge: + notes: + - "Sets isClosed state for outbound flow." + - "When closed, _bridgeTo reverts with BRIDGE_LIMITS('closed')." + mutability: nonpayable + access: ownerOrAvatarOrGuardian + inputs: + - isPaused: bool + outputs: [] + +canBridge: + notes: + - "Purely diagnostic read path for preflight checks." + - "Returns (ok, reason) using BridgeHelperLibrary policy evaluation." + - "Includes close flag, whitelist gate, and per-account/global daily limit windows." + mutability: view + inputs: + - from: address + - amount: uint256 + outputs: + - isWithinLimit: bool + - error: string + +bridgeTo: + notes: + - "Generic outbound bridge entrypoint selecting BridgeService.AXELAR or BridgeService.LZ." + - "Burns source-side token amount, normalizes to 18 decimals, and emits BridgeRequest with request id." + - "Requires msg.value for transport fee payment." + mutability: payable + access: anyone + inputs: + - target: address + - targetChainId: uint256 + - amount: uint256 + - bridge: uint8 + outputs: [] + +bridgeToWithLz: + notes: + - "Outbound convenience wrapper fixed to LayerZero transport." + - "If adapterParams is empty, contract injects default type-1 options with 400k gas." + - "Compares required native fee from estimateSendFee against msg.value and reverts on underpayment." + mutability: payable + access: anyone + inputs: + - target: address + - targetChainId: uint256 + - amount: uint256 + - adapterParams: bytes + outputs: [] + +bridgeToWithAxelar: + notes: + - "Outbound convenience wrapper fixed to Axelar transport." + - "Uses gasRefundAddress when provided, otherwise msg.sender as refund recipient." + mutability: payable + access: anyone + inputs: + - target: address + - targetChainId: uint256 + - amount: uint256 + - gasRefundAddress: address + outputs: [] + +toLzChainId: + notes: + - "Lookup helper from local chain id to LayerZero chain id." + mutability: view + inputs: + - chainId: uint256 + outputs: + - lzChainId: uint16 + +fromLzChainId: + notes: + - "Lookup helper from LayerZero chain id to local chain id." + mutability: view + inputs: + - lzChainId: uint16 + outputs: + - chainId: uint256 + +toAxelarChainId: + notes: + - "Pure mapping in current implementation: chainId 1 -> 'Ethereum', 5 -> 'ethereum-2', 42220 and 44787 -> 'celo'; any other id returns empty string and outbound Axelar reverts UNSUPPORTED_CHAIN." + mutability: pure + inputs: + - chainId: uint256 + outputs: + - axlChainId: string + +fromAxelarChainId: + notes: + - "Maps Axelar chain-name strings back into local numeric chain ids." + mutability: pure + inputs: + - axlChainId: string + outputs: + - chainId: uint256 + +estimateSendFee: + notes: + - "Transport fee preflight for LZ path." + - "Builds payload with request id set to 0 for estimation." + - "Returns (0,0) on endpoint estimation failure in handler implementation." + - "_normalizedAmount must match BridgeHelperLibrary.normalizeFromTokenTo18Decimals burned raw amount using native token decimals on the outbound chain." + mutability: view + inputs: + - _dstChainId: uint16 + - _fromAddress: address + - _toAddress: address + - _normalizedAmount: uint256 + - _useZro: bool + - _adapterParams: bytes + outputs: + - nativeFee: uint256 + - zroFee: uint256 + +lzReceive: + notes: + - "LayerZero receive entrypoint." + - "Validates caller is configured endpoint before dispatching nonblocking receive pipeline." + mutability: nonpayable + inputs: + - _srcChainId: uint16 + - _srcAddress: bytes + - _nonce: uint64 + - _payload: bytes + outputs: [] + +lzEndpoint_: + notes: + - "Immutable LayerZero endpoint configured in constructor." + mutability: view + inputs: [] + outputs: + - endpoint: address + +HOME_CHAIN_ID: + notes: + - "Immutable deployment home chain marker." + mutability: view + inputs: [] + outputs: + - chainId: uint32 + +guardian: + notes: + - "Current guardian account authorized for admin operations." + mutability: view + inputs: [] + outputs: + - account: address + +executedRequests: + notes: + - "Replay protection state for inbound request ids." + mutability: view + inputs: + - id: uint256 + outputs: + - done: bool + +disabledSourceBridges: + notes: + - "Per-source bridge toggle state keyed by (sourceChainId, bridgeService) hash." + mutability: view + inputs: + - key: bytes32 + outputs: + - disabled: bool + +isClosed: + notes: + - "Outbound bridge pause state." + mutability: view + inputs: [] + outputs: + - closed: bool + +bridgeFees: + notes: + - "Current fee policy parameters: minFee, maxFee, feeBps." + mutability: view + inputs: [] + outputs: + - minFee: uint256 + - maxFee: uint256 + - fee: uint256 + +bridgeLimits: + notes: + - "Current bridge traffic limits and whitelist policy." + mutability: view + inputs: [] + outputs: + - dailyLimit: uint256 + - txLimit: uint256 + - accountDailyLimit: uint256 + - minAmount: uint256 + - onlyWhitelisted: bool + +bridgeDailyLimit: + notes: + - "Aggregate 24h bridge usage tracker." + mutability: view + inputs: [] + outputs: + - lastTransferReset: uint256 + - bridged24Hours: uint256 + +accountsDailyLimit: + notes: + - "Per-account 24h usage tracker." + mutability: view + inputs: + - account: address + outputs: + - lastTransferReset: uint256 + - bridged24Hours: uint256 + +faucet: + notes: + - "Optional gas top-up contract used on destination completion." + mutability: view + inputs: [] + outputs: + - addr: address + +currentId: + notes: + - "Monotonic nonce component used to derive outbound request ids." + mutability: view + inputs: [] + outputs: + - id: uint256 + +lzChainIdsMapping: + notes: + - "Local chain id -> LayerZero chain id mapping." + mutability: view + inputs: + - chainId: uint256 + outputs: + - lzChainId: uint16 + +feeRecipient: + notes: + - "Address receiving bridge fee mints on destination." + mutability: view + inputs: [] + outputs: + - recipient: address + +approvedRequests: + notes: + - "Manual approvals allowing request execution above standard limits." + mutability: view + inputs: + - id: uint256 + outputs: + - approved: bool + +lzChainToIdsMapping: + notes: + - "LayerZero chain id -> local chain id mapping." + mutability: view + inputs: + - lzChainId: uint16 + outputs: + - chainId: uint256 + +trustedRemoteLookup: + notes: + - "Trusted remote path used to authenticate inbound LZ/Axelar source contract." + mutability: view + inputs: + - lzChainId: uint16 + outputs: + - path: bytes + +gateway: + notes: + - "Axelar gateway address inherited from AxelarExecutable." + mutability: view + inputs: [] + outputs: + - addr: address + +gasService: + notes: + - "Axelar gas service used to prepay cross-chain execution." + mutability: view + inputs: [] + outputs: + - addr: address + +nameService: + notes: + - "DAO NameService pointer from DAOUpgradeableContract." + mutability: view + inputs: [] + outputs: + - addr: address + +avatar: + notes: + - "DAO avatar authority address." + mutability: view + inputs: [] + outputs: + - addr: address + +owner: + notes: + - "Ownable owner from upgradeable ownership flow." + mutability: view + inputs: [] + outputs: + - account: address + +events: + BridgeRequest: + indexed: + - from: address + - to: address + - id: uint256 + data: + - targetChainId: uint256 + - normalizedAmount: uint256 + - timestamp: uint256 + - bridge: uint8 + note: "Outbound intent event; relayers consume this to deliver payload on destination." + ExecutedTransfer: + indexed: + - from: address + - to: address + - id: uint256 + data: + - normalizedAmount: uint256 + - fee: uint256 + - sourceChainId: uint256 + - bridge: uint8 + note: "Destination completion event after replay checks, source-auth checks, limit checks, and mint/fee mint." + FalseSender: + indexed: [] + data: + - sourceChainId: uint256 + - sourceAddress: address + note: "Inbound message ignored because source contract does not match trusted remote path." + +errors: + AlreadyInitialized: "LZ chain support already initialized for this lz chain id." + NOT_GUARDIAN: + inputs: + - sender: address + WRONG_TOKEN: + inputs: + - token: address + INVALID_TARGET_OR_CHAINID: + inputs: + - target: address + - chainId: uint256 + BRIDGE_LIMITS: + inputs: + - reason: string + TRANSFER_FROM: "Source burn/transferFrom style failure." + TRANSFER: "Destination transfer style failure." + ALREADY_EXECUTED: + inputs: + - requestId: uint256 + MISSING_FEE: "Bridge call requires msg.value for transport fee." + UNSUPPORTED_CHAIN: + inputs: + - chainId: uint256 + LZ_FEE: + inputs: + - required: uint256 + - sent: uint256 + INVALID_SENDER: + inputs: + - _srcAddress: bytes + INVALID_ENDPOINT: + inputs: + - lzEndpoint: address diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml new file mode 100644 index 00000000..104b9435 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml @@ -0,0 +1,52 @@ +# Generated by scripts/selectors.mjs +functions: + initialize(address,(uint256,uint256,uint256,uint256,bool),(uint256,uint256,uint256)): 0xfad16abe + upgrade(): 0xd55ec697 + addLzChainSupport(uint16,uint256,address): 0xc01c8123 + approveRequest(uint256): 0xd7d1bbdb + preventRequest(uint256): 0xc612f9ea + setFeeRecipient(address): 0xe74b981b + setBridgeLimits((uint256,uint256,uint256,uint256,bool)): 0x29b70872 + setBridgeFees((uint256,uint256,uint256)): 0x82dc737d + setDisabledBridges(bytes32[],bool[]): 0xc78ab882 + setFaucet(address): 0xd8b31c77 + setGuardian(address): 0x8a0dac4a + withdraw(address,uint256): 0xf3fef3a3 + pauseBridge(bool): 0x1a394795 + canBridge(address,uint256): 0x3095634a + bridgeTo(address,uint256,uint256,uint8): 0x1fec5c5c + bridgeToWithLz(address,uint256,uint256,bytes): 0xc56bbdd9 + bridgeToWithAxelar(address,uint256,uint256,address): 0x40a00aaf + toLzChainId(uint256): 0x5b23a990 + fromLzChainId(uint16): 0x16ad5512 + toAxelarChainId(uint256): 0x9a9ee081 + fromAxelarChainId(string): 0x56026f37 + estimateSendFee(uint16,address,address,uint256,bool,bytes): 0x05fead15 + lzReceive(uint16,bytes,uint64,bytes): 0x001d3567 + lzEndpoint_(): 0x020051cf + HOME_CHAIN_ID(): 0x8f65be85 + guardian(): 0x452a9320 + executedRequests(uint256): 0x425cfb53 + disabledSourceBridges(bytes32): 0x69c20d36 + isClosed(): 0xc2b6b58c + bridgeFees(): 0x7b0240c0 + bridgeLimits(): 0xc6dd812f + bridgeDailyLimit(): 0xb2f7667f + accountsDailyLimit(address): 0xd4227947 + faucet(): 0xde5f72fd + currentId(): 0xe00dd161 + lzChainIdsMapping(uint256): 0xb5569f18 + feeRecipient(): 0x46904840 + approvedRequests(uint256): 0x0dce292e + lzChainToIdsMapping(uint16): 0x73800fc4 + trustedRemoteLookup(uint16): 0x7533d788 + gateway(): 0x116191b6 + gasService(): 0x6a22d8cc + nameService(): 0x3e6326fc + avatar(): 0x5aef7de6 + owner(): 0x8da5cb5b +events: + BridgeRequest(address,address,uint256,uint256,uint256,uint256,uint8): 0x34b675c8c84b6a9e7979a0d3a54f2b036a19d6444167091a925af2d81e8e66fe + ExecutedTransfer(address,address,uint256,uint256,uint256,uint256,uint8): 0x81e772e0c4366ddbae472926005267ef278dbb257be45dfe97c676ceae348dbe + FalseSender(uint256,address): 0x1eafb58197ea0dc76b9278ccad47f61a239f00b017d89a19081a89321cce213d +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml b/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml new file mode 100644 index 00000000..01692b7a --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml @@ -0,0 +1,104 @@ +# NameService — string-key to address registry for GoodProtocol deployments +# Resolves keys such as IDENTITY, UBISCHEME, GOODDOLLAR, GDAO_STAKING. + +meta: + name: NameService + version: UUPS-upgradeable v1 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/NameService.sol + implements: [] + inherits: [Initializable, UUPSUpgradeable] + note: > + Almost every integration starts here. Writes are restricted to the DAO + avatar. Reads are permissionless. Use getAddress(string) with the same + key strings your deployment documented in releases/deployment.json. + deployments: + mainnet: + production: + NameService: + networkId: 122 + address: "0xec6dcE387B1616a0c44fF2E4fA9E90e53Cf14eb0" + creationBlock: 15740314 + production-celo: + NameService: + networkId: 42220 + address: "0x0F5dB7a64A6a64052693676CA898EC7F7A94FF4e" + creationBlock: 17237962 + production-xdc: + NameService: + networkId: 50 + address: "0x1e5154Bf5e31FF56051bbd45958b879Fb7a290FE" + creationBlock: 95143608 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/nameservice + - https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json + +addresses: + # Solidity auto-getter for public mapping addresses(bytes32 => address). + notes: + - "Prefer getAddress(string) off-chain; this expects keccak256(abi.encodePacked(name)) for manual lookups." + mutability: view + inputs: + - nameHash: bytes32 + outputs: + - addr: address + +dao: + mutability: view + inputs: [] + outputs: + - controller: address + +initialize: + notes: + - "Initializer wires Controller and optional pre-hashed name batch." + - "Automatically registers CONTROLLER and AVATAR hashes to dao and dao.avatar()." + mutability: nonpayable + access: initializer + inputs: + - _dao: address + - _nameHashes: bytes32[] + - _addresses: address[] + outputs: [] + +setAddress: + notes: + - "Stores addresses[keccak256(bytes(name))] = addr and emits AddressChanged." + mutability: nonpayable + access: avatar + inputs: + - name: string + - addr: address + outputs: [] + emits: [AddressChanged] + errors: [ONLY_AVATAR] + +setAddresses: + notes: + - "Batch write by raw bytes32 keys; no per-name event emission in this loop." + mutability: nonpayable + access: avatar + inputs: + - hash: bytes32[] + - addrs: address[] + outputs: [] + errors: [ONLY_AVATAR] + +getAddress: + notes: + - "Primary lookup used by claim, staking, identity, and swap integrations." + mutability: view + inputs: + - name: string + outputs: + - addr: address + +events: + AddressChanged: + indexed: [] + data: + - name: string + - addr: address + +errors: + ONLY_AVATAR: "only avatar can call this method — revert string on any avatar-gated path including UUPS _authorizeUpgrade." diff --git a/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml b/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml new file mode 100644 index 00000000..48e1bcfa --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml @@ -0,0 +1,11 @@ +# Generated by scripts/selectors.mjs +functions: + addresses(bytes32): 0x699f200f + dao(): 0x4162169f + initialize(address,bytes32[],address[]): 0xd41b4e72 + setAddress(string,address): 0x9b2ea4bd + setAddresses(bytes32[],address[]): 0x4ab01f5b + getAddress(string): 0xbf40fac1 +events: + AddressChanged(string,address): 0x135cf55549d8538a41f19f46cc85625da93e68b63484cca8fcb9aaf19e520137 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml new file mode 100644 index 00000000..abfc91cb --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml @@ -0,0 +1,1033 @@ +# SuperToken — the Superfluid Protocol's token standard +# Merges three variants into a single ABI surface: +# • Wrapper Super Token — wraps an existing ERC-20 (upgrade / downgrade) +# • Native Asset Super Token (SETH) — wraps chain-native asset (upgradeByETH / downgradeToETH) +# • Pure Super Token — pre-minted, no underlying token (upgrade/downgrade revert) +# +# Inherits ERC-20, ERC-777, ERC-2612 (permit), EIP-5267, and the Superfluid real-time +# balance and agreement hosting layer from SuperfluidToken. +# +# Proxy / upgradability functions are omitted: +# castrate, getCodeAddress, proxiableUUID, initialize, initializeWithAdmin, updateCode +# Proxy-related events are also omitted: CodeUpdated, Initialized. + +meta: + name: SuperToken + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/IYieldBackend.sol + inherits: [SuperfluidToken, ISuperToken, UUPSProxiable] + yield_backends: + - name: AaveYieldBackend + note: "Production backend for ERC-20 wrapper tokens using Aave V3" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveYieldBackend.sol + - name: AaveETHYieldBackend + note: "Variant for native-asset (ETH/WETH) wrapper tokens using Aave V3" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveETHYieldBackend.sol + - name: ERC4626YieldBackend + note: "Generic backend for any ERC-4626 vault" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/ERC4626YieldBackend.sol + - name: SparkYieldBackend + note: "Extends ERC4626YieldBackend for Spark Protocol vaults with referral tracking" + source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SparkYieldBackend.sol + variants: + wrapper: "Standard ERC-20 wrapper — upgrade() / downgrade()" + native-asset: "Native asset wrapper (SETH) — upgradeByETH() / downgradeToETH()" + pure: "Pre-minted supply, no underlying — upgrade/downgrade revert" + note: > + Super Tokens are deployed as individual proxies (one per token) by the + SuperTokenFactory. There is no single canonical deployment address. + All proxies share the same logic contract which is upgraded via governance. + +# wrapper super token — a Super Token backed by an ERC-20 underlying token +# native asset super token (SETH) — a Super Token backed by the chain-native asset (ETH, MATIC, etc.) +# pure super token — a Super Token with no underlying; supply is pre-minted at deploy time +# underlying token — the ERC-20 token that a wrapper super token wraps +# real-time balance — balance that changes continuously based on active agreements (CFA/GDA) +# available balance — real-time balance minus locked deposits +# critical — an account whose available balance is negative +# solvent — an account whose real-time balance (before deposit deductions) is non-negative +# operator (ERC-777) — an address authorized to send/burn tokens on behalf of a holder +# buffer / deposit — collateral locked while an outgoing flow is active + +# == ERC-20 Token Info == + +name: + mutability: view + inputs: [] + outputs: + - string + +symbol: + mutability: view + inputs: [] + outputs: + - string + +decimals: + # Always returns 18 regardless of underlying token decimals. + notes: + - "Always returns 18, regardless of the underlying token's decimals (e.g. USDC has 6). The upgrade/downgrade functions handle decimal conversion internally." + mutability: pure + inputs: [] + outputs: + - uint8 + +totalSupply: + mutability: view + inputs: [] + outputs: + - uint256 + +# == ERC-20 Balance & Allowance == + +balanceOf: + # Returns max(0, availableBalance) — clamps negative balances to zero. + # For the full real-time picture, use realtimeBalanceOfNow. + notes: + - "Returns max(0, availableBalance) — negative/critical balances are clamped to zero. Use realtimeBalanceOfNow to detect critical accounts." + mutability: view + inputs: + - account: address + outputs: + - balance: uint256 + +allowance: + mutability: view + inputs: + - account: address + - spender: address + outputs: + - uint256 + +approve: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - amount: uint256 + outputs: + - bool + emits: [Approval] + errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +increaseAllowance: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - addedValue: uint256 + outputs: + - bool + emits: [Approval] + errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +decreaseAllowance: + mutability: nonpayable + access: anyone + inputs: + - spender: address + - subtractedValue: uint256 + outputs: + - bool + emits: [Approval] + errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +# == ERC-20 Transfers == + +transfer: + # ERC-20 transfer. Does NOT invoke ERC-777 send/receive hooks (by design). + mutability: nonpayable + access: anyone + inputs: + - recipient: address + - amount: uint256 + outputs: + - bool + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +transferFrom: + # ERC-20 transferFrom. Does NOT invoke ERC-777 send/receive hooks. + # Does NOT emit an Approval event on allowance spend (OZ v5 behaviour). + mutability: nonpayable + access: anyone + inputs: + - holder: address + - recipient: address + - amount: uint256 + outputs: + - bool + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +transferAll: + # Transfer the caller's entire balanceOf to recipient. + mutability: nonpayable + access: anyone # transfers msg.sender's entire balance + inputs: + - recipient: address + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +# == ERC-777 == + +granularity: + # Always returns 1. + mutability: pure + inputs: [] + outputs: + - uint256 + +send: + # ERC-777 send — invokes tokensToSend / tokensReceived hooks. + mutability: nonpayable + access: anyone + inputs: + - recipient: address + - amount: uint256 + - userData: bytes + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] + +burn: + # ERC-777 burn — in SuperToken this actually performs a downgrade (unwrap). + notes: + - "Gotcha: Reverts with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure Super Tokens." + mutability: nonpayable + access: anyone + inputs: + - amount: uint256 + - userData: bytes + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] + +authorizeOperator: + mutability: nonpayable + access: anyone + inputs: + - operator: address + emits: [AuthorizedOperator] + +revokeOperator: + mutability: nonpayable + access: anyone + inputs: + - operator: address + emits: [RevokedOperator] + +isOperatorFor: + mutability: view + inputs: + - operator: address + - tokenHolder: address + outputs: + - bool + +defaultOperators: + mutability: view + inputs: [] + outputs: + - "address[]" + +operatorSend: + # ERC-777 operatorSend — invokes tokensToSend / tokensReceived hooks. + mutability: nonpayable + access: operator + inputs: + - sender: address + - recipient: address + - amount: uint256 + - userData: bytes + - operatorData: bytes + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] + +operatorBurn: + # ERC-777 operatorBurn — like burn(), actually performs a downgrade. + mutability: nonpayable + access: operator + inputs: + - account: address + - amount: uint256 + - userData: bytes + - operatorData: bytes + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] + +# == ERC-20 Permit == + +permit: + mutability: nonpayable + access: anyone + inputs: + - owner: address + - spender: address + - value: uint256 + - deadline: uint256 + - v: uint8 + - r: bytes32 + - s: bytes32 + emits: [Approval] + errors: [SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE, SUPER_TOKEN_PERMIT_INVALID_SIGNER, ECDSAInvalidSignature, ECDSAInvalidSignatureLength, ECDSAInvalidSignatureS, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +nonces: + mutability: view + inputs: + - owner: address + outputs: + - uint256 + +DOMAIN_SEPARATOR: + mutability: view + inputs: [] + outputs: + - bytes32 + +eip712Domain: + # EIP-5267 domain retrieval. + mutability: view + inputs: [] + outputs: + - fields: bytes1 + - name: string + - version: string + - chainId: uint256 + - verifyingContract: address + - salt: bytes32 + - "extensions: uint256[]" + +# == ERC-20 Wrapping == +# These functions interact with the underlying ERC-20 token. +# They revert with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure and Native Asset +# Super Tokens (use upgradeByETH / downgradeToETH for SETH). + +upgrade: + # Wrap underlying ERC-20 into Super Tokens for msg.sender. + # Requires prior ERC-20 approval on the underlying token. + notes: + - "Gotcha: amount is always in SuperToken decimals (18), regardless of the underlying token's decimals. The contract handles downscaling internally when pulling from the underlying via transferFrom. However, the ERC-20 approval on the underlying token must use the underlying's native decimals. Example: to wrap 1000 USDC, approve 1000e6 on USDC, then call upgrade(1000e18)." + mutability: nonpayable + access: anyone # wraps for msg.sender + inputs: + - amount: uint256 # in SuperToken decimals (always 18) + emits: [Minted, Transfer, TokenUpgraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +upgradeTo: + # Wrap underlying ERC-20 and mint Super Tokens to a different address. + mutability: nonpayable + access: anyone + inputs: + - to: address + - amount: uint256 + - userData: bytes + emits: [Minted, Transfer, TokenUpgraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +downgrade: + # Unwrap Super Tokens back to underlying ERC-20 for msg.sender. + mutability: nonpayable + access: anyone # unwraps to msg.sender + inputs: + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +downgradeTo: + # Unwrap Super Tokens and send underlying ERC-20 to a different address. + mutability: nonpayable + access: anyone + inputs: + - to: address + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +getUnderlyingToken: + # Returns address(0) for Pure and Native Asset Super Tokens. + mutability: view + inputs: [] + outputs: + - address + +getUnderlyingDecimals: + mutability: view + inputs: [] + outputs: + - uint8 + +toUnderlyingAmount: + # Convert a Super Token amount to its underlying equivalent, accounting for + # decimal differences. adjustedAmount strips precision loss. + mutability: view + inputs: + - amount: uint256 + outputs: + - underlyingAmount: uint256 + - adjustedAmount: uint256 + +# == Native Asset Wrapping (SETH only) == +# These functions exist only on Native Asset Super Token (SETH) proxies. +# They are defined in the SETHProxy contract, not in the SuperToken logic. +# The SETH proxy also accepts plain ETH transfers via receive() which mints +# Super Tokens to the sender. + +upgradeByETH: + # Wrap sent ETH/native asset into Super Tokens for msg.sender. + # Variant: native-asset + mutability: payable + access: anyone # wraps for msg.sender + inputs: [] + emits: [Minted, Transfer, TokenUpgraded] + +upgradeByETHTo: + # Wrap sent ETH/native asset into Super Tokens for a different address. + # Variant: native-asset + mutability: payable + access: anyone + inputs: + - to: address + emits: [Minted, Transfer, TokenUpgraded] + +downgradeToETH: + # Unwrap Super Tokens back to ETH/native asset for msg.sender. + # Variant: native-asset + mutability: nonpayable + access: anyone # unwraps to msg.sender + inputs: + - wad: uint256 + emits: [Burned, Transfer, TokenDowngraded] + +# == Real-time Balance == + +realtimeBalanceOf: + # Full real-time balance across ALL agreements at a given timestamp. + # This aggregates the realtimeBalanceOf from every agreement (CFA, GDA, IDA) + # into a single balance. Use this (or realtimeBalanceOfNow) to check an + # account's actual balance — not the per-agreement versions. + # availableBalance = settled + agreement dynamics − max(0, deposit − owedDeposit) + mutability: view + inputs: + - account: address + - timestamp: uint256 + outputs: + - availableBalance: int256 + - deposit: uint256 + - owedDeposit: uint256 + +realtimeBalanceOfNow: + # Convenience wrapper: realtimeBalanceOf at the current Host timestamp. + # This is the recommended way to check an account's real-time balance. + mutability: view + inputs: + - account: address + outputs: + - availableBalance: int256 + - deposit: uint256 + - owedDeposit: uint256 + - timestamp: uint256 + +isAccountCritical: + # True if availableBalance < 0 at the given timestamp. + mutability: view + inputs: + - account: address + - timestamp: uint256 + outputs: + - isCritical: bool + +isAccountCriticalNow: + mutability: view + inputs: + - account: address + outputs: + - isCritical: bool + +isAccountSolvent: + # True if realtime balance (before deposit deduction) >= 0 at the given timestamp. + mutability: view + inputs: + - account: address + - timestamp: uint256 + outputs: + - isSolvent: bool + +isAccountSolventNow: + mutability: view + inputs: + - account: address + outputs: + - isSolvent: bool + +# == Admin == +# The admin defaults to the Host contract when address(0). +# An explicit admin can be set during initializeWithAdmin or via changeAdmin. + +changeAdmin: + mutability: nonpayable + access: admin + inputs: + - newAdmin: address + emits: [AdminChanged] + errors: [SUPER_TOKEN_ONLY_ADMIN] + +getAdmin: + # Returns address(0) if the Host is the implicit admin. + mutability: view + inputs: [] + outputs: + - address + +# == Yield Backend == +# Hot-pluggable yield generation for wrapper Super Tokens. The yield backend is a +# contract implementing IYieldBackend that is invoked via delegatecall. When enabled, +# the underlying ERC-20 is deposited into a yield protocol (e.g. Aave, Spark, ERC4626 vaults). +# Surplus yield (balance exceeding totalSupply) can be withdrawn by the admin. + +enableYieldBackend: + # Enable a yield backend and deposit all underlying token balance into it. + # Requires no existing yield backend to be set. + notes: + - "Gotcha: Reverts if a yield backend is already enabled — must call disableYieldBackend first to switch backends." + - "The yield backend contract is invoked via delegatecall — it executes in the SuperToken's context." + mutability: nonpayable + access: admin + inputs: + - newYieldBackend: address # must implement IYieldBackend + emits: [YieldBackendEnabled] + +disableYieldBackend: + # Withdraw all assets from the yield backend and disable it. + notes: + - "Calls withdrawMax() then disable() on the backend via delegatecall." + mutability: nonpayable + access: admin + inputs: [] + emits: [YieldBackendDisabled] + +getYieldBackend: + # Returns the current yield backend address, or address(0) if none is set. + mutability: view + inputs: [] + outputs: + - yieldBackend: address + +withdrawSurplusFromYieldBackend: + # Withdraw yield surplus (deposited amount exceeding totalSupply) from the backend. + # The surplus is sent to a receiver defined by the yield backend implementation. + mutability: nonpayable + access: admin + inputs: [] + +# == Self Operations == +# Can only be called by the token contract itself (address(this)). +# Used by custom super token proxies (SETH, Pure) to mint/burn/approve/transfer +# tokens through their own proxy logic. + +selfMint: + mutability: nonpayable + access: self + inputs: + - account: address + - amount: uint256 + - userData: bytes + emits: [Minted, Transfer] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS] + +selfBurn: + mutability: nonpayable + access: self + inputs: + - account: address + - amount: uint256 + - userData: bytes + emits: [Burned, Transfer] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE] + +selfApproveFor: + mutability: nonpayable + access: self + inputs: + - account: address + - spender: address + - amount: uint256 + emits: [Approval] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +selfTransferFrom: + mutability: nonpayable + access: self + inputs: + - holder: address + - spender: address + - recipient: address + - amount: uint256 + emits: [Sent, Transfer] + errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +# == Host Batch Operations == +# Called by the Host contract on behalf of a user within batchCall / +# forwardBatchCall. Each mirrors a user-facing function but takes the +# account address explicitly. + +operationApprove: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - amount: uint256 + emits: [Approval] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +operationIncreaseAllowance: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - addedValue: uint256 + emits: [Approval] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +operationDecreaseAllowance: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - subtractedValue: uint256 + emits: [Approval] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] + +operationTransferFrom: + mutability: nonpayable + access: host + inputs: + - account: address + - spender: address + - recipient: address + - amount: uint256 + emits: [Sent, Transfer] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] + +operationSend: + mutability: nonpayable + access: host + inputs: + - spender: address + - recipient: address + - amount: uint256 + - userData: bytes + emits: [Sent, Transfer] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] + +operationUpgrade: + mutability: nonpayable + access: host + inputs: + - account: address + - amount: uint256 + emits: [Minted, Transfer, TokenUpgraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +operationDowngrade: + mutability: nonpayable + access: host + inputs: + - account: address + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +operationUpgradeTo: + mutability: nonpayable + access: host + inputs: + - account: address + - to: address + - amount: uint256 + emits: [Minted, Transfer, TokenUpgraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] + +operationDowngradeTo: + mutability: nonpayable + access: host + inputs: + - account: address + - to: address + - amount: uint256 + emits: [Burned, Transfer, TokenDowngraded] + errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] + +# == Agreement Hosting == +# The token acts as storage host for agreement data. These functions are +# called by registered agreement contracts (CFA, GDA, IDA) through the Host. + +createAgreement: + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - "data: bytes32[]" + emits: [AgreementCreated] + errors: [SF_TOKEN_AGREEMENT_ALREADY_EXISTS] + +getAgreementData: + mutability: view + inputs: + - agreementClass: address + - id: bytes32 + - dataLength: uint256 + outputs: + - "data: bytes32[]" + +updateAgreementData: + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - "data: bytes32[]" + emits: [AgreementUpdated] + +terminateAgreement: + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - dataLength: uint256 + emits: [AgreementTerminated] + errors: [SF_TOKEN_AGREEMENT_DOES_NOT_EXIST] + +updateAgreementStateSlot: + mutability: nonpayable + access: agreement + inputs: + - account: address + - slotId: uint256 + - "slotData: bytes32[]" + emits: [AgreementStateUpdated] + +getAgreementStateSlot: + mutability: view + inputs: + - agreementClass: address + - account: address + - slotId: uint256 + - dataLength: uint256 + outputs: + - "slotData: bytes32[]" + +settleBalance: + # Adjust an account's settled balance. Only listed agreements can call this. + mutability: nonpayable + access: agreement + inputs: + - account: address + - delta: int256 + errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] + +makeLiquidationPayoutsV2: + # Execute liquidation payouts. Transfers reward to liquidator or reward account, + # and adjusts the target account's balance. Only listed agreements can call this. + mutability: nonpayable + access: agreement + inputs: + - id: bytes32 + - liquidationTypeData: bytes + - liquidatorAccount: address + - useDefaultRewardAccount: bool + - targetAccount: address + - rewardAmount: uint256 + - targetAccountBalanceDelta: int256 + emits: [Transfer, AgreementLiquidatedV2] + errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] + +getAccountActiveAgreements: + mutability: view + inputs: + - account: address + outputs: + - "ISuperAgreement[]" + +# == Protocol Info == + +getHost: + mutability: view + inputs: [] + outputs: + - host: address + +POOL_ADMIN_NFT: + # Immutable address of the canonical PoolAdminNFT proxy. + mutability: view + inputs: [] + outputs: + - address + +POOL_MEMBER_NFT: + # Immutable address of the (deprecated) PoolMemberNFT proxy. + mutability: view + inputs: [] + outputs: + - address + +VERSION: + # Returns the version string of the SuperToken logic contract (e.g. "1.0.0"). + mutability: view + inputs: [] + outputs: + - string + +# == Events == + +events: + + # -- ERC-20 -- + + Transfer: + indexed: + - from: address + - to: address + data: + - value: uint256 + + Approval: + indexed: + - owner: address + - spender: address + data: + - value: uint256 + + # -- ERC-777 -- + + Sent: + indexed: + - operator: address + - from: address + - to: address + data: + - amount: uint256 + - data: bytes + - operatorData: bytes + + Minted: + indexed: + - operator: address + - to: address + data: + - amount: uint256 + - data: bytes + - operatorData: bytes + + Burned: + indexed: + - operator: address + - from: address + data: + - amount: uint256 + - data: bytes + - operatorData: bytes + + AuthorizedOperator: + indexed: + - operator: address + - tokenHolder: address + + RevokedOperator: + indexed: + - operator: address + - tokenHolder: address + + # -- EIP-5267 -- + + EIP712DomainChanged: + # Signalled when the EIP-712 domain changes. Not explicitly emitted by + # SuperToken, but declared in the EIP-5267 interface for indexing support. + + # -- Wrapping -- + + TokenUpgraded: + indexed: + - account: address + data: + - amount: uint256 + + TokenDowngraded: + indexed: + - account: address + data: + - amount: uint256 + + # -- Admin -- + + AdminChanged: + indexed: [] + data: + - oldAdmin: address + - newAdmin: address + + # -- Yield Backend -- + + YieldBackendEnabled: + indexed: + - yieldBackend: address + data: + - depositAmount: uint256 + + YieldBackendDisabled: + indexed: + - yieldBackend: address + + PoolAdminNFTCreated: + # Emitted once during logic contract constructor deployment. + indexed: [] + data: + - poolAdminNFT: address + + # -- Agreement Hosting -- + + AgreementCreated: + indexed: + - agreementClass: address + - id: bytes32 + data: + - "data: bytes32[]" + + AgreementUpdated: + indexed: + - agreementClass: address + - id: bytes32 + data: + - "data: bytes32[]" + + AgreementTerminated: + indexed: + - agreementClass: address + - id: bytes32 + + AgreementStateUpdated: + indexed: + - agreementClass: address + - account: address + data: + - slotId: uint256 + + AgreementLiquidatedV2: + indexed: + - agreementClass: address + - id: bytes32 + data: + - liquidatorAccount: address + - targetAccount: address + - rewardAmountReceiver: address + - rewardAmount: uint256 + - targetAccountBalanceDelta: int256 + - liquidationTypeData: bytes + + # -- Deprecated events (from ISuperfluidToken, no longer emitted) -- + + AgreementLiquidated: + deprecated: true + note: "Use AgreementLiquidatedV2" + indexed: + - agreementClass: address + - id: bytes32 + data: + - penaltyAccount: address + - rewardAccount: address + - rewardAmount: uint256 + + AgreementLiquidatedBy: + deprecated: true + note: "Use AgreementLiquidatedV2" + indexed: + - liquidatorAccount: address + - agreementClass: address + - id: bytes32 + data: + - penaltyAccount: address + - bondAccount: address + - rewardAmount: uint256 + - bailoutAmount: uint256 + + Bailout: + deprecated: true + note: "Use AgreementLiquidatedV2" + indexed: + - bailoutAccount: address + - bailoutAmount: uint256 + +# == Errors == + +errors: + + # -- Super Token access -- + - SUPER_TOKEN_ONLY_SELF + - SUPER_TOKEN_ONLY_ADMIN + - SUPER_TOKEN_ONLY_GOV_OWNER + + # -- SuperfluidToken access -- + - SF_TOKEN_ONLY_HOST + - SF_TOKEN_ONLY_LISTED_AGREEMENT + + # -- Wrapping -- + - SUPER_TOKEN_NO_UNDERLYING_TOKEN + - SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED + + # -- ERC-20 / ERC-777 transfer -- + - SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS + - SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS + - SF_TOKEN_MOVE_INSUFFICIENT_BALANCE + + # -- Approve -- + - SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS + - SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS + + # -- Mint / Burn -- + - SUPER_TOKEN_MINT_TO_ZERO_ADDRESS + - SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS + - SF_TOKEN_BURN_INSUFFICIENT_BALANCE + + # -- ERC-777 operator -- + - SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER + - SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT + + # -- ERC-2612 permit -- + - SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE: + inputs: + - deadline: uint256 + - SUPER_TOKEN_PERMIT_INVALID_SIGNER: + inputs: + - signer: address + - owner: address + + # -- NFT proxy -- + - SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED + + # -- Agreement hosting -- + - SF_TOKEN_AGREEMENT_ALREADY_EXISTS + - SF_TOKEN_AGREEMENT_DOES_NOT_EXIST + + # -- OpenZeppelin ECDSA (from permit) -- + - ECDSAInvalidSignature + - ECDSAInvalidSignatureLength: + inputs: + - length: uint256 + - ECDSAInvalidSignatureS: + inputs: + - s: bytes32 + + # -- OpenZeppelin SafeCast -- + - SafeCastOverflowedUintToInt: + inputs: + - value: uint256 + + # -- OpenZeppelin SafeERC20 -- + - SafeERC20FailedOperation: + inputs: + - token: address diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml new file mode 100644 index 00000000..f9bb474d --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml @@ -0,0 +1,99 @@ +# Generated by scripts/selectors.mjs +functions: + name(): 0x06fdde03 + symbol(): 0x95d89b41 + decimals(): 0x313ce567 + totalSupply(): 0x18160ddd + balanceOf(address): 0x70a08231 + allowance(address,address): 0xdd62ed3e + approve(address,uint256): 0x095ea7b3 + increaseAllowance(address,uint256): 0x39509351 + decreaseAllowance(address,uint256): 0xa457c2d7 + transfer(address,uint256): 0xa9059cbb + transferFrom(address,address,uint256): 0x23b872dd + transferAll(address): 0xa3a7e7f3 + granularity(): 0x556f0dc7 + send(address,uint256,bytes): 0x9bd9bbc6 + burn(uint256,bytes): 0xfe9d9303 + authorizeOperator(address): 0x959b8c3f + revokeOperator(address): 0xfad8b32a + isOperatorFor(address,address): 0xd95b6371 + defaultOperators(): 0x06e48538 + operatorSend(address,address,uint256,bytes,bytes): 0x62ad1b83 + operatorBurn(address,uint256,bytes,bytes): 0xfc673c4f + permit(address,address,uint256,uint256,uint8,bytes32,bytes32): 0xd505accf + nonces(address): 0x7ecebe00 + DOMAIN_SEPARATOR(): 0x3644e515 + eip712Domain(): 0x84b0196e + upgrade(): 0xd55ec697 + upgradeTo(address,uint256,bytes): 0x5b9d09cc + downgrade(uint256): 0x11bcc81e + downgradeTo(address,uint256): 0x83ba2525 + getUnderlyingToken(): 0xee719bc8 + getUnderlyingDecimals(): 0x92081a47 + toUnderlyingAmount(uint256): 0x282a050b + upgradeByETH(): 0xcf81464b + upgradeByETHTo(address): 0x7687d19b + downgradeToETH(uint256): 0x160e8be3 + realtimeBalanceOf(address,uint256): 0xeb3537cc + realtimeBalanceOfNow(address): 0x2ec8eec7 + isAccountCritical(address,uint256): 0xd9d078d6 + isAccountCriticalNow(address): 0x79359f6f + isAccountSolvent(address,uint256): 0xb84cdd4a + isAccountSolventNow(address): 0xbb0d196e + changeAdmin(address): 0x8f283970 + getAdmin(): 0x6e9960c3 + enableYieldBackend(): 0x5127b621 + disableYieldBackend(): 0x370a190f + getYieldBackend(): 0xe729804b + withdrawSurplusFromYieldBackend(): 0x4a3acda0 + selfMint(address,uint256,bytes): 0xc68d4283 + selfBurn(address,uint256,bytes): 0x9d876741 + selfApproveFor(address,address,uint256): 0x66a12fb6 + selfTransferFrom(address,address,address,uint256): 0x41b706be + operationApprove(address,address,uint256): 0x62aa5287 + operationIncreaseAllowance(address,address,uint256): 0x4b2763b3 + operationDecreaseAllowance(address,address,uint256): 0xc780fd82 + operationTransferFrom(address,address,address,uint256): 0x16d055d6 + operationSend(address,address,uint256,bytes): 0xca0c1e7f + operationUpgrade(address,uint256): 0xca789464 + operationDowngrade(address,uint256): 0x245887fc + operationUpgradeTo(address,address,uint256): 0x1ae88ffc + operationDowngradeTo(address,address,uint256): 0x47ba7ad1 + createAgreement(bytes32,bytes32[]"): 0x816e2dc5 + getAgreementData(address,bytes32,uint256): 0x6c2d9f2f + updateAgreementData(bytes32,bytes32[]"): 0x1bced9c7 + terminateAgreement(bytes32,uint256): 0x27048397 + updateAgreementStateSlot(address,uint256,bytes32[]"): 0x003af740 + getAgreementStateSlot(address,address,uint256,uint256): 0x4b61cc33 + settleBalance(address,int256): 0xcf97256d + makeLiquidationPayoutsV2(bytes32,bytes,address,bool,address,uint256,int256): 0x1863e809 + getAccountActiveAgreements(address): 0x386fa221 + getHost(): 0x20bc4425 + POOL_ADMIN_NFT(): 0xb20db1ac + POOL_MEMBER_NFT(): 0xf5a8b4dd + VERSION(): 0xffa1ad74 +events: + Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + Sent(address,address,address,uint256,bytes,bytes): 0x06b541ddaa720db2b10a4d0cdac39b8d360425fc073085fac19bc82614677987 + Minted(address,address,uint256,bytes,bytes): 0x2fe5be0146f74c5bce36c0b80911af6c7d86ff27e89d5cfa61fc681327954e5d + Burned(address,address,uint256,bytes,bytes): 0xa78a9be3a7b862d26933ad85fb11d80ef66b8f972d7cbba06621d583943a4098 + AuthorizedOperator(address,address): 0xf4caeb2d6ca8932a215a353d0703c326ec2d81fc68170f320eb2ab49e9df61f9 + RevokedOperator(address,address): 0x50546e66e5f44d728365dc3908c63bc5cfeeab470722c1677e3073a6ac294aa1 + EIP712DomainChanged(): 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31 + TokenUpgraded(address,uint256): 0x25ca84076773b0455db53621c459ddc84fe40840e4932a62706a032566f399df + TokenDowngraded(address,uint256): 0x3bc27981aebbb57f9247dc00fde9d6cd91e4b230083fec3238fedbcba1f9ab3d + AdminChanged(address,address): 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f + YieldBackendEnabled(address,uint256): 0x8d15dd569157df615aedb4b16273001ae8980ce2aac93e39adc48481dfaefbb5 + YieldBackendDisabled(address): 0x2486b5241145ce1b97a13655ecd4e8e51094ac93259a0589d24524304d6d70d4 + PoolAdminNFTCreated(address): 0xeb87fb34067547f3dc0b85096c3da73c99d4fbb08ff41212b8d7c0b5008b42e6 + AgreementCreated(address,bytes32,bytes32[]"): 0x9a5caaf4460c6dff81ed3dc58f3aef40fe511737a163340e06a4abc1bfe21a73 + AgreementUpdated(address,bytes32,bytes32[]"): 0xc00aaaf56818c1a9d6c54e14c6f93965eb34c9729232fdafa62f90a10600e70a + AgreementTerminated(address,bytes32): 0x71a63dc095de07aa5512ad57a7596a39516317e316981a1cd71000057be1537b + AgreementStateUpdated(address,address,uint256): 0x30f416fa68fca014a0f334464c64b000ba53e99b6d2afdea9d5ca756372d5985 + AgreementLiquidatedV2(address,bytes32,address,address,address,uint256,int256,bytes): 0xb8381a3ce157650e06186e3e8f4dd4dc29236f2688b6eed1893d0a60d7c6386f + AgreementLiquidated(address,bytes32,address,address,uint256): 0x8505c3d8f1f184f032cf0bc4cd80ee61c8b9d94f8907c3281bf0101a2610fe80 + AgreementLiquidatedBy(address,address,bytes32,address,address,uint256,uint256): 0x5f22b60e58b1d6de858bc27c48d5a4653e052da99e083c1d88bb8c58e1abc8ef + Bailout(address,uint256): 0xd6c9a04afc81e8c614310bbee6c9e84f5abe15b82038bf8347014ce0852e6ffd +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml new file mode 100644 index 00000000..33ac5a94 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml @@ -0,0 +1,757 @@ +# Superfluid Host — the central protocol router +# All agreement calls (CFA, IDA, GDA) are routed through the Host, which manages +# the Superfluid call context, Super App callbacks, app credit, and governance. +# The Host is also the entry point for batch calls and meta-transactions. +# +# NOTE: emits/errors mappings are traced from source code — verify against implementation. +# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID, initialize) +# are omitted — they belong to the UUPSProxiable layer. + +meta: + name: Superfluid + version: v1 + source: + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol + - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol + implements: [ISuperfluid] + inherits: [UUPSProxiable, BaseRelayRecipient] + deployments: + mainnet: + eth-mainnet: "0x4E583d9390082B65Bef884b629DFA426114CED6d" + polygon-mainnet: "0x3E14dC1b13c488a8d5D310918780c983bD5982E7" + xdai-mainnet: "0x2dFe937cD98Ab92e59cF3139138f18c823a4efE7" + base-mainnet: "0x4C073B3baB6d8826b8C5b229f3cfdC1eC6E47E74" + optimism-mainnet: "0x567c4B141ED61923967cA25Ef4906C8781069a10" + arbitrum-one: "0xCf8Acb4eF033efF16E8080aed4c7D5B9285D2192" + bsc-mainnet: "0xd1e2cFb6441680002Eb7A44223160aB9B67d7E6E" + avalanche-c: "0x60377C7016E4cdB03C87EF474896C11cB560752C" + celo-mainnet: "0xA4Ff07cF81C02CFD356184879D953970cA957585" + scroll-mainnet: "0x0F86a21F6216c061B222c224e315d9FC34520bb7" + degenchain: "0xc1314EdcD7e478C831a7a24169F7dEADB2646eD2" + testnet: + avalanche-fuji: "0x85Fe79b998509B77BF10A8BD4001D58475D29386" + base-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" + eth-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" + optimism-sepolia: "0xd399e2Fb5f4cf3722a11F65b88FAB6B2B8621005" + scroll-sepolia: "0x42b05a6016B9eED232E13fd56a8F0725693DBF8e" + deploymentCreationBlocks: + mainnet: + celo-mainnet: 16393469 + +# == Abbreviations == +# ctx — context (Superfluid call context bytes) +# ACL — Access Control List +# ERC — Ethereum Request for Comments + +# == Glossary == +# Host — central Superfluid contract routing agreement/app calls +# agreement class — a registered agreement contract (CFA, IDA, GDA) +# Super App — a smart contract registered with the Host that receives callbacks +# callback — beforeAgreement* / afterAgreement* hooks called on Super Apps +# context (ctx) — encoded call metadata: msgSender, timestamp, userData, app credit +# app credit — temporary token allowance granted to a Super App during a callback +# jailed — a Super App that violated protocol rules; its callbacks are bypassed +# configWord — bitmask encoding a Super App's app level and callback noop flags. +# App level: APP_LEVEL_FINAL (1 << 1) or APP_LEVEL_SECOND (1 << 0). +# Noop flags disable specific before/after callbacks per agreement type. +# batch operation — a single step in a batchCall array (see BatchOperation in Definitions.sol) +# trusted forwarder — a contract allowed to call forwardBatchCall for meta-transactions + +# == Batch Call == +# Primary entry point for interacting with the protocol. Users compose arrays of +# typed operations (ERC-20 approvals, SuperToken wrapping, agreement calls, etc.). +# +# == Batch Operation Types == +# Used in the operationType field of batchCall/forwardBatchCall operations. +# Data encoding varies by category — see notes on each type. +# +# Category 1–5: ERC-20/ERC-777 token ops +# target: must be a SuperToken (routes through SuperToken.operation* methods, +# NOT arbitrary ERC-20 contracts — use a separate tx for underlying approvals) +# data: abi-encoded parameters only (strip the 4-byte function selector) +# +# Category 101–102: SuperToken wrap/unwrap +# target: the SuperToken +# data: abi-encoded parameters only (strip the 4-byte function selector) +# +# Category 201: agreement call +# target: the agreement contract (e.g. CFA, GDA) +# data: abi.encode(callData, userData) where callData is full +# encodeFunctionData (selector included, with empty ctx placeholder "0x") +# +# Category 202: app action +# target: the Super App +# data: abi.encode(callData) where callData is full encodeFunctionData +# (selector included, with empty ctx placeholder "0x") +# +# Category 301: simple forward +# target: any contract +# data: full encodeFunctionData (selector included), or "0x" for pure value transfers +# +# Category 302: ERC-2771 forward +# target: any contract (must be an ERC-2771 recipient to extract the real sender) +# data: full encodeFunctionData (selector included) +# The ERC2771Forwarder appends the original msg.sender to calldata per ERC-2771. +# Use when the target contract needs to know who initiated the batch. + +batch_operation_types: + ERC20_APPROVE: 1 + ERC20_TRANSFER_FROM: 2 + ERC777_SEND: 3 # deprecated — ERC-777 is being phased out + ERC20_INCREASE_ALLOWANCE: 4 + ERC20_DECREASE_ALLOWANCE: 5 + SUPERTOKEN_UPGRADE: 101 + SUPERTOKEN_DOWNGRADE: 102 + SUPERFLUID_CALL_AGREEMENT: 201 + CALL_APP_ACTION: 202 + SIMPLE_FORWARD_CALL: 301 + ERC2771_FORWARD_CALL: 302 + +batchCall: + # Execute an array of operations in a single transaction as msg.sender. + notes: + - "Gotcha: For SIMPLE_FORWARD_CALL (type 301), target sees msg.sender as the SimpleForwarder contract, NOT the original caller. Use only when the target doesn't need to identify the sender." + - "Gotcha: ERC-20 ops (types 1-5) route through SuperToken.operation* methods, NOT arbitrary ERC-20 contracts. The target must be a SuperToken address. To approve an underlying ERC-20 (e.g. for wrapping), use a separate transaction." + - "Gotcha: If msg.value > 0, the entire amount is forwarded to the first CALL_APP_ACTION, SIMPLE_FORWARD, or ERC2771_FORWARD operation. If none exist, the native tokens are returned to the sender." + mutability: payable + access: anyone + inputs: + - operations: tuple[] + components: + - operationType: uint32 # see batch_operation_types above + - target: address + - data: bytes # encoding varies by operationType — see above + errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] + +forwardBatchCall: + # Same as batchCall but uses EIP-2771 to extract msgSender from calldata. + # Can only be called by contracts registered as trusted forwarders in governance. + # If native tokens are left over, they are refunded to the EIP-2771 sender. + mutability: payable + access: trusted-forwarder + inputs: + - operations: tuple[] + components: + - operationType: uint32 # see batch_operation_types above + - target: address + - data: bytes # encoding varies by operationType — see above + errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] + +# == Contextless Call Proxies == +# Entry points for EOAs and non-app contracts to interact with agreements or apps. +# These create a fresh Superfluid context with msg.sender, inject it into the call, +# and route through to the target agreement/app. + +callAgreement: + # Call an agreement function. The Host creates a context with msg.sender and replaces + # the placeholder ctx bytes in callData. This is how EOAs interact with CFA/IDA/GDA. + mutability: nonpayable + access: anyone # context set to msg.sender + inputs: + - agreementClass: address # must be a registered agreement + - callData: bytes # abi-encoded agreement call with placeholder ctx + - userData: bytes # extra data forwarded to Super App callbacks + outputs: + - returnedData: bytes + errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT] + +callAppAction: + # Call an app action function on a Super App. The Host creates a context and routes + # the call. The action must not be an agreement callback selector. + mutability: nonpayable + access: anyone # context set to msg.sender + inputs: + - app: address # must be a registered, non-jailed Super App + - callData: bytes # abi-encoded app action call with placeholder ctx + outputs: + - returnedData: bytes + errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION] + +# == Contextual Call Proxies == +# For Super Apps to chain calls during callbacks. The app must pass the ctx it received. +# Only callable by the app currently executing a callback (ctx.appAddress == msg.sender). + +callAgreementWithContext: + # Chain an agreement call from within a Super App callback. + # The ctx must be valid and msg.sender must be the current callback app. + mutability: nonpayable + access: super-app + inputs: + - agreementClass: address + - callData: bytes + - userData: bytes + - ctx: bytes + outputs: + - newCtx: bytes + - returnedData: bytes + errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT, HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS] + +callAppActionWithContext: + # Chain an app action call from within a Super App callback. + mutability: nonpayable + access: super-app + inputs: + - app: address + - callData: bytes + - ctx: bytes + outputs: + - newCtx: bytes + errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION, HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS] + +# == Context Utilities == + +decodeCtx: + # Decode a raw context bytes blob into the structured Context fields. + mutability: pure + inputs: + - ctx: bytes + outputs: + - context: + type: tuple + components: + - appCallbackLevel: uint8 + - callType: uint8 + - timestamp: uint256 + - msgSender: address + - agreementSelector: bytes4 + - userData: bytes + - appCreditGranted: uint256 + - appCreditWantedDeprecated: uint256 + - appCreditUsed: int256 + - appAddress: address + - appCreditToken: address + +isCtxValid: + # Check if a context blob matches the current transaction's context stamp. + mutability: view + inputs: + - ctx: bytes + outputs: + - bool + +# == App Registry == +# Super App registration. On some deployments, governance permission (via SimpleACL +# or legacy governance config) is required before an app can register. + +registerApp: + # Register msg.sender as a Super App with the given config word. + # On whitelisting-enabled deployments, tx.origin must have the + # ACL_SUPERAPP_REGISTRATION_ROLE or a valid legacy governance key. + mutability: nonpayable + access: self + inputs: + - configWord: uint256 # Super App manifest flags (see SuperAppDefinitions) + emits: [AppRegistered] + errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +registerApp(address,uint256): + # Register an external contract as a Super App. + # Useful for factory patterns where the deployer registers the app. + mutability: nonpayable + access: anyone + inputs: + - app: address # must be a contract + - configWord: uint256 + emits: [AppRegistered] + errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +registerAppWithKey: + # DEPRECATED — use registerApp(uint256) instead. + # Legacy registration using a governance-provided registration key string. + mutability: nonpayable + access: self + inputs: + - configWord: uint256 + - registrationKey: string + emits: [AppRegistered] + errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +registerAppByFactory: + # DEPRECATED — use registerApp(address, uint256) instead. + # Legacy factory-based registration using governance-authorized factory addresses. + mutability: nonpayable + access: factory + inputs: + - app: address + - configWord: uint256 + emits: [AppRegistered] + errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] + +# == App Queries == + +isApp: + mutability: view + inputs: + - app: address + outputs: + - bool + +getAppCallbackLevel: + mutability: view + inputs: + - appAddr: address + outputs: + - uint8 + +getAppManifest: + # Returns the full manifest of a Super App. + # isSuperApp is false if the address was never registered. + mutability: view + inputs: + - app: address + outputs: + - isSuperApp: bool + - isJailed: bool + - noopMask: uint256 # bitmask of noop'd agreement callback selectors + +isAppJailed: + mutability: view + inputs: + - app: address + outputs: + - isJail: bool + +# == App Composition == +# Source apps can whitelist target apps for composability (calling downstream). +# Currently limited to MAX_APP_CALLBACK_LEVEL = 1 (one level of nesting). + +allowCompositeApp: + # Whitelist a target Super App for composition. msg.sender must be a Super App + # with a higher callback level than the target. + mutability: nonpayable + access: super-app + inputs: + - targetApp: address + errors: [HOST_SENDER_IS_NOT_SUPER_APP, HOST_RECEIVER_IS_NOT_SUPER_APP, HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL] + +isCompositeAppAllowed: + mutability: view + inputs: + - app: address + - targetApp: address + outputs: + - isAppAllowed: bool + +# == Agreement Framework == +# Internal protocol functions called by registered agreement contracts (CFA, IDA, GDA) +# to manage Super App callbacks and app credit. Only callable by agreements. +# +# Callback lifecycle (called by agreement during flow operations): +# 1. appCallbackPush — set up credit, push callback frame +# 2. callAppBeforeCallback — staticcall app's before hook (read-only) +# 3. (agreement executes core logic) +# 4. callAppAfterCallback — call app's after hook (state changes OK) +# 5. appCallbackPop — finalize credit accounting, pop frame + +callAppBeforeCallback: + # StaticCall a Super App's before-callback. Returns the callback data. + # If the callback reverts during a termination, the app is jailed instead of reverting. + mutability: nonpayable + access: agreement + inputs: + - app: address + - callData: bytes + - isTermination: bool + - ctx: bytes + outputs: + - cbdata: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] + +callAppAfterCallback: + # Call a Super App's after-callback. Returns the (potentially modified) context. + # If the app returns an invalid ctx, it is jailed on termination or reverts otherwise. + mutability: nonpayable + access: agreement + inputs: + - app: address + - callData: bytes + - isTermination: bool + - ctx: bytes + outputs: + - newCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] + +appCallbackPush: + # Push a new callback frame onto the context stack before invoking a Super App. + # Sets up app credit and tracks the callback nesting level. + notes: + - "Gotcha: The appCreditGranted is backed by the flow sender's deposit as \"owed deposit\". If the Super App opens outgoing streams using this credit, the sender's total locked capital increases accordingly (roughly doubles for 1:1 relay, more for fan-out patterns)." + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - app: address + - appCreditGranted: uint256 + - appCreditUsed: int256 + - appCreditToken: address + outputs: + - appCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT, APP_RULE] + +appCallbackPop: + # Pop the callback frame after a Super App callback completes. + # Adjusts appCreditUsed by the delta from the callback. + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - appCreditUsedDelta: int256 + outputs: + - newCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT] + +ctxUseCredit: + # Record additional app credit usage during an agreement operation. + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - appCreditUsedMore: int256 + outputs: + - newCtx: bytes + errors: [HOST_ONLY_LISTED_AGREEMENT] + +jailApp: + # Jail a Super App from within an agreement. Jailed apps have their callbacks bypassed. + mutability: nonpayable + access: agreement + inputs: + - ctx: bytes + - app: address + - reason: uint256 # see SuperAppDefinitions for jail reason codes + outputs: + - newCtx: bytes + emits: [Jail] + errors: [HOST_ONLY_LISTED_AGREEMENT] + +# == Governance == + +getGovernance: + mutability: view + outputs: + - governance: address + +replaceGovernance: + mutability: nonpayable + access: governance + inputs: + - newGov: address + emits: [GovernanceReplaced] + errors: [HOST_ONLY_GOVERNANCE] + +# == Agreement Whitelisting == +# Governance registers and upgrades agreement classes (CFA, IDA, GDA). + +registerAgreementClass: + # Register a new agreement class. Creates a UUPS proxy in upgradable deployments. + mutability: nonpayable + access: governance + inputs: + - agreementClassLogic: address + emits: [AgreementClassRegistered] + errors: [HOST_ONLY_GOVERNANCE, HOST_AGREEMENT_ALREADY_REGISTERED, HOST_MAX_256_AGREEMENTS] + +updateAgreementClass: + # Upgrade an existing agreement class to a new implementation. + mutability: nonpayable + access: governance + inputs: + - agreementClassLogic: address + emits: [AgreementClassUpdated] + errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE, HOST_AGREEMENT_IS_NOT_REGISTERED] + +isAgreementTypeListed: + mutability: view + inputs: + - agreementType: bytes32 # keccak256 of agreement name string + outputs: + - yes: bool + +isAgreementClassListed: + mutability: view + inputs: + - agreementClass: address + outputs: + - yes: bool + +getAgreementClass: + mutability: view + inputs: + - agreementType: bytes32 + outputs: + - agreementClass: address + errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] + +mapAgreementClasses: + # Decode a bitmap into an array of agreement class addresses. + mutability: view + inputs: + - bitmap: uint256 + outputs: + - agreementClasses: address[] + +addToAgreementClassesBitmap: + mutability: view + inputs: + - bitmap: uint256 + - agreementType: bytes32 + outputs: + - newBitmap: uint256 + errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] + +removeFromAgreementClassesBitmap: + mutability: view + inputs: + - bitmap: uint256 + - agreementType: bytes32 + outputs: + - newBitmap: uint256 + errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] + +# == Super Token Factory == + +getSuperTokenFactory: + mutability: view + outputs: + - factory: address + +getSuperTokenFactoryLogic: + # Returns the logic address behind the factory proxy. For non-upgradable + # deployments, returns the factory address itself. + mutability: view + outputs: + - logic: address + +updateSuperTokenFactory: + # Deploy or upgrade the SuperTokenFactory. On first call, creates a UUPS proxy. + mutability: nonpayable + access: governance + inputs: + - newFactory: address + emits: [SuperTokenFactoryUpdated] + errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE] + +updateSuperTokenLogic: + # Upgrade a SuperToken proxy to new logic. If newLogicOverride is address(0), + # uses the canonical logic from the SuperTokenFactory. + mutability: nonpayable + access: governance + inputs: + - token: address + - newLogicOverride: address # address(0) means use canonical logic + emits: [SuperTokenLogicUpdated] + errors: [HOST_ONLY_GOVERNANCE] + +changeSuperTokenAdmin: + # Change the admin of a SuperToken. The admin is the only account allowed to + # upgrade the token logic. Default admin is the Host itself. + mutability: nonpayable + access: governance + inputs: + - token: address + - newAdmin: address + errors: [HOST_ONLY_GOVERNANCE] + +# == Pool Beacon == + +updatePoolBeaconLogic: + # Upgrade the SuperfluidPool beacon to new logic. Affects all GDA pool proxies. + mutability: nonpayable + access: governance + inputs: + - newLogic: address + emits: [PoolBeaconLogicUpdated] + errors: [HOST_ONLY_GOVERNANCE] + +# == ERC2771 / Forwarder == + +isTrustedForwarder: + # Check if an address is a governance-approved trusted forwarder for meta-transactions. + mutability: view + inputs: + - forwarder: address + outputs: + - bool + +versionRecipient: + # ERC2771 relay recipient version string. Returns "v1". + mutability: pure + outputs: + - string + +getERC2771Forwarder: + # Returns the ERC2771Forwarder contract used for ERC2771_FORWARD_CALL batch operations. + mutability: view + outputs: + - address + +getSimpleACL: + # Returns the SimpleACL contract used for Super App registration permissioning. + mutability: view + outputs: + - address + +# == Time == + +getNow: + # Returns block.timestamp. Useful for off-chain tooling and testing. + mutability: view + outputs: + - uint256 + +# == Protocol Constants == + +NON_UPGRADABLE_DEPLOYMENT: + # True if the Host was deployed in non-upgradable mode. + mutability: view + outputs: + - bool + +APP_WHITE_LISTING_ENABLED: + # True if Super App registration requires governance permission. + mutability: view + outputs: + - bool + +CALLBACK_GAS_LIMIT: + # Maximum gas forwarded to Super App callbacks. + mutability: view + outputs: + - uint64 + +MAX_APP_CALLBACK_LEVEL: + # Maximum callback nesting depth for composed Super Apps. Currently 1. + mutability: view + outputs: + - uint256 + +MAX_NUM_AGREEMENTS: + # Maximum number of agreement classes that can be registered. 256. + mutability: view + outputs: + - uint32 + +ACL_SUPERAPP_REGISTRATION_ROLE: + # keccak256("ACL_SUPERAPP_REGISTRATION_ROLE") — the SimpleACL role for app registration. + mutability: view + outputs: + - bytes32 + +SIMPLE_FORWARDER: + # Address of the SimpleForwarder used for SIMPLE_FORWARD_CALL batch operations. + mutability: view + outputs: + - address + +# == Events == + +events: + GovernanceReplaced: + data: + - oldGov: address + - newGov: address + + AgreementClassRegistered: + # agreementType is keccak256("org.superfluid-finance.agreements..") + data: + - agreementType: bytes32 + - code: address + + AgreementClassUpdated: + data: + - agreementType: bytes32 + - code: address + + SuperTokenFactoryUpdated: + data: + - newFactory: address + + SuperTokenLogicUpdated: + indexed: + - token: address + data: + - code: address + + PoolBeaconLogicUpdated: + indexed: + - beaconProxy: address + data: + - newBeaconLogic: address + + AppRegistered: + indexed: + - app: address + + Jail: + # Emitted when a Super App is jailed for violating protocol rules. + # See APP_RULE error below for reason code values. + indexed: + - app: address + data: + - reason: uint256 + + # Inherited events (from UUPSProxiable): + # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) + # Initialized — emitted on proxy initialization (version) + +# == Errors == + +errors: + # Governance + - HOST_ONLY_GOVERNANCE # caller is not the governance contract + + # Agreement whitelisting + - HOST_AGREEMENT_ALREADY_REGISTERED + - HOST_AGREEMENT_IS_NOT_REGISTERED + - HOST_MAX_256_AGREEMENTS # maximum agreement slots exhausted + - HOST_ONLY_LISTED_AGREEMENT # caller is not a registered agreement + + # Upgradability + - HOST_NON_UPGRADEABLE # deployment is non-upgradable + - HOST_CANNOT_DOWNGRADE_TO_NON_UPGRADEABLE + + # App registration + - HOST_MUST_BE_CONTRACT # app address has no code + - HOST_NO_APP_REGISTRATION_PERMISSION + - HOST_INVALID_CONFIG_WORD # invalid Super App manifest flags + - HOST_SUPER_APP_ALREADY_REGISTERED + - HOST_NOT_A_SUPER_APP # target is not a registered Super App + - HOST_SUPER_APP_IS_JAILED + + # App composition + - HOST_SENDER_IS_NOT_SUPER_APP + - HOST_RECEIVER_IS_NOT_SUPER_APP + - HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL + + # Context & call proxies + - HOST_NON_ZERO_LENGTH_PLACEHOLDER_CTX # placeholder ctx must be empty + - HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress + - HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress + - HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION # app action selector matches a callback + + # Batch call + - HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE + + # Callback execution + - HOST_NEED_MORE_GAS # insufficient gas for Super App callback + + # Super App rule violations (generic, carries a reason code) + # Reason codes (from SuperAppDefinitions.sol): + # 10 — NO_REVERT_ON_TERMINATION_CALLBACK (delete callback reverted) + # 11 — NO_CRITICAL_SENDER_ACCOUNT (callback made sender critical) + # 12 — NO_CRITICAL_RECEIVER_ACCOUNT (callback made receiver critical) + # 20 — CTX_IS_READONLY (state change in before-callback staticcall) + # 21 — CTX_IS_NOT_CLEAN (context not properly returned) + # 22 — CTX_IS_MALFORMATED (context bytes corrupted/fabricated) + # 30 — COMPOSITE_APP_IS_NOT_WHITELISTED (no allowCompositeApp) + # 31 — COMPOSITE_APP_IS_JAILED (downstream app is jailed) + # 40 — MAX_APP_LEVEL_REACHED (callback nesting exceeded limit) + - APP_RULE: + inputs: + - _code: uint256 + + # SafeCast (inherited from OpenZeppelin) + - SafeCastOverflowedUintDowncast: + inputs: + - bits: uint8 + - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml new file mode 100644 index 00000000..18edf9b0 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml @@ -0,0 +1,51 @@ +# Generated by scripts/selectors.mjs +functions: + batchCall(tuple[]): 0x31131761 + forwardBatchCall(tuple[]): 0xbc5a2101 + callAgreement(): 0xb7a9d0f0 + callAppAction(): 0x40df3d29 + callAgreementWithContext(address,bytes,bytes,bytes): 0x4329d293 + callAppActionWithContext(address,bytes,bytes): 0xba48b5f8 + decodeCtx(bytes): 0x3f6c923a + isCtxValid(bytes): 0xbf428734 + registerApp(): 0x6d6c85c3 + registerAppWithKey(uint256,string): 0xbd1c448b + registerAppByFactory(address,uint256): 0xf3733052 + isApp(address): 0x3ca3ad4e + getAppCallbackLevel(address): 0x9378fa13 + getAppManifest(address): 0xf9f522f4 + isAppJailed(address): 0x6b4f3335 + allowCompositeApp(address): 0x57121e0c + isCompositeAppAllowed(address,address): 0xbb84cfa1 + callAppBeforeCallback(address,bytes,bool,bytes): 0x74041e02 + callAppAfterCallback(address,bytes,bool,bytes): 0x1e6d0a84 + appCallbackPush(bytes,address,uint256,int256,address): 0x768fabb0 + appCallbackPop(bytes,int256): 0x989b0c3e + ctxUseCredit(bytes,int256): 0x59a29141 + jailApp(bytes,address): 0x5578431e + replaceGovernance(address): 0x7283100c + registerAgreementClass(address): 0x15a024e1 + updateAgreementClass(address): 0x06cecba8 + isAgreementTypeListed(): 0xf8dba358 + isAgreementClassListed(address): 0x8ca48484 + getAgreementClass(bytes32): 0xb6d200de + mapAgreementClasses(uint256): 0xc56a069d + addToAgreementClassesBitmap(uint256,bytes32): 0xbced3ddc + removeFromAgreementClassesBitmap(uint256,bytes32): 0xa5dbbbcd + updateSuperTokenFactory(address): 0x54fbc493 + updateSuperTokenLogic(address): 0x787afde7 + changeSuperTokenAdmin(address,address): 0x0c565075 + updatePoolBeaconLogic(address): 0x2f89bf89 + isTrustedForwarder(address): 0x572b6c05 + SIMPLE_FORWARDER(): 0xf85263b9 +events: + GovernanceReplaced(address,address): 0x13abda02e63c790d0e2818b251282cfe5cbe0a8abd69c54bf5d2260c0907bd2e + AgreementClassRegistered(bytes32,address): 0x878135063a6cfb3bc333e534b1fdc83f4f12221cad6705c31c0567048a8bd3d1 + AgreementClassUpdated(bytes32,address): 0x9279aa773f2b588996032d8de89911555039f28b13a11a7c17074330bc082d9a + SuperTokenFactoryUpdated(address): 0xce13a9895a1719ad4493b2ac1a9bfb36070566161abab408e7ecbe586da8d499 + SuperTokenLogicUpdated(address,address): 0x840acbd291b38534819f47f875839277e502f40e1c7bfea2c5fc2c8017442cd3 + PoolBeaconLogicUpdated(address,address): 0x052cea8931962dd445ef48b0b998d3056bd0705f437087d60fe3c46a3fa09e1f + AppRegistered(address): 0x0d540ad8f39e07d19909687352b9fa017405d93c91a6760981fbae9cf28bfef7 + Jail(address,uint256): 0xbe3aa33bd245135e4e26b223d79d14ea479a47bff09f2b03c53838af1edbb14b + # Inherited events (from UUPSProxiable)(): 0x3a1d0f622cf5be33dc2558b359f025fbeca1d6fb271fb70452a02b0ec2666ce8 +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml new file mode 100644 index 00000000..75e5e437 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml @@ -0,0 +1,305 @@ +# UBISchemeV2 — dynamic daily UBI pool with per-root claiming and cycle accounting +# Claim uses Identity getWhitelistedRoot(msg.sender); transfers use the root as claim key. + +meta: + name: UBISchemeV2 + version: v2 + source: + - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/ubi/UBISchemeV2.sol + inherits: [DAOUpgradeableContract] + note: > + Pool sizing uses cycleLength, reserveFactor, minActiveUsers, and on-chain G$ + balance. claim() reverts if Identity root is zero. Internal accounting keys + claims by whitelisted root, payouts go to msg.sender. + deployments: + mainnet: + production: + UBIScheme: + networkId: 122 + address: "0xd253A5203817225e9768C05E5996d642fb96bA86" + creationBlock: 15747401 + production-celo: + UBIScheme: + networkId: 42220 + address: "0x43d72Ff17701B2DA814620735C39C620Ce0ea4A1" + creationBlock: 18006679 + production-xdc: + UBIScheme: + networkId: 50 + address: "0x22867567E2D80f2049200E25C6F31CB6Ec2F0faf" + creationBlock: 95249624 + related: + - https://docs.gooddollar.org/for-developers/core-contracts/ubischeme + - references/guides/claim.md + +getClaimerCount: + mutability: view + inputs: + - day: uint256 + outputs: + - count: uint256 + +getClaimAmount: + notes: + - "Returns aggregate amount claimed for the provided day index." + mutability: view + inputs: + - day: uint256 + outputs: + - amount: uint256 + +getDailyStats: + notes: + - "Returns (claimer count, total claimed amount) for the calendar day derived from periodStart." + mutability: view + inputs: [] + outputs: + - claimerCount: uint256 + - claimAmount: uint256 + +setCycleLength: + mutability: nonpayable + access: avatar + inputs: + - _newLength: uint256 + outputs: [] + emits: [CycleLengthSet] + errors: [CYCLE_TOO_SHORT, ONLY_AVATAR] + +setDay: + notes: + - "Anyone may advance currentDay when wall-clock day crosses the next boundary." + mutability: nonpayable + inputs: [] + outputs: [] + emits: [DaySet] + +hasClaimed: + mutability: view + inputs: + - account: address + outputs: + - claimed: bool + +isNotNewUser: + mutability: view + inputs: + - _account: address + outputs: + - isReturning: bool + +estimateNextDailyUBI: + notes: + - "View-only projection when dailyUbi not yet fixed for the current wall day." + mutability: view + inputs: [] + outputs: + - projected: uint256 + +"checkEntitlement()": + notes: + - "Uses msg.sender as member; resolves Identity root before hasClaimed check." + mutability: view + inputs: [] + outputs: + - amount: uint256 + +"checkEntitlement(address)": + notes: + - "Checks claimable amount for the provided member using that account's whitelisted root." + mutability: view + inputs: + - _member: address + outputs: + - amount: uint256 + +claim: + notes: + - "Resolves whitelistedRoot = Identity.getWhitelistedRoot(msg.sender); reverts if zero." + - "_claim keys bookkeeping by root but pays msg.sender." + - "May notify ClaimersDistribution at GDAO_CLAIMERS if configured." + - "On success emits UBIClaimed; distributionFormula may emit UBICalculated, UBICycleCalculated, DaySet, WithdrawFromDao." + mutability: nonpayable + access: anyone + inputs: [] + outputs: + - didClaim: bool + errors: + - NOT_IN_PERIOD_OR_PAUSED + - NOT_WHITELISTED + - CLAIM_TRANSFER_FAILED + - DAO_TRANSFER_FAILED + +setShouldWithdrawFromDAO: + mutability: nonpayable + access: avatar + inputs: + - _shouldWithdraw: bool + outputs: [] + emits: [ShouldWithdrawFromDAOSet] + errors: [ONLY_AVATAR] + +pause: + mutability: nonpayable + access: avatar + inputs: + - _pause: bool + outputs: [] + errors: [ONLY_AVATAR] + +setNewClaimersReserveFactor: + notes: + - "Adjusts reserveFactor used by distribution formula to reserve part of pool for new claimers." + mutability: nonpayable + access: avatar + inputs: + - _reserveFactor: uint32 + outputs: [] + errors: [ONLY_AVATAR] + +withdraw: + notes: + - "Avatar-controlled treasury action transferring G$ from scheme balance to recipient." + mutability: nonpayable + access: avatar + inputs: + - _amount: uint256 + - _recipient: address + outputs: [] + errors: + - ONLY_AVATAR + - WITHDRAW_FAILED + +lastClaimed: + notes: + - "Returns last claim timestamp keyed by account root logic used by hasClaimed checks." + mutability: view + inputs: + - account: address + outputs: + - ts: uint256 + +currentDay: + mutability: view + inputs: [] + outputs: + - day: uint256 + +periodStart: + mutability: view + inputs: [] + outputs: + - ts: uint256 + +dailyUbi: + mutability: view + inputs: [] + outputs: + - amount: uint256 + +lastWithdrawDay: + mutability: view + inputs: [] + outputs: + - day: uint256 + +shouldWithdrawFromDAO: + mutability: view + inputs: [] + outputs: + - flag: bool + +cycleLength: + mutability: view + inputs: [] + outputs: + - days: uint256 + +dailyCyclePool: + mutability: view + inputs: [] + outputs: + - pool: uint256 + +startOfCycle: + mutability: view + inputs: [] + outputs: + - ts: uint256 + +currentCycleLength: + mutability: view + inputs: [] + outputs: + - length: uint256 + +minActiveUsers: + mutability: view + inputs: [] + outputs: + - n: uint256 + +totalClaimsPerUser: + mutability: view + inputs: + - account: address + outputs: + - count: uint256 + +reserveFactor: + mutability: view + inputs: [] + outputs: + - bps: uint32 + +paused: + mutability: view + inputs: [] + outputs: + - isPaused: bool + +events: + WithdrawFromDao: + indexed: [] + data: + - prevBalance: uint256 + - newBalance: uint256 + UBICalculated: + indexed: [] + data: + - day: uint256 + - dailyUbi: uint256 + - blockNumber: uint256 + UBICycleCalculated: + indexed: [] + data: + - day: uint256 + - pool: uint256 + - cycleLength: uint256 + - dailyUBIPool: uint256 + UBIClaimed: + indexed: + - claimer: address + data: + - amount: uint256 + CycleLengthSet: + indexed: [] + data: + - newCycleLength: uint256 + DaySet: + indexed: [] + data: + - newDay: uint256 + ShouldWithdrawFromDAOSet: + indexed: [] + data: + - ShouldWithdrawFromDAO: bool + +errors: + NOT_IN_PERIOD_OR_PAUSED: "not in periodStarted or paused — requireStarted modifier." + CYCLE_TOO_SHORT: "cycle must be at least 1 day long" + DAO_TRANSFER_FAILED: "DAO transfer has failed — _withdrawFromDao balance check." + NOT_WHITELISTED: "UBIScheme: not whitelisted — claim()" + CLAIM_TRANSFER_FAILED: "claim transfer failed — G$ transfer in _transferTokens." + WITHDRAW_FAILED: "withdraw failed — avatar withdraw." + ONLY_AVATAR: "Inherited avatar-only guard on DAOUpgradeable paths." diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml new file mode 100644 index 00000000..666b89d5 --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml @@ -0,0 +1,40 @@ +# Generated by scripts/selectors.mjs +functions: + getClaimerCount(uint256): 0xc7a76adf + getClaimAmount(uint256): 0xcef63600 + getDailyStats(): 0x069786ea + setCycleLength(uint256): 0x3d84ceca + setDay(): 0xdddc3616 + hasClaimed(address): 0x73b2e80e + isNotNewUser(address): 0xa21f698a + estimateNextDailyUBI(): 0xc7713870 + checkEntitlement(): 0x98d6621b + checkEntitlement(address): 0x1a787f2e + claim(): 0x4e71d92d + setShouldWithdrawFromDAO(bool): 0xde1de3a0 + pause(bool): 0x02329a29 + setNewClaimersReserveFactor(uint32): 0x414089be + withdraw(uint256,address): 0x00f714ce + lastClaimed(address): 0x013eba92 + currentDay(): 0x5c9302c9 + periodStart(): 0xeda4e6d6 + dailyUbi(): 0x1d8f5ea9 + lastWithdrawDay(): 0xd7c4cbb8 + shouldWithdrawFromDAO(): 0x456ac1c2 + cycleLength(): 0xeac471a0 + dailyCyclePool(): 0x9dc2c033 + startOfCycle(): 0xba075410 + currentCycleLength(): 0x741470ac + minActiveUsers(): 0x37658574 + totalClaimsPerUser(address): 0xcc054dfc + reserveFactor(): 0x4322b714 + paused(): 0x5c975abb +events: + WithdrawFromDao(uint256,uint256): 0x3107ec7eaa50b775d2486c7a394472235804b6fe1c0d4b7bd1d79b09df60f2ba + UBICalculated(uint256,uint256,uint256): 0x836fa39995340265746dfe9587d9fe5c5de35b7bce778afd9b124ce1cfeafdc4 + UBICycleCalculated(uint256,uint256,uint256,uint256): 0x83e0d535b9e84324e0a25922406398d6ff5f96d0c686204ee490e16d7670566f + UBIClaimed(address,uint256): 0x89ed24731df6b066e4c5186901fffdba18cd9a10f07494aff900bdee260d1304 + CycleLengthSet(uint256): 0xa61e6cca2c12e2a0a493683acfe95b034f0f50d793434f4dfe3ba06ea201f344 + DaySet(uint256): 0x67eb03bd555181f9dd23f546e4331ddfb8b4a7d0c8d261ba44e037f30ce894ea + ShouldWithdrawFromDAOSet(bool): 0x6cd9a0fd2e006be39a9918bf56c85cae1d4f4599474483ff18cb93355ebaaf8e +errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md b/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md new file mode 100644 index 00000000..41e2baeb --- /dev/null +++ b/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md @@ -0,0 +1,93 @@ +# Reading the Rich ABI YAMLs + +Each YAML is a self-contained contract reference. Here's how to parse them. + +## Root structure + +``` +# Header comment — contract name, description, key notes +meta: # name, version, source, implements, inherits, deployments, + # deploymentCreationBlocks (optional), notes +# == Section == # Grouped functions (these are the core content) +events: # All events the contract emits +errors: # Complete error index +``` + +Three root keys are reserved: `meta`, `events`, `errors`. Every other +root-level key is a **function**. + +## Function entries + +```yaml +createFlow: + # Description of what the function does. + notes: + - "Gotcha: Non-obvious behavior or edge cases listed here as structured data." + mutability: nonpayable # view | pure | nonpayable | payable + access: sender | operator # who can call (omitted for view/pure) + inputs: + - token: address + - receiver: address + - flowRate: int96 # inline comments for non-obvious params + - ctx: bytes + outputs: + - newCtx: bytes + emits: [FlowUpdated, FlowUpdatedExtension] # ordered by emission sequence + errors: [CFA_FLOW_ALREADY_EXISTS, CFA_INVALID_FLOW_RATE] # ordered by check sequence +``` + +Fields appear in this order: description comment, `notes`, `mutability`, +`access`, `inputs`, `outputs`, `emits`, `errors`. All are omitted when not +applicable. + +## Key conventions + +- **`ctx: bytes` parameter** = function is called through the Host + (`callAgreement` / `batchCall`), never directly. +- **`access` labels**: `anyone`, `host`, `self`, `admin`, `governance`, + `sender`, `receiver`, `operator`, `manager`, `pic`, `agreement`, + `trusted-forwarder`, `factory`, `super-app`. Combine with `|`. Conditional: + `anyone(if-critical-or-jailed)`. +- **`emits` and `errors` ordering** carries meaning: matches execution flow, + not alphabetical. First errors in the list are the most likely causes. +- **`notes:` field** on functions (and `meta.notes:` at contract level) + lists non-obvious behavior, common mistakes, or edge cases. Always read + these carefully. +- **`meta.source`** is an array of raw GitHub URLs to the Solidity source files + (implementation, interface, base — filenames are self-documenting). +- **`meta.deployments`** has per-network addresses split into `mainnet` and + `testnet` subgroups. +- **`creationBlock`**: decimal block where that **`address`** first has code; + put it on the same object as **`networkId`** / **`address`**, immediately after **`address`**. +- **`meta.deploymentCreationBlocks`**: same shape as **`meta.deployments`**, for + deployments stored as plain address strings; leaves are decimal block numbers. + +## Events section + +```yaml +events: + FlowUpdated: + indexed: # log topics (filterable) + - token: address + - sender: address + data: # log payload + - flowRate: int96 +``` + +## Errors section + +```yaml +errors: + # -- Category -- + - SIMPLE_ERROR # description + - PARAMETERIZED_ERROR: # errors with diagnostic data + inputs: + - value: uint256 +``` + +## Selector sidecar files + +Every `Foo.abi.yaml` has a companion `Foo.selectors.yaml` in the same +directory. These contain computed hex selectors (function 4-byte, error +4-byte, event 32-byte topic0) with full Solidity signatures for +verification. Generated by `scripts/selectors.mjs`. diff --git a/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md b/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md new file mode 100644 index 00000000..12b18990 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md @@ -0,0 +1,42 @@ +# Faucet flows (user-facing explanation) + +This note explains the Faucet in plain language: what it does for users, why a top-up can fail, and what limits exist. + +The on-chain Faucet contract is used to add a small amount of native gas token to a wallet so the user can pay transaction fees. +Reference implementation: [`contracts/fuseFaucet/Faucet.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/fuseFaucet/Faucet.sol). +Addresses per chain: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `Faucet` under `production`, `production-celo`, `production-xdc`). + +## What this means for users + +- If your wallet is eligible, Faucet can send a small gas top-up. +- Eligibility usually depends on identity status and anti-abuse limits. +- This is a support mechanism for transaction fees, not a general transfer or swap service. + +## Main actions (in user language) + +- **Top up wallet** (`topWallet`) + Attempts to send gas to the target wallet after checks pass. + +- **Check eligibility first** (`canTop`) + Fast pre-check to see if top-up is currently allowed. + +- **Estimate top-up amount** (`getToppingAmount`) + Shows the amount Faucet would try to send right now. + +## Why a top-up may fail + +- You are not currently authorized by identity rules. +- Daily limit reached. +- Weekly limit reached. +- Wallet is temporarily banned. +- Wallet is too new for current policy. +- Calculated top-up is below minimum threshold. + +## Important safety note + +- The `onTokenTransfer` path includes a swap-like mechanism and is not meant as a normal user swap route. +- It does not enforce slippage protection in the same way users expect from a dedicated swap UI. + +## For developers and agents + +Use `references/guides/faucet.md` for step-by-step execution flow and deterministic preflight calls. diff --git a/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md b/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md new file mode 100644 index 00000000..261c2d7f --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md @@ -0,0 +1,38 @@ +# Why Fuse to CELO staking migration uses a staged backend flow + +This explains why the migration is split into allowance detection, unstake, bridge, and destination re-stake instead of a single transaction. In this context, Fuse `GovernanceStakingV2` is the old/source staking contract and Celo **`GooddollarSavingsStream`** is the destination staking contract ([source](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [0x059ee811414230d1Fb157878D2b491240F4D8d3B](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). + +## Why this cannot be one-chain atomic + +Fuse governance staking and CELO destination savings live on different chains, so the system cannot atomically close and reopen stake in one EVM transaction. Cross-chain migration is asynchronous by design and must tolerate timing gaps between source completion and destination finalization. + +## Why user allowance is first + +The migration flow assumes a backend-operated execution wallet. If that wallet needs to pull or operate on user funds in the source path, user approval must exist first. Without allowance, all downstream steps fail, so approval state is the earliest hard gate. + +## Why unstake is separated from bridge + +The source staking position is the canonical balance record on Fuse. The migration must first materialize transferable G$ by closing or reducing stake, then bridge only the confirmed unlocked amount. Bridging before final unstake confirmation introduces mismatch risk. + +## Why bridge finalization must be explicit + +Bridge transfers are eventually consistent across chains. The destination stake step must only run after the bridged G$ is confirmed on CELO. This avoids phantom staking attempts and preserves deterministic accounting. + +## Why destination uses `stakeFor` + +Ubeswap `GooddollarSavingsStream` on Celo supports `stakeFor(amount, recipient)`, which allows the backend execution wallet to stake on behalf of the user after bridge finalization. This fits migration operations where custody is temporary during the transfer window. + +## Main operational risks + +- partial migration from source unstake or bridge limit constraints +- stuck-in-transit bridge messages delaying destination staking +- stale assumptions about contract addresses across networks +- reward expectation mismatch when moving from Fuse governance staking mechanics to CELO savings mechanics + +## Contract/source map + +- Fuse staking family (GoodProtocol): [`GovernanceStaking.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/governance/GovernanceStaking.sol) +- Fuse deployment mapping: [`deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`production.GovernanceStakingV2`) +- Celo savings (Ubeswap): [`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol) +- Ubeswap contracts repository: [Ubeswap/gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) +- Bridge normalization for LZ quotes: [`BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) (off-chain LayerZero fee estimation must match this; `canBridge` and `bridgeToWithLz` use the raw burn amount on the source chain) diff --git a/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md b/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md new file mode 100644 index 00000000..754a3a05 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md @@ -0,0 +1,37 @@ +# GoodDAO and DAOStack surface + +GoodProtocol’s on-chain **governance shell** is largely **DAOStack-shaped**: an **Avatar** holds protocol assets and reputation context; a **Controller** registers **schemes** and routes privileged calls. GoodDocs summarizes DAO-facing roles; **Avatar**, **Controller**, and other DAO contract addresses live only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). Implementation follows [DAOStack Arc](https://github.com/daostack/arc) patterns. + +## Core interfaces (GoodProtocol) + +[`DAOStackInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/DAOStackInterfaces.sol) defines the pieces most integrations touch: + +### Avatar + +- **`nativeToken()`** — often the G$ token address for the deployment. +- **`nativeReputation()`** — **`GReputation`** token for voting weight (see `GReputation` in `deployment.json`). +- **`owner()`** — owner of the Avatar, typically the **`Controller`** contract. + +### Controller + +- **`avatar()`** — address of the Avatar contract. +- **`registerScheme` / `unregisterScheme` / `unregisterSelf` / `isSchemeRegistered` / `getSchemePermissions`** — scheme lifecycle and permission bitmask per scheme+avatar. +- **`genericCall(contract, data, avatar, value)`** — executes arbitrary calls **as the avatar** (used heavily by DAO-backed contracts to move tokens or call NameService). +- **`mintTokens`**, **`externalTokenTransfer`**, **`sendEther`** — treasury-style operations through the controller/avatar. + +## How GoodProtocol contracts use it + +- **`DAOUpgradeableContract`** / **`DAOContract`** descendants resolve **`dao`** (Controller) and **`avatar`** from **NameService** keys such as **`CONTROLLER`** and **`AVATAR`** (also written during **NameService.initialize**). +- **Avatar-gated writes** (for example **NameService.setAddress**, **UBISchemeV2** admin functions, **IdentityV3** after `initDAO`) require **`msg.sender == dao.avatar()`** (or equivalent role), not EOAs. +- Schemes that upgrade themselves (for example staking **`upgrade()`**) use **`dao.genericCall`** and **`unregisterSelf`** against the avatar. + +## What agents should do + +1. Treat **DAO calls** as **governance-only** unless the user explicitly controls the avatar or a registered scheme. +2. For **read-only** work, use **NameService** and per-contract **view** functions; use **Controller.isSchemeRegistered** when validating that a target contract is an approved scheme (for example staking migrations). +3. Never fabricate **Avatar** or **Controller** addresses — use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or NameService. + +## References + +- [Core contracts — DAO contracts](https://docs.gooddollar.org/for-developers/core-contracts) (narrative only; addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) +- DAOStack Arc controller and avatar concepts in the upstream repo linked from GoodDocs. diff --git a/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md b/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md new file mode 100644 index 00000000..96d321a3 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md @@ -0,0 +1,68 @@ +# How UBI is minted + +This document aligns agent explanations with [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and GoodDocs component pages; **contract addresses** come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). + +## Monetary creation (protocol level) + +- New G$ is created in connection with reserve mechanics: purchases into the reserve and reserve-side parameters (including reserve ratio) influence how much G$ can be issued while maintaining backing (see sustainability and issuance sections in GoodDocs). +- Selling G$ back to the reserve burns supply in that model. +- G$ that the protocol creates is allocated across UBI, savings incentives, treasury, and ecosystem uses per the distribution section of GoodDocs. + +## Where G$ is actually created + +Creation happens at the G$ token `mint(...)` call site, not inside `UBIScheme.claim()`. + +Current implementation uses the Mento-core expansion path: + +- `GoodDollarExpansionController` mints and routes to distribution helper. + +**DistributionHelper recipients** decide how much eventually lands in the UBI pool. + +## Claim vs reserve minting (important distinction) + +- **Reserve path:** Buying G$ through the reserve-backed AMM (or related Mento rails) is where mint and burn tied to the reserve model most directly apply at the token level. +- **Daily UBI `claim()`:** On `UBISchemeV2`, a successful claim typically **transfers G$ from the scheme contract’s balance** to the user (`token.transfer` in `_transferTokens`). The scheme may be **refilled** from the DAO avatar via internal `_withdrawFromDao` when configured, not necessarily minting in the same transaction as `claim()`. So describe user-facing UBI as **receipt from the UBI scheme balance**; reserve **minting** is the macro story, **transfer** is the usual claim-time mechanism. + +## Mento-core expansion flow (detailed) + +This is the detailed path for modern reserve-ratio-aware expansion. + +1. A caller triggers `mintUBIFromExpansion(exchangeId)` on `GoodDollarExpansionController`. +2. Expansion is time-gated by config (`expansionFrequency`, `lastExpansion`), so it does not run every block. +3. Controller computes a reserve-ratio scalar (effectively compounding `(1 - expansionRate)` for elapsed periods). +4. Controller calls `GoodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar)`. +5. Exchange provider updates exchange state (including reserve-ratio math) and returns `amountToMint`. +6. Controller mints G$ to `distributionHelper`. +7. Controller triggers distribution (`onDistribution`) so recipients (including UBIScheme when configured) receive allocation. + +Also in `GoodDollarExpansionController`: + +- `mintUBIFromInterest(exchangeId, reserveInterest)` +- `mintUBIFromReserveBalance(exchangeId)` + +These are additional funding paths that mint to distribution helper as part of reserve-driven policy. + +## Reserve ratio, expansion, and risk + +- Practical reserve-ratio intuition: collateral backing strength per unit of G$ supply. +- Lower reserve ratio means weaker backing and higher risk when adding new supply. +- Expansion uses reserve-ratio-aware math instead of blind fixed minting, but aggressive params can still increase sell pressure. +- Key policy levers are expansion rate and expansion frequency (plus caller cadence/automation quality). + +## What claimers experience + +- Verified users receive daily UBI from a pool split among those who claim in each period (GoodDocs). +- The user-facing transaction is a UBIScheme-style `claim` on chains where it is deployed; ABI and version follow your deployment. + +## On-chain components (typical) + +- Identity system for verification and whitelist roots. +- UBIScheme (or successor) for entitlement and claim execution. +- G$ token: value reaches users via **transfer** from scheme balance and/or broader minting economics from the reserve side depending on which action you analyze. +- DistributionHelper: bridge layer between mint source and recipient buckets (UBI, others). + +## Agent guidance + +- Use GoodDocs for macro issuance and allocation; use UBIScheme + token transfer behavior for **claim** explanations. +- Use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for contract addresses instead of guessing. +- When users ask "why no UBI funding today", check whether mint functions were executed, then verify DistributionHelper recipient config and UBIScheme balance. diff --git a/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md b/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md new file mode 100644 index 00000000..267fa744 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md @@ -0,0 +1,58 @@ +# Inviter and invitee reward model + +This explains why invite rewards sometimes work and sometimes fail, in user-facing terms. + +Invite rewards are handled by `InvitesV2`, but eligibility depends heavily on the identity system (`Identity`, often `IdentityV4`). In practice, most confusing cases are caused by whitelist or reverification state, not by the invite contract itself. + +## How the reward model works + +There are two separate moments: + +- `join`: the invitee registers with an invite code. +- bounty payout: the contract later checks if this invitee is eligible and pays inviter and invitee when rules pass. + +So joining does not guarantee immediate payout. Payout depends on current eligibility at claim time. + +## Why whitelist status is the main gate + +For bounty eligibility, `InvitesV2` checks whitelist state through Identity. + +The important behavior is: + +- A user can still have status `1` in identity storage but fail `isWhitelisted(...)` if reverification is due. +- When reverification is due, bounty checks fail until an admin refreshes authentication(Face Verification). +- Connected-wallet setups can still fail if the specific address used in invite flow does not pass the whitelist check expected by the contract path. + +## Why reverification blocks rewards + +Reverification cadence is defined in `IdentityV4` with day-based options (`reverifyDaysOptions`) and per-user progression (`authCount`). + +When too many days pass since the last authentication for that user’s current step: + +- `shouldReverify(...)` becomes true +- `isWhitelisted(...)` becomes false for bounty gating +- `canCollectBountyFor(...)` fails until authentication is refreshed + +This is why teams may see users who were once valid but are currently not eligible for invite rewards. + +## Common reasons a bounty is not paid + +- Invitee or inviter is not currently whitelisted. +- Reverification is due for invitee or inviter. +- `minimumClaims` or `minimumDays` thresholds are not met yet. +- Bounty was already paid or was zero at join time. +- Contract is inactive, or identity-chain checks do not match the active chain. +- Invite code or join state is invalid (duplicate code, self-invite, already joined). + +## What to measure for analytics + +- Historical pass: account was authenticated at least once (`lastAuthenticated > 0`). +- Current eligibility: account is currently whitelist-valid (`isWhitelisted`/non-zero root, depending on query design). + +Do not treat these as the same metric. Historical pass explains past onboarding success; current eligibility explains current payout success. + +## Contract sources + +- Invite contract: [`InvitesV2.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/invite/InvitesV2.sol) +- Identity contract: [`IdentityV4.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/identity/IdentityV4.sol) +- Deployment addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) diff --git a/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md b/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md new file mode 100644 index 00000000..e46db2a8 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md @@ -0,0 +1,27 @@ +# Mento and reserve economics + +This note ties **macro G$ economics** (GoodDocs) to **Mento trading surfaces** (broker, reserve, expansion) that agents integrate on-chain. + +## Protocol-level story (GoodDocs) + +- G$ is **reserve-backed**; issuance and price discovery follow an **augmented bonding curve** (Bancor-style dynamics) described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). +- **Buying** from the reserve side increases reserve assets and can support **new G$ supply** within reserve rules; **selling back** burns G$ and returns collateral, with parameters governed by **GoodDAO**. +- Distribution of newly created G$ spans UBI, savings incentives, treasury, and ecosystem allocations (same doc), but execution timing depends on the mint path and distribution trigger used by the deployment flow. + +## User-facing buy and sell (historical vs current) + +- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) describes reserve interaction, fees (including exit contribution on some paths), and older explorer flows (GoodMarketMaker / exchangeHelper on Ethereum testnets). Treat that page as **product narrative**; **live contract addresses** must come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). + +## Mento stack (agent integration) + +On networks where GoodDollar uses Mento (see `MentoBroker` and related keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for your environment): + +- **`IBroker`** (implementation in [mento-core `Broker.sol`](https://github.com/mento-org/mento-core)) is the usual **swap entrypoint**: `getAmountIn` / `getAmountOut`, `swapIn` (exact in), `swapOut` (exact out), plus **trading limits** state per exchange id and token. +- **Reserve** holds collateral; **exchange provider** contracts price trades against the reserve; **GoodDollarExpansionController** and related interfaces in GoodProtocol’s [`MentoInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/MentoInterfaces.sol) describe expansion and avatar wiring for governance-facing changes. +- Agents should not hardcode **exchangeId** or provider addresses: read them from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or discover via `getExchangeProviders()` after confirming the broker address for the chain from that file. + +## Agent guidance + +1. Explain **macro** supply and reserve behavior with GoodDocs language. +2. Execute **swaps** with broker quotes, slippage bounds, and allowances per `references/guides/swap.md`. +3. On revert, distinguish **slippage / limit** failures (broker) from **reserve liquidity** messages (see `MentoBroker.abi.yaml` error map and mento-core source). diff --git a/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md b/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md new file mode 100644 index 00000000..b59e5c92 --- /dev/null +++ b/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md @@ -0,0 +1,69 @@ +# On- and off-ramp service via stable token swap + +Use this note for the service pattern where ramp providers do not list G$ directly. The practical path is: ramp in/out with a listed stable token (for example cUSD), then swap between stable and G$ on-chain. + +## Why this is required + +- Most on-/off-ramp providers list mainstream stable tokens, not G$. +- Service needs a bridge asset for fiat rails. +- Stable token becomes the integration point with ramp providers, while G$ remains the in-app asset. + +## On-chain source of truth + +- Solidity: [`contracts/utils/BuyGDClone.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/utils/BuyGDClone.sol) +- Components: + - `BuyGDCloneFactory` + - `BuyGDCloneV2` + - `DonateGDClone` +- Deployments: [releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) + +## Service architecture + +The factory deploys EIP-1167 minimal clones and wires swap infrastructure (router, oracle, quoter, optional Mento broker configuration, G$ token, stable token). + +Swap execution is **dual-path**: + +- **Uniswap-style route** (router/quoter path) +- **Mento-based route** (broker/exchange-provider path) + +For each swap request, the service compares quoted outputs and selects the route with the **larger `amountOut`** (best execution for the same input amount), then enforces `minAmount` guard on the selected path. + +Each user gets a deterministic clone address from owner-based salt: + +- `predict(owner)` for buy clone +- `predictDonation(owner, donor)` for donation clone + +This is why clone-per-user design is used: + +- predictable per-user addresses for audit and routing +- isolated execution context +- cheaper deployment than full contract instances + +## Execution surface (conceptual) + +The operational surface is intentionally small and deterministic: + +- `create(owner)` +- `createAndSwap(owner, minAmount)` +- `predict(owner)` +- `createDonation`, `createDonationAndSwap`, `predictDonation` + +This keeps on-/off-ramp architecture auditable and predictable across users. + +## Risks + +- Wrong factory or wrong chain causes permanent fund loss risk. +- Stale router/oracle/mento config can fail swap or produce bad execution. +- Missing `minAmount` protection increases slippage risk. +- Quote source mismatch or stale quotes across Uniswap/Mento can pick a suboptimal route if not refreshed just before execution. + +## Boundary note + +This file explains **why** this architecture exists for ramp services and why per-user clones matter. +For step-by-step service execution flow, use `references/guides/on-off-ramp.md`. + +## Cross-reference + +- User narrative: [Buy and Sell G$](https://docs.gooddollar.org/user-guides) +- Token integration details: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) +- Broker ABI: `references/contracts/MentoBroker.abi.yaml` diff --git a/.agents/skills/gooddollar/references/guides/bridge.md b/.agents/skills/gooddollar/references/guides/bridge.md new file mode 100644 index 00000000..71fd6e8c --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/bridge.md @@ -0,0 +1,187 @@ +# Bridge guide + +Use for moving G$ across supported networks with deployment-specific bridge contracts. + +Primary local ABI reference for MessagePassingBridge flow: + +- `references/contracts/MessagePassingBridge.abi.yaml` + +## GoodDocs alignment + +- User flow and high-level behavior: [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). +- Resolve supported bridge contract addresses per chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `MpbBridge` under `production`, `production-celo`, `production-xdc`). Use [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) for user-facing flow and troubleshooting, not for addresses. + +## Goal + +Bridge with deterministic pre-checks: bridge support, allowance, amount, cross-chain transport fee, and delivered G$ after destination **amount** limits and protocol fee. + +## Required inputs + +- source and destination chain metadata +- bridge contract address for source chain +- source G$ token address +- amount in source token decimals +- signer and rpc url + +## Execution flow + +1. Resolve source bridge and token addresses for the network pair. +2. Run bridge eligibility checks for sender and amount via `canBridge(from, amount)` on the **source** bridge (same contract you call for `bridgeToWithLz` / `bridgeToWithAxelar`). Outbound burn does not invoke `canBridge` inside `_bridgeTo`; destination mint still enforces **amount** limits (see **Bridge amount limit context**). +3. Read allowance and approve bridge spender when required. +4. Resolve transport mode (`LZ` or `AXELAR`) and estimate required native fee. +5. Send bridge transaction with nonzero `msg.value` and explicit transport method. +6. Return tx hash and normalized bridge parameters. + +## Bridge fee context + +Two different costs show up on `MessagePassingBridge`; do not conflate them. + +**1. Cross-chain transport fee (native gas token on the source chain)** +Paid as **`msg.value`** on the outbound call. The contract reverts with **`MISSING_FEE`** if `msg.value` is zero. On the **LayerZero** path the contract compares your `msg.value` to **`estimateSendFee`** and reverts **`LZ_FEE(required, sent)`** if it is too low. Use the same **normalized** amount for `estimateSendFee` as the contract uses internally (see the next section). On the **Axelar** path you still attach native value for Gas Service / execution; there is no single `estimateSendFee` analogue in the snippet—follow Envio/Axelar docs or simulate the exact call for production amounts. + +**2. Protocol fee on minted G$ (destination chain, basis points)** +When the message is executed on the **destination** chain, the bridge applies **`bridgeFees`** (min / max / fee bps via `setBridgeFees`) and mints the recipient **minus** that fee; the fee portion is minted to **`feeRecipient`** when it is non-zero (see `bridgeFees()`, `feeRecipient`, and `_takeFee` / `ExecutedTransfer` in `references/contracts/MessagePassingBridge.abi.yaml`). This is **not** the LayerZero relayer fee; it is a separate cut on the **token amount** delivered on arrival. + +**3. Optional OFT / LayerZero token-adapter path** +If the flow uses the GoodDollar OFT-style adapter instead of `MessagePassingBridge`, fee quoting follows **`quoteSend`** / **`MessagingFee`** on that contract; see `references/contracts/GoodDollarOFTAdapter.abi.yaml`. + +## Bridge amount limit context + +**Bridge limit** means **bridge amount limit**: policy on **how much G$** (token volume) may move—**`minAmount`**, per-transfer cap, per-account daily cap, and aggregate daily cap—plus **`onlyWhitelisted`**. It does **not** mean the cross-chain **native** transport fee (`msg.value`, **`LZ_FEE`**), and it does **not** mean the **destination mint fee** in **`bridgeFees`** (bps); those are covered under **Bridge fee context**. + +Amount caps and counters are **per bridge deployment**: read the **source** contract for outbound **amount** policy and usage meters tied to the burn, and the **destination** contract for **`_enforceLimits`** at inbound mint completion; do not assume identical **`bridgeLimits`** across chains. + +**1. Amount caps and usage meters** +**`bridgeLimits()`** exposes **`dailyLimit`**, **`txLimit`**, **`accountDailyLimit`**, **`minAmount`**, and **`onlyWhitelisted`**. Compare those caps to **`bridgeDailyLimit()`** (aggregate **`bridged24Hours`** and **`lastTransferReset`**) and **`accountsDailyLimit(account)`** (same fields per sender). Updates use **`setBridgeLimits`** (access per the ABI). Field-level notes and accessors live in `references/contracts/MessagePassingBridge.abi.yaml`. + +**2. Source preflight vs destination enforcement** +**`canBridge(from, amount)`** on the **source** is a view-only diagnostic for **that amount**: same policy family as amount limit checks, evaluated on the **raw** burn size (not the LayerZero fee normalization). Outbound **`_bridgeTo`** does **not** call **`canBridge`**; call it from the client if you want **`(false, reason)`** before signing instead of learning only from a revert after burn setup. When the message is executed on the **destination**, **`_enforceLimits`** is the hard gate for **amount** throttles and whitelist behavior at mint time. + +## Outbound pause, approved requests, and inbound source bridges + +These controls are separate from numeric **amount** caps; they still block or relax bridging and can surface as **`BRIDGE_LIMITS`** or inbound skips. + +**`pauseBridge`** sets **`isClosed`**; when closed, outbound flow reverts with **`BRIDGE_LIMITS('closed')`**. **`approvedRequests(requestId)`** on the destination lets **`_bridgeFrom`** skip standard **amount** limit enforcement for that completion when set. **`setDisabledBridges`** toggles **`disabledSourceBridges`** entries keyed by **`keccak256(abi.encode(sourceChainId, BridgeService))`**, controlling whether an inbound relay from that source is accepted before the rest of destination handling. See `references/contracts/MessagePassingBridge.abi.yaml`. + +## Axelar vs LayerZero on GoodDollar deployments + +LayerZero mappings are initialized for Ethereum, Celo, Fuse, and XDC in `initialize` / `upgrade` on `MessagePassingBridge`. The **Axelar** path is only usable where `toAxelarChainId(targetChainId)` returns a non-empty string; the on-chain pure function currently maps **1**, **5**, **42220**, and **44787** only. For **Fuse (122)** or **XDC (50)** targets, use **LZ** (`bridgeToWithLz`) unless governance ships a broader Axelar mapping. + +## LayerZero fee and `estimateSendFee` + +`bridgeToWithLz` burns the G$ **raw** amount in the source token’s `decimals()`. Inside the contract, LayerZero payload and `estimateSendFee` use a value normalized to **18 decimals** the same way as [GoodBridge `BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol): if `decimals < 18`, multiply by `10^(18 - decimals)`; if `decimals > 18`, divide by `10^(decimals - 18)`; otherwise use the raw amount. + +`canBridge(from, amount)` is evaluated on the **raw** burn amount, not the normalized value. + +Read `decimals()` from the source G$ contract when building off-chain fee quotes so you stay aligned if a deployment differs. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +function normalizedForLzFee(raw, tokenDecimals) { + if (tokenDecimals < 18) return raw * 10n ** BigInt(18 - tokenDecimals); + if (tokenDecimals > 18) return raw / 10n ** BigInt(tokenDecimals - 18); + return raw; +} + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const token = new ethers.Contract( + process.env.GOODDOLLAR_ADDRESS, + [ + "function decimals() view returns (uint8)", + "function allowance(address,address) view returns (uint256)", + "function approve(address,uint256) returns (bool)", + ], + signer, +); + +const bridge = new ethers.Contract( + process.env.BRIDGE_ADDRESS, + [ + "function canBridge(address,uint256) view returns (bool,string)", + "function toLzChainId(uint256) view returns (uint16)", + "function estimateSendFee(uint16,address,address,uint256,bool,bytes) view returns (uint256,uint256)", + "function bridgeToWithLz(address,uint256,uint256,bytes) payable", + "function bridgeToWithAxelar(address,uint256,uint256,address) payable", + ], + signer, +); + +const owner = await signer.getAddress(); +const targetChainId = Number(process.env.TARGET_CHAIN_ID); +const recipient = process.env.RECIPIENT; +const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); +const transport = (process.env.BRIDGE_TRANSPORT || "LZ").toUpperCase(); +const tokenDecimals = await token.decimals(); +const normalizedForLzEstimate = normalizedForLzFee(amount, Number(tokenDecimals)); + +const [canBridge, reason] = await bridge.canBridge(owner, amount); +if (!canBridge) throw new Error(`Bridge blocked: ${reason}`); + +const allowance = await token.allowance(owner, process.env.BRIDGE_ADDRESS); +if (allowance < amount) { + const approveTx = await token.approve(process.env.BRIDGE_ADDRESS, amount); + await approveTx.wait(); +} + +let tx; + +if (transport === "LZ") { + const dstEid = await bridge.toLzChainId(targetChainId); + if (dstEid === 0) throw new Error("Unsupported target chain for LayerZero"); + + const adapterParams = process.env.LZ_ADAPTER_PARAMS || "0x"; + const [nativeFee] = await bridge.estimateSendFee( + dstEid, + owner, + recipient, + normalizedForLzEstimate, + false, + adapterParams, + ); + if (nativeFee <= 0n) throw new Error("Estimated LayerZero fee is zero"); + + tx = await bridge.bridgeToWithLz(recipient, targetChainId, amount, adapterParams, { + value: nativeFee, + }); +} else if (transport === "AXELAR") { + const nativeFee = ethers.parseEther(process.env.AXELAR_FEE_ETH || "0.01"); + tx = await bridge.bridgeToWithAxelar(recipient, targetChainId, amount, owner, { + value: nativeFee, + }); +} else { + throw new Error("Unsupported BRIDGE_TRANSPORT. Use LZ or AXELAR"); +} + +const receipt = await tx.wait(); +console.log( + JSON.stringify( + { + txHash: receipt.hash, + sourceBridge: process.env.BRIDGE_ADDRESS, + targetChainId, + transport, + recipient, + rawAmount: amount.toString(), + tokenDecimals: Number(tokenDecimals), + normalizedAmountForLz: normalizedForLzEstimate.toString(), + }, + null, + 2, + ), +); +``` + +## Failure handling + +- unsupported destination: return targetChainId, bridge address, and transport mode +- fee too low (`LZ_FEE` or underpriced Axelar fee): re-estimate and retry with user confirmation +- approval or balance issue: return required delta +- credited G$ on destination is reduced by **`bridgeFees`** (bps / min / max); that is independent of the source **`msg.value`** transport fee +- **`canBridge`** false on source: return the **`reason`** string from the view call +- **`BRIDGE_LIMITS(reason)`** custom error (see `references/contracts/MessagePassingBridge.abi.yaml` **errors**): **`reason`** labels the failing check (numeric **amount** limit, whitelist, **`closed`**, or other policy string from the implementation) +- source preflight passed but destination still reverts: re-read **`bridgeLimits`** and daily counters for **amount** caps and **`onlyWhitelisted`**; check **`isClosed`**, **`approvedRequests`**, and **`disabledSourceBridges`** per **Outbound pause, approved requests, and inbound source bridges**; message delivery can cross a reset boundary or policy change diff --git a/.agents/skills/gooddollar/references/guides/check-identity.md b/.agents/skills/gooddollar/references/guides/check-identity.md new file mode 100644 index 00000000..adc01659 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/check-identity.md @@ -0,0 +1,87 @@ +# Check identity guide + +Use when the user asks whether an address is eligible for UBI or how identity links wallets. + +## GoodDocs alignment + +- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity): associated addresses resolve to a verified root in the Identity contract; `connectAccount` links wallets. +- One claim per day applies across all connected addresses for the same verified identity (see the hint on that page). + +## Goal + +Determine whitelist or authentication status with deterministic on-chain reads. + +## Metric semantics + +- **Passed whitelisting (historical):** use `lastAuthenticated(account) > 0`. +- **Still whitelisted (current):** use `getWhitelistedRoot(account) != 0x0` or `isWhitelisted(account) == true`. +- `getWhitelistedRoot(account) != 0x0` is a current-state signal, not an "ever passed" signal. + +## Required inputs + +- `nameServiceAddress` or explicit Identity address +- `account` to check +- `rpcUrl` and chain configuration + +## Execution flow + +1. Resolve `IDENTITY` from NameService when used on the deployment. +2. Read `getWhitelistedRoot(account)` or equivalent for the deployed Identity version. +3. Treat non-zero root as tied to a whitelisted identity tree when that is the protocol rule for the deployment. +4. Read `lastAuthenticated(account)` for historical pass status and `getWhitelistedRoot(account)` or `isWhitelisted(account)` for current status. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); + +const nameService = new ethers.Contract( + process.env.NAMESERVICE_ADDRESS, + ["function getAddress(string) view returns (address)"], + provider, +); + +const identityAddress = await nameService.getAddress("IDENTITY"); +const identity = new ethers.Contract( + identityAddress, + [ + "function getWhitelistedRoot(address) view returns (address)", + "function isWhitelisted(address) view returns (bool)", + "function lastAuthenticated(address) view returns (uint256)", + ], + provider, +); + +const account = process.env.ACCOUNT; +const root = await identity.getWhitelistedRoot(account); +const isWhitelisted = await identity.isWhitelisted(account); +const lastAuthenticated = await identity.lastAuthenticated(account); + +console.log( + JSON.stringify( + { + account, + identityAddress, + whitelistedRoot: root, + isWhitelisted, + lastAuthenticated: lastAuthenticated.toString(), + }, + null, + 2, + ), +); +``` + +## Return shape + +- `isWhitelisted` or equivalent boolean summary +- `whitelistedRoot` or equivalent +- `lastAuthenticated` for historical-pass classification +- optional metadata fields when available + +## Failure handling + +- NameService cannot resolve `IDENTITY`: stop and fix inputs using [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`Identity` / `NameService` for the target environment)—not GoodDocs tables. +- Read failures: return the failing call and next step. diff --git a/.agents/skills/gooddollar/references/guides/claim.md b/.agents/skills/gooddollar/references/guides/claim.md new file mode 100644 index 00000000..bd51abd0 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/claim.md @@ -0,0 +1,83 @@ +# Claim guide + +Use when the user wants to claim daily UBI. Protocol context: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [UBIScheme (GoodDocs behavior)](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme)—contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). + +## Goal + +Execute a safe `claim()` with identity pre-checks and clear outputs. + +## GoodDocs alignment + +- UBI is distributed daily to verified users; the active pool is split among claimers in each period (see [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works)). +- UBIScheme deployments vary by chain (Fuse, Celo, XDC); resolve live contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`UBIScheme` under `production`, `production-celo`, or `production-xdc`). Use [Core contracts / UBIScheme](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme) for documented behavior, not for addresses. + +## Required inputs + +- `nameServiceAddress` or explicit UBIScheme and Identity addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) +- `rpcUrl` and chain configuration +- signer context + +## Execution flow + +1. Resolve `IDENTITY` and `UBISCHEME` from NameService when NameService is the source of truth for the deployment. +2. Confirm whitelist status for the claiming account. +3. Optionally read entitlement or claimable state before sending `claim()`. +4. Call `claim()` on the resolved UBIScheme (contract generation may differ by deployment; align ABI with your target). +5. Return tx hash and claimed amount when derivable from events or balance delta. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const nameService = new ethers.Contract( + process.env.NAMESERVICE_ADDRESS, + ["function getAddress(string) view returns (address)"], + provider, +); + +const identityAddress = await nameService.getAddress("IDENTITY"); +const ubiAddress = await nameService.getAddress("UBISCHEME"); + +const identity = new ethers.Contract( + identityAddress, + [ + "function isWhitelisted(address) view returns (bool)", + "function getWhitelistedRoot(address) view returns (address)", + ], + provider, +); + +const account = await signer.getAddress(); +const isWhitelisted = await identity.isWhitelisted(account); +if (!isWhitelisted) throw new Error("Account is not whitelisted"); + +const root = await identity.getWhitelistedRoot(account); +if (root === ethers.ZeroAddress) throw new Error("No whitelisted root"); + +const ubi = new ethers.Contract( + ubiAddress, + ["function claim()", "event UBICalculated(address,uint256,uint256,uint256)"], + signer, +); + +const tx = await ubi.claim(); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, account, root }, null, 2)); +``` + +## Pre-check failures + +- Not whitelisted: stop and point the user to identity verification flows in GoodDocs. +- Missing contract address: stop; use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. +- Zero entitlement: communicate that nothing is claimable in the current period without guessing amounts. + +## Output contract + +- network +- resolved contract addresses +- tx hash +- claim outcome details when available diff --git a/.agents/skills/gooddollar/references/guides/faucet.md b/.agents/skills/gooddollar/references/guides/faucet.md new file mode 100644 index 00000000..d224e85e --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/faucet.md @@ -0,0 +1,76 @@ +# Faucet top-up guide + +Use when the user needs native gas top-up via GoodProtocol Faucet. + +## Goal + +Run deterministic pre-checks and call `topWallet` only when eligibility and limits pass. + +## Required inputs + +- `rpcUrl`, chain configuration, signer +- Faucet address for the chain (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) `Faucet` under the matching environment, or `NameService.getAddress` when the deployment documents the key) +- target user address + +## Execution flow + +1. Resolve Faucet contract address for the active chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). +2. Run `canTop(user)` as preflight. +3. Read `getToppingAmount(user)` and communicate expected top-up. +4. If eligible, call `topWallet(user)`. +5. Return tx hash and resulting top-up context. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const faucet = new ethers.Contract( + process.env.FAUCET_ADDRESS, + [ + "function canTop(address) view returns (bool)", + "function getToppingAmount(address) view returns (uint256)", + "function topWallet(address payable)", + ], + signer, +); + +const user = process.env.USER; +const canTop = await faucet.canTop(user); +if (!canTop) throw new Error("Faucet canTop returned false"); + +const amount = await faucet.getToppingAmount(user); +const tx = await faucet.topWallet(user); +const receipt = await tx.wait(); + +console.log( + JSON.stringify( + { + txHash: receipt.hash, + user, + toppingAmount: amount.toString(), + }, + null, + 2, + ), +); +``` + +## Common rejection reasons + +- `not authorized` +- daily or weekly cap reached +- banned address +- low effective `toTop` vs minimum threshold +- faucet inactive or wrong chain/address + +## Output contract + +- network +- faucet address +- `canTop` preflight result +- top-up amount estimate +- tx hash (when executed) diff --git a/.agents/skills/gooddollar/references/guides/gooddocs.md b/.agents/skills/gooddollar/references/guides/gooddocs.md new file mode 100644 index 00000000..5e5b4b39 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/gooddocs.md @@ -0,0 +1,37 @@ +# GoodDocs hub + +Canonical protocol documentation lives at [GoodDocs](https://docs.gooddollar.org/). + +This is a **routing guide** (quick link map), not a deep protocol analysis note. + +## Start here + +- [Welcome](https://docs.gooddollar.org/) +- [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) + +## User guides + +- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) (reserve-backed buy/sell; includes historical Ethereum/Kovan explorer workflows in the doc) +- [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) (MessagePassingBridge, fees, limits, troubleshooting) +- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity) + +## Developers + +- [Core contracts](https://docs.gooddollar.org/for-developers/core-contracts) (module overview; **do not** read contract addresses from GoodDocs—use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) +- [Developer guides index](https://docs.gooddollar.org/for-developers/developer-guides) +- [Integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (ERC-677, ERC-777, decimals by chain, `transferAndCall`, fees) +- [Use G$ streaming](https://docs.gooddollar.org/for-developers/developer-guides/use-gusd-streaming) (Superfluid on Celo, CFAv1Forwarder) + +## Chain IDs (bridge doc) + +| Network | Chain ID | +| -------- | -------- | +| Ethereum | 1 | +| Fuse | 122 | +| Celo | 42220 | +| XDC | 50 | + +## This repo + +- Action playbooks: `references/guides/*.md`. +- Rich ABIs: `references/contracts/*.abi.yaml`. diff --git a/.agents/skills/gooddollar/references/guides/goodsdks.md b/.agents/skills/gooddollar/references/guides/goodsdks.md new file mode 100644 index 00000000..8092e407 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/goodsdks.md @@ -0,0 +1,110 @@ +# GoodSDKs integration guide + +Use this guide when the task is SDK-first (app integration), not raw contract-first. + +## Scope + +GoodSDKs is the app integration layer for GoodDollar: + +- `@goodsdks/citizen-sdk` for identity and claim flows. +- `@goodsdks/react-hooks` for Wagmi React hooks. +- `@goodsdks/good-reserve` for reserve buy or sell flows. +- `@goodsdks/engagement-sdk` for engagement rewards flows. +- `@goodsdks/ui-components` and `@goodsdks/savings-widget` for web components. + +## Routing map + +- Check whitelist, identity root, FV link -> `@goodsdks/citizen-sdk` (`IdentitySDK`) +- Claim UBI with entitlement checks and fallback chains -> `@goodsdks/citizen-sdk` (`ClaimSDK`) +- React app with Wagmi and minimal glue code -> `@goodsdks/react-hooks` +- Buy or sell via reserve rails (Celo or XDC support rules) -> `@goodsdks/good-reserve` +- Reward app registration, claims, reward history -> `@goodsdks/engagement-sdk` +- Embeddable UI in non-React or mixed stacks -> `@goodsdks/ui-components` or `@goodsdks/savings-widget` + +## Deterministic setup + +Monorepo prerequisites: + +```bash +cd ~/Projects/GoodSDKs +corepack enable +yarn install --immutable +yarn build +``` + +Target one workspace: + +```bash +yarn workspace @goodsdks/citizen-sdk build +yarn workspace @goodsdks/react-hooks build +``` + +## Deterministic usage snippets + +Identity SDK: + +```ts +import { createPublicClient, createWalletClient, custom, http } from "viem"; +import { IdentitySDK } from "@goodsdks/citizen-sdk"; + +const publicClient = createPublicClient({ transport: http("https://forno.celo.org") }); +const walletClient = createWalletClient({ transport: custom(window.ethereum) }); + +const identitySDK = await IdentitySDK.init({ + publicClient, + walletClient, + env: "production", +}); + +const { isWhitelisted, root } = await identitySDK.getWhitelistedRoot("0xYourAccount"); +console.log({ isWhitelisted, root }); +``` + +Claim SDK: + +```ts +import { ClaimSDK, IdentitySDK } from "@goodsdks/citizen-sdk"; + +const identitySDK = await IdentitySDK.init({ publicClient, walletClient, env: "production" }); +const claimSDK = await ClaimSDK.init({ + publicClient, + walletClient, + identitySDK, + env: "production", +}); + +const entitlement = await claimSDK.checkEntitlement(); +if (entitlement.amount > 0n) { + const receipt = await claimSDK.claim(); + console.log(receipt.transactionHash); +} +``` + +React hooks: + +```tsx +import { useIdentitySDK, useClaimSDK, useGoodReserve } from "@goodsdks/react-hooks"; + +const identity = useIdentitySDK("production"); +const claim = useClaimSDK("production"); +const reserve = useGoodReserve("production"); +``` + +Reserve SDK: + +```ts +import { GoodReserveSDK } from "@goodsdks/good-reserve"; + +const sdk = new GoodReserveSDK(publicClient, walletClient, "production"); +const quote = await sdk.getBuyQuote(CUSD_ADDRESS, amountIn); +const tx = await sdk.buy(CUSD_ADDRESS, amountIn, (quote * 95n) / 100n); +console.log(tx.hash); +``` + +## Agent rules + +1. Prefer SDK methods first for app tasks. +2. Use contract-level guides only when SDK does not expose required behavior. +3. Do not invent SDK method names; align with package READMEs and exported types. +4. For chain support errors, report chain and env explicitly (do not silently fallback). +5. For UI tasks, prefer hooks or components over bespoke wallet and viem plumbing. diff --git a/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md b/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md new file mode 100644 index 00000000..e7b6f286 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md @@ -0,0 +1,110 @@ +# Envio HyperSync and HyperRPC + +Use this guide when the task is high-volume historical blockchain data fetch (events, blocks, txs), especially analytics and indexing workflows. + +## Official docs + +- HyperSync overview: [docs.envio.dev/docs/HyperSync/overview](https://docs.envio.dev/docs/HyperSync/overview) +- HyperRPC overview: [docs.envio.dev/docs/HyperRPC/overview-hyperrpc](https://docs.envio.dev/docs/HyperRPC/overview-hyperrpc) +- HyperRPC supported networks: [docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks](https://docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks) + +## What to use + +- **HyperSync**: preferred for new data pipelines and heavy historical scans. +- **HyperRPC**: read-only JSON-RPC drop-in for existing RPC code paths. + +## HyperRPC vs HyperSync (avoid mixing them up) + +- **HyperRPC** is a **hosted JSON-RPC URL** (same methods as `eth_getLogs`, `eth_blockNumber`, and so on). Any HTTP client or existing RPC stack can call it; put the API token in the URL path as Envio documents. +- **HyperSync** is a **separate high-throughput query API** used through **Envio client libraries** (for example `@envio-dev/hypersync-client` in Node). It is **not** “just another RPC endpoint” with the same ergonomics as a one-line `fetch` to `eth_getLogs` at large scale. + +## Decision rule + +1. For GoodDollar protocol history that exists on subgraphs, query the subgraph first and validate fields in `references/subgraphs/*-guide.md`. +2. If subgraph schema or freshness cannot satisfy the request, use **HyperSync** for large scans and pipelines, or **HyperRPC** when you must stay inside standard JSON-RPC. +3. For write operations (sending tx), use normal RPC providers; HyperRPC is read-only. + +## GoodDollar-relevant network coverage + +- Celo and XDC are supported on HyperRPC. +- Fuse is not currently listed; treat this as non-blocking and use existing providers for Fuse. + +## Access and auth + +- HyperRPC/HyperSync usage is account-based. +- HyperRPC requires an API key for reliable production use. +- Requests without API token are rate-limited and should be treated as non-production fallback only. +- Add API key in endpoint URL as documented by Envio. +- HyperRPC token pattern example from docs: `https://.rpc.hypersync.xyz/` + +## Agents: Envio API token when HyperSync is the best option + +After you decide **HyperSync** is the right tool for the user query (for example large historical scans or pipeline-scale log pulls where subgraphs are insufficient), check for a usable Envio credential in the execution environment (`ENVIO_API_TOKEN` for `@envio-dev/hypersync-client`, or the token Envio documents for your chosen URL pattern). + +If **no** Envio API token is available and you cannot complete the HyperSync path without it, **stop and explicitly ask the user** to provide an Envio API token (name the env var you need, typically `ENVIO_API_TOKEN`). Do not silently rely on anonymous or heavily rate-limited access as a substitute when HyperSync was already identified as the best approach. + +## Practical use in this repo + +- Keep subgraphs as first option for indexed protocol entities. +- Use HyperSync/HyperRPC when subgraph coverage is missing, stale, or insufficient for bulk historical pulls. +- When an agent chooses **HyperSync** as the best path and no Envio API token is available, follow **Agents: Envio API token when HyperSync is the best option** in this file and ask the user for `ENVIO_API_TOKEN` before proceeding. +- Keep contract addresses from [GoodProtocol/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only; use GoodDocs for product context, not for resolving contract addresses. +- For implementation details (client setup, query structure, supported methods), follow the Envio docs links above directly. + +## From block for historical fetches + +For **`eth_getLogs`**, HyperRPC, and HyperSync range queries, the lower bound is **`fromBlock`** (or the client’s equivalent). Prefer the deployment’s **`creationBlock`** from the matching row in `references/contracts/*.abi.yaml` (or **`meta.deploymentCreationBlocks`** where deployments are plain address strings) so scans do not start at genesis when you only need post-deploy history. Field placement for **`creationBlock`** is defined in `references/contracts/_rich-abi-yaml-format.md`. If you cannot determine the creation block, **`fromBlock` 0** is valid. + +## Prebuilt scripts (developers and local agents) + +These scripts avoid rediscovering HyperRPC wiring on every task. They require **Node.js 18 or newer** (global `fetch`). Paths like `scripts/...` are relative to the **GoodSkills repository root** (the directory that contains both `skills/` and `scripts/`), not relative to `skills/gooddollar/` alone. + +### Last N Identity `WhitelistedAdded` logs via HyperRPC + +- Script: `scripts/fetch-whitelist-events-hyperrpc.mjs` +- Default `EVENT_TOPIC0` matches `WhitelistedAdded(address)` on `IdentityV4`; override `EVENT_TOPIC0` for other events. +- Production Celo defaults: `CONTRACT_ADDRESS` defaults to `Identity` from `production-celo` in [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`0xC361A6E67822a0EDc17D899227dd9FC50BD62F42`). If `HYPERRPC_URL` is unset, the script builds `https://celo.rpc.hypersync.xyz/` from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN`. +- Optional env: `HYPERRPC_URL` (overrides token-based default), `CONTRACT_ADDRESS`, `LIMIT` (default `500`), `STEP` (default `2000`), `FROM_BLOCK` (decimal; omit to read **`creationBlock`** for `CONTRACT_ADDRESS` from `ABI_PATH` or the default `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`), `ABI_PATH`, `TO_BLOCK` (default `latest`). **`fromBlock`** behavior is described in **From block for historical fetches** above. + +```bash +cd /path/to/GoodSkills +export HYPERRPC_API_TOKEN='' +node scripts/fetch-whitelist-events-hyperrpc.mjs +``` + +Web-only assistants without a shell cannot run the file; they should return the same env keys and command text so the user runs it locally. + +## HyperSync client minimal path (install required) + +HyperSync uses the official client. Install and query pattern (Celo example URLs from [Envio Celo docs](https://docs.envio.dev/docs/HyperIndex/celo)): + +```bash +npm install @envio-dev/hypersync-client +export ENVIO_API_TOKEN='' +``` + +Save as a `.mjs` file (or use `"type": "module"` in a local `package.json`) and run with `node`: + +```javascript +import { HypersyncClient, presetQueryLogsOfEvent } from "@envio-dev/hypersync-client"; + +const client = new HypersyncClient({ + url: "https://celo.hypersync.xyz", + apiToken: process.env.ENVIO_API_TOKEN, +}); + +const identity = "0x..."; +const whitelistedAddedTopic0 = + "0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f"; + +const fromBlock = 17237952; +const toBlock = await client.getHeight(); + +const query = presetQueryLogsOfEvent(identity, whitelistedAddedTopic0, fromBlock, toBlock); +const res = await client.get(query); +console.log(res.data.logs.length); +``` + +The example **`fromBlock`** matches **`creationBlock`** for production Celo Identity in `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`; see **From block for historical fetches** above. + +Full API and streaming patterns: [HyperSync clients](https://docs.envio.dev/docs/HyperSync/hypersync-clients) and the package README for `@envio-dev/hypersync-client`. diff --git a/.agents/skills/gooddollar/references/guides/invite-bounties.md b/.agents/skills/gooddollar/references/guides/invite-bounties.md new file mode 100644 index 00000000..d60aafdd --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/invite-bounties.md @@ -0,0 +1,115 @@ +# Invite bounties guide + +Use when the task is to verify or execute inviter-invitee bounty payout flow and explain why payout is blocked. + +## Goal + +Check eligibility deterministically, execute payout only when eligible, and return exact failure reason when not eligible. + +## Required inputs + +- target chain +- `InvitesV2` address +- `Identity` address +- optional `UBISchemeV2` address when claims threshold is active +- invitee address +- inviter address +- rpc url and signer + +## Execution flow + +1. Resolve contract addresses from `deployment.json`. +2. Read invitee state from `users(invitee)` and global thresholds (`minimumClaims`, `minimumDays`, `active`). +3. Check current eligibility with `canCollectBountyFor(invitee)`. +4. If not eligible, read identity whitelist for invitee and inviter and return concrete blocker. +5. If eligible, execute `bountyFor(invitee)` or `collectBounties()` and return tx hash plus payout values from events. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const invites = new ethers.Contract( + process.env.INVITES_ADDRESS, + [ + "function canCollectBountyFor(address) view returns (bool)", + "function bountyFor(address)", + "function users(address) view returns (bytes32,address,uint40,uint24,bool,uint256)", + "function minimumClaims() view returns (uint256)", + "function minimumDays() view returns (uint256)", + "function active() view returns (bool)", + "event InviterBounty(address indexed inviter,address indexed invitee,uint256 bountyPaid,uint256 inviterLevel,bool earnedLevel)" + ], + signer, +); + +const identity = new ethers.Contract( + process.env.IDENTITY_ADDRESS, + [ + "function isWhitelisted(address) view returns (bool)" + ], + provider, +); + +const invitee = process.env.INVITEE_ADDRESS; +const inviter = process.env.INVITER_ADDRESS; + +const [isActive, eligible, inviteeWhitelisted, inviterWhitelisted, minClaims, minDays] = await Promise.all([ + invites.active(), + invites.canCollectBountyFor(invitee), + identity.isWhitelisted(invitee), + identity.isWhitelisted(inviter), + invites.minimumClaims(), + invites.minimumDays(), +]); + +if (!isActive) throw new Error("Invites contract is inactive"); +if (!eligible) { + throw new Error( + `Not eligible. inviteeWhitelisted=${inviteeWhitelisted} inviterWhitelisted=${inviterWhitelisted} minimumClaims=${minClaims} minimumDays=${minDays}`, + ); +} + +const tx = await invites.bountyFor(invitee); +const receipt = await tx.wait(); +const bountyEvent = receipt.logs + .map((log) => { + try { + return invites.interface.parseLog(log); + } catch { + return null; + } + }) + .find((e) => e && e.name === "InviterBounty"); + +console.log( + JSON.stringify( + { + txHash: receipt.hash, + invitee, + inviter, + bountyPaid: bountyEvent?.args?.bountyPaid?.toString() ?? null, + }, + null, + 2, + ), +); +``` + +## Failure handling + +- invitee or inviter is not currently whitelisted +- reverification is due and whitelist check fails until re-authentication +- minimum claims or minimum days is not met +- bounty already paid or bounty-at-join is zero +- contract inactive or wrong deployment addresses + +## Output contract + +- network and addresses used +- eligibility status and blockers +- tx hash when sent +- payout values when available from logs diff --git a/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md b/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md new file mode 100644 index 00000000..4ad9fd26 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md @@ -0,0 +1,116 @@ +# Fuse to CELO staking migration guide + +Use when the user wants to migrate an existing Fuse governance stake into a CELO destination savings flow. In this flow, Fuse `GovernanceStakingV2` is the old staking contract (source) and **`GooddollarSavingsStream`** on Celo is the destination savings contract ([`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). + +## Goal + +Close a user stake on Fuse, bridge the resulting G$ to CELO, and stake on CELO for that user in a controlled backend flow. + +## Required inputs + +- user address on Fuse and corresponding destination address on CELO +- Fuse `GovernanceStakingV2` address (`production.GovernanceStakingV2` in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json)) +- Fuse G$ token address and bridge contract address (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json): `GoodDollar`, `MpbBridge` under `production`) +- CELO G$ token address (from `production-celo` in the same file) and destination savings contract address +- backend signer or service wallet with required execution permissions +- chain RPC URLs for Fuse and CELO + +## Address resolution quick table + +| Purpose | Network | Source key/path | Value | +|---|---|---|---| +| Governance staking (source close) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStakingV2` | `0xB7C3e738224625289C573c54d402E9Be46205546` | +| Governance staking (previous) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStaking` | `0xFAF457Fb4A978Be059506F6CD41f9B30fCa753b0` | +| Fuse G$ token | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GoodDollar` | `0x495d133B938596C9984d462F007B676bDc57eCEC` | +| Fuse bridge | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.MpbBridge` | `0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5` | +| Destination savings | CELO (`networkId: 42220`) | [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B) / `GooddollarSavingsStream` | `0x059ee811414230d1Fb157878D2b491240F4D8d3B` (`process.env.CELO_SAVINGS`) | + +Canonical sources: + +- [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) +- [Fuse explorer contract (GovernanceStakingV2 address)](https://explorer.fuse.io/address/0xB7C3e738224625289C573c54d402E9Be46205546?tab=contract) +- [Ubeswap gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) +- [GoodBridge bridge helper normalization](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) + +## Token decimals and `MessagePassingBridge` LZ fees + +Production deployments checked on-chain: Fuse `GoodDollar` uses **2** decimals and Celo `GoodDollar` uses **18**. Fuse `GovernanceStakingV2` (sG$) uses **2** decimals. Resolve `decimals()` from each live token in your runner so you stay correct if deployments change. + +On the Fuse `MpbBridge` (`MessagePassingBridge`), `canBridge(from, amount)` and `bridgeToWithLz(..., amount, ...)` use the **raw G$ burn amount** in source token decimals. Off-chain **`estimateSendFee`’s `_normalizedAmount` argument** must match what the contract builds internally: **`normalizeFromTokenTo18Decimals(amount, IERC20(nativeToken()).decimals())`** ([`BridgeHelperLibrary`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol)). Format displayed amounts per chain (`formatUnits`/UI) using each chain’s G$ decimals. + +## Execution flow + +1. Confirm allowance for whichever asset your flow spends first (for example sG$ allowance to the backend on Fuse `GovernanceStakingV2`, or Fuse G$ allowance if you pull G$ directly), before any transfers or bridges. +2. Verify current stake state on Fuse before closing: + - stake token balance + - withdrawable stake amount + - pending rewards if any +3. Execute Fuse unstake or close flow on governance staking (`withdrawStake` or equivalent full-close path). +4. Compute net G$ available for migration after unstake completion and any reward claim behavior. +5. Bridge G$ from Fuse to CELO using the configured bridge path and track the transfer id or tx hash pair. +6. Wait for destination finalization on CELO and verify credited G$ balance at the backend execution wallet. +7. Approve destination savings contract to spend migrated G$ amount. +8. Stake for the user on CELO with `stakeFor(amount, recipient)` on `GooddollarSavingsStream` (G$ native Super Token; approve the savings contract, not only ERC20 GoodDollar from `deployment.json` if your wallet holds the Super Token). +9. Return a migration result with both chain tx hashes and final CELO staked amount. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const fuse = new ethers.JsonRpcProvider(process.env.FUSE_RPC_URL); +const celo = new ethers.JsonRpcProvider(process.env.CELO_RPC_URL); +const signerFuse = new ethers.Wallet(process.env.BACKEND_PK, fuse); +const signerCelo = new ethers.Wallet(process.env.BACKEND_PK, celo); + +const user = process.env.USER_ADDRESS; +const migrateAmount = BigInt(process.env.MIGRATE_AMOUNT); + +const celoGd = new ethers.Contract( + process.env.CELO_GD_TOKEN, + [ + "function approve(address spender,uint256 amount) returns (bool)", + "function balanceOf(address) view returns (uint256)", + ], + signerCelo, +); + +const savings = new ethers.Contract( + process.env.CELO_SAVINGS, + ["function stakeFor(uint256 amount,address recipient)"], + signerCelo, +); + +const approveTx = await celoGd.approve(process.env.CELO_SAVINGS, migrateAmount); +await approveTx.wait(); + +const stakeTx = await savings.stakeFor(migrateAmount, user); +const receipt = await stakeTx.wait(); + +console.log( + JSON.stringify( + { + user, + celoStakeTx: receipt.hash, + migratedAmount: migrateAmount.toString(), + }, + null, + 2, + ), +); +``` + +## Pre-check failures + +- User allowance missing on Fuse: stop and request allowance tx from user. +- Stake close fails on Fuse: stop and return exact revert reason before bridge. +- Bridge transfer not finalized on CELO: do not call `stakeFor` until destination balance is confirmed. +- CELO savings approval missing or too low: re-approve exact amount before staking. + +## Output contract + +- user address +- Fuse unstake tx hash +- bridge tx hash or transfer identifier +- CELO stake tx hash +- final staked amount on CELO diff --git a/.agents/skills/gooddollar/references/guides/on-off-ramp.md b/.agents/skills/gooddollar/references/guides/on-off-ramp.md new file mode 100644 index 00000000..d2127051 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/on-off-ramp.md @@ -0,0 +1,77 @@ +# On- and off-ramp service guide + +Use when implementing service flow where fiat ramps support a listed stable token (for example cUSD), and the app needs stable <-> G$ swap on-chain. + +## Goal + +Operate deterministic clone-based swap routing with explicit chain/factory verification and slippage guard. + +## Required inputs + +- target chain and factory address +- owner address used for clone derivation +- stable token and G$ token addresses +- direction: on-ramp or off-ramp +- `minAmount` guard +- signer and rpc url + +## Execution flow + +1. Resolve factory for target chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`BuyGDFactory` / `BuyGDFactoryV2` under `production-celo`, or the key your deployment uses). +2. Compute expected clone via `predict(owner)`. +3. If clone not yet deployed for flow, call `create(owner)` or `createAndSwap(owner, minAmount)`. +4. Execute stable -> G$ (on-ramp) or G$ -> stable (off-ramp) through clone path. +5. Return chain id, factory, predicted clone, effective clone, tx hashes. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const factory = new ethers.Contract( + process.env.BUY_GD_CLONE_FACTORY, + [ + "function predict(address) view returns (address)", + "function create(address) returns (address)", + "function createAndSwap(address,uint256) returns (address)", + ], + signer, +); + +const owner = process.env.OWNER; +const minAmount = ethers.parseUnits(process.env.MIN_AMOUNT, Number(process.env.DECIMALS_OUT)); +const predicted = await factory.predict(owner); + +const tx = await factory.createAndSwap(owner, minAmount); +const receipt = await tx.wait(); + +console.log( + JSON.stringify( + { + txHash: receipt.hash, + owner, + predictedClone: predicted, + chainId: (await provider.getNetwork()).chainId.toString(), + }, + null, + 2, + ), +); +``` + +## Failure handling + +- predicted clone mismatch with trusted expectation +- wrong chain or wrong factory address +- swap output below `minAmount` +- stale router/oracle or exchange configuration + +## Output contract + +- network and chain id +- factory address +- predicted and actual clone addresses +- tx hashes diff --git a/.agents/skills/gooddollar/references/guides/save.md b/.agents/skills/gooddollar/references/guides/save.md new file mode 100644 index 00000000..d0e716cb --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/save.md @@ -0,0 +1,93 @@ +# Save and stake guide + +Use when the user wants to stake G$, withdraw rewards, or exit stake. Staking economics sit alongside other protocol allocations described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). + +## GoodDocs alignment + +- Token integration and fee awareness: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (`_processFees`, decimals per chain). +- Contract addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example staking and G$ token keys under `production` / `production-celo`). GoodDocs covers behavior and decimals patterns, not canonical deployment addresses. + +## Goal + +Run staking actions with balance and allowance safety checks. + +## Required inputs + +- `nameServiceAddress` or explicit staking and token addresses +- `amount` or `shares` depending on the action +- `rpcUrl`, chain configuration, signer + +## Execution flow + +1. Resolve staking and G$ token addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or, when the deployment documents the key, from `NameService.getAddress` on chain. +2. Read token balance and allowance. +3. Approve the staking contract when `stake` uses `transferFrom`. +4. Execute `stake`, `withdrawRewards`, or `withdrawStake` as requested. +5. Return tx hash and key resulting balances or events. + +## Deterministic snippets + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const token = new ethers.Contract( + process.env.GOODDOLLAR_ADDRESS, + [ + "function balanceOf(address) view returns (uint256)", + "function allowance(address,address) view returns (uint256)", + "function approve(address,uint256) returns (bool)", + ], + signer, +); + +const staking = new ethers.Contract( + process.env.STAKING_ADDRESS, + [ + "function stake(uint256)", + "function withdrawRewards()", + "function withdrawStake(uint256)", + ], + signer, +); +``` + +Stake: + +```js +const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); +const owner = await signer.getAddress(); +const allowance = await token.allowance(owner, process.env.STAKING_ADDRESS); +if (allowance < amount) { + const approveTx = await token.approve(process.env.STAKING_ADDRESS, amount); + await approveTx.wait(); +} +const tx = await staking.stake(amount); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: "stake" }, null, 2)); +``` + +Withdraw rewards: + +```js +const tx = await staking.withdrawRewards(); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawRewards" }, null, 2)); +``` + +Withdraw stake: + +```js +const shares = ethers.parseUnits(process.env.SHARES, Number(process.env.DECIMALS)); +const tx = await staking.withdrawStake(shares); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawStake" }, null, 2)); +``` + +## Failure handling + +- Insufficient balance: report shortfall. +- Approval issues: report token, spender, and required allowance. +- Reverts: return attempted function and parameters without guessing custom errors. diff --git a/.agents/skills/gooddollar/references/guides/stream.md b/.agents/skills/gooddollar/references/guides/stream.md new file mode 100644 index 00000000..6cbf7a45 --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/stream.md @@ -0,0 +1,135 @@ +# Stream guide + +Primary references for stream execution are local ABI assets in this repo: + +- `references/contracts/CFAv1Forwarder.abi.yaml` +- `references/contracts/ConstantFlowAgreementV1.abi.yaml` +- `references/contracts/Superfluid.abi.yaml` +- `references/contracts/SuperToken.abi.yaml` + +## Goal + +Create, update, or delete Superfluid constant flows using deterministic contract calls and local ABI references. + +## Protocol facts used by this guide + +- Forwarder path uses `CFAv1Forwarder.createFlow`, `updateFlow`, `deleteFlow`. +- Host path uses `Superfluid.callAgreement` with CFA calldata for `createFlow`, `updateFlow`, `deleteFlow`. +- Stream token is a SuperToken; flow rates are `int96` in token-wei per second. +- `getBufferAmountByFlowrate(token, flowRate)` is the canonical pre-check for required buffer. + +## Two implementation styles in this repo + +1. **Forwarder (matches GoodDocs):** call CFAv1Forwarder with token, sender, receiver, flowRate, userData. +2. **Host callAgreement:** encode CFA `createFlow` / `updateFlow` / `deleteFlow` and call `Superfluid.callAgreement`. + +## Minimal method map + +- Forwarder: + - `createFlow(address token, address receiver, int96 flowrate, bytes userData)` + - `updateFlow(address token, address receiver, int96 flowrate, bytes userData)` + - `deleteFlow(address token, address sender, address receiver, bytes userData)` + - `getBufferAmountByFlowrate(address token, int96 flowrate)` +- Host: + - `callAgreement(address agreementClass, bytes callData, bytes userData)` +- CFA: + - `createFlow(address token, address receiver, int96 flowRate, bytes ctx)` + - `updateFlow(address token, address receiver, int96 flowRate, bytes ctx)` + - `deleteFlow(address token, address sender, address receiver, bytes ctx)` + +## Required inputs + +- G$ Super Token address for the environment +- CFA forwarder address, or Superfluid host address plus CFA agreement address +- `action`: create, update, delete +- `receiver`, `flowRate` where applicable +- `rpcUrl`, chain configuration, signer + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const forwarder = new ethers.Contract( + process.env.CFA_FORWARDER, + [ + "function createFlow(address,address,int96,bytes)", + "function updateFlow(address,address,int96,bytes)", + "function deleteFlow(address,address,address,bytes)", + ], + signer, +); + +const token = process.env.SUPER_TOKEN; +const sender = await signer.getAddress(); +const receiver = process.env.RECEIVER; +const flowRate = BigInt(process.env.FLOW_RATE); + +if (process.env.ACTION === "create") { + const tx = await forwarder.createFlow(token, receiver, flowRate, "0x"); + const receipt = await tx.wait(); + console.log(JSON.stringify({ txHash: receipt.hash, action: "create" }, null, 2)); +} + +if (process.env.ACTION === "update") { + const tx = await forwarder.updateFlow(token, receiver, flowRate, "0x"); + const receipt = await tx.wait(); + console.log(JSON.stringify({ txHash: receipt.hash, action: "update" }, null, 2)); +} + +if (process.env.ACTION === "delete") { + const tx = await forwarder.deleteFlow(token, sender, receiver, "0x"); + const receipt = await tx.wait(); + console.log(JSON.stringify({ txHash: receipt.hash, action: "delete" }, null, 2)); +} +``` + +Host callAgreement example: + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const cfa = new ethers.Interface([ + "function createFlow(address,address,int96,bytes)", + "function updateFlow(address,address,int96,bytes)", + "function deleteFlow(address,address,address,bytes)", +]); + +const host = new ethers.Contract( + process.env.SUPERFLUID_HOST, + ["function callAgreement(address,bytes,bytes) returns (bytes)"], + signer, +); + +const token = process.env.SUPER_TOKEN; +const sender = await signer.getAddress(); +const receiver = process.env.RECEIVER; +const flowRate = BigInt(process.env.FLOW_RATE); + +let callData = "0x"; +if (process.env.ACTION === "create") { + callData = cfa.encodeFunctionData("createFlow", [token, receiver, flowRate, "0x"]); +} +if (process.env.ACTION === "update") { + callData = cfa.encodeFunctionData("updateFlow", [token, receiver, flowRate, "0x"]); +} +if (process.env.ACTION === "delete") { + callData = cfa.encodeFunctionData("deleteFlow", [token, sender, receiver, "0x"]); +} + +const tx = await host.callAgreement(process.env.CFA_ADDRESS, callData, "0x"); +const receipt = await tx.wait(); +console.log(JSON.stringify({ txHash: receipt.hash, action: process.env.ACTION }, null, 2)); +``` + +## Failure handling + +- Wrong network or missing addresses: stop and return missing host or forwarder or token addresses. +- Insufficient buffer: use `getBufferAmountByFlowrate` and reduce flow rate or top up balance. +- Revert on create or update: verify token is a SuperToken and flowRate is positive. diff --git a/.agents/skills/gooddollar/references/guides/swap.md b/.agents/skills/gooddollar/references/guides/swap.md new file mode 100644 index 00000000..3db8c2dc --- /dev/null +++ b/.agents/skills/gooddollar/references/guides/swap.md @@ -0,0 +1,83 @@ +# Swap guide + +Use for buying or selling G$ through Mento-connected contracts on networks where they appear in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `MentoBroker`, `MentoReserve`, `MentoExchangeProvider`, `MentoExpansionController` keys under `production-celo` or `production-xdc`). GoodDocs describes Mento product behavior, not deployment addresses. + +## GoodDocs alignment + +- Reserve and buy or sell mechanics at the protocol level: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [Buy and Sell G$ user guide](https://docs.gooddollar.org/user-guides) (includes reserve AMM narrative; older explorer step-by-step for Ethereum testnets remains in that page for reference). +- Integration patterns and decimals: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token). + +## Goal + +Execute bounded swaps using broker quotes and correct allowances. + +## Required inputs + +- `direction` as buy or sell +- broker and exchange identifiers for the deployment +- amounts in correct token decimals for the chain +- `rpcUrl`, chain configuration, signer + +## Execution flow + +1. Confirm Mento Broker (and related) addresses for the chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. +2. Fetch quote (`getAmountOut` or `getAmountIn` depending on direction and ABI). +3. Apply slippage bounds. +4. Approve the spent token for the broker when required. +5. Call `swapIn` or `swapOut` per your integration. +6. Return tx hash and effective amounts. + +## Deterministic snippet + +```js +import { ethers } from "ethers"; + +const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); +const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); + +const broker = new ethers.Contract( + process.env.BROKER_ADDRESS, + [ + "function getAmountOut(address,address,uint256) view returns (uint256)", + "function swapIn(address,address,uint256,uint256) returns (uint256)", + ], + signer, +); + +const tokenIn = new ethers.Contract( + process.env.TOKEN_IN, + [ + "function allowance(address,address) view returns (uint256)", + "function approve(address,uint256) returns (bool)", + ], + signer, +); + +const amountIn = ethers.parseUnits(process.env.AMOUNT_IN, Number(process.env.DECIMALS_IN)); +const quotedOut = await broker.getAmountOut(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn); +const slippageBps = BigInt(process.env.SLIPPAGE_BPS); +const minOut = quotedOut * (10000n - slippageBps) / 10000n; + +const owner = await signer.getAddress(); +const allowance = await tokenIn.allowance(owner, process.env.BROKER_ADDRESS); +if (allowance < amountIn) { + const approveTx = await tokenIn.approve(process.env.BROKER_ADDRESS, amountIn); + await approveTx.wait(); +} + +const tx = await broker.swapIn(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn, minOut); +const receipt = await tx.wait(); +console.log( + JSON.stringify( + { txHash: receipt.hash, amountIn: amountIn.toString(), minOut: minOut.toString() }, + null, + 2, + ), +); +``` + +## Failure handling + +- No deployment on chain: direct the user to an environment that defines the needed keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `production-celo` with `MentoBroker`). +- Stale quote or tight slippage: refresh quote or relax bounds with user consent. +- Allowance or balance shortfall: report exact delta. diff --git a/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md b/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md new file mode 100644 index 00000000..36af6398 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md @@ -0,0 +1,35 @@ +# Subgraph query discipline (GoodDollar) + +## Subgraph vs RPC + +- **Subgraph:** historical events, lists, aggregates, time ranges, analytics. Data lags chain head. +- **RPC / SDK:** current balances, live `claim` eligibility, exact view calls. Prefer for user-facing “what is true right now”. + +## Generic GraphQL mechanics + +Graph-node generates `entity`, `entities`, `Entity_filter`, `Entity_orderBy`, pagination, and `_meta` from each deployment’s `schema.graphql`. For scalar rules, filters, `_meta`, and common pitfalls, see [The Graph — Querying a subgraph](https://thegraph.com/docs/en/querying/graphql-api/). + +## GoodDollar-specific + +- **Addresses in `where` clauses:** normalize to **lowercase** hex strings; subgraphs store addresses lowercased. +- **BigInt fields:** query as **string** literals in GraphQL JSON (e.g. `"1000000000000000000"`). +- **Schema truth:** entity names differ per deployment. Run introspection or read the deployment’s `schema.graphql` under the relevant package in [GoodDollar/GoodSubGraphs](https://github.com/GoodDollar/GoodSubGraphs) before assuming field names. + +## Meta block + +Use `_meta { block { number } }` to detect how far behind indexing is when debugging stale data. + +```graphql +{ + _meta { + block { + number + } + hasIndexingErrors + } +} +``` + +## When subgraphs are not enough + +If the subgraph cannot answer the question (missing entities or fields, or stale indexing per `_meta`), switch to the decision rules in `references/guides/hypersync-hyperrpc.md`. When **HyperSync** is the best fit and no Envio API token is available in the environment, **ask the user directly** for `ENVIO_API_TOKEN` (or the token your HyperSync client expects) before running large scans; do not silently use anonymous quota as a stand-in. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md b/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md new file mode 100644 index 00000000..a266ceab --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md @@ -0,0 +1,54 @@ +# GoodCollective — Subgraph Usage Guide + +Companion to `goodcollective.graphql`. + +## Endpoint + +- Explorer: [GoodCollective](https://thegraph.com/explorer/subgraphs/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y?view=Query&chain=arbitrum-one) +- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y` + +--- + +## Terminology: “claim” here is not daily UBI + +In this subgraph, **Claim** and **ClaimEvent** refer to **GoodCollective reward or pool claim flows**, not the protocol’s **daily UBI claim** from `UBIScheme` / `UBISchemeV2`. + +When a user says **“claim”** in normal GoodDollar product language, they almost always mean **claim daily UBI**. For that, use the GoodDollar Celo subgraph (`walletStats` / claim-related aggregates) and on-chain `claim` per `references/guides/claim.md` — do not answer “last N UBI claims” from GoodCollective **Claim** alone. + +--- + +## Entity Overview + +### Core collective graph + +**Collective** — pool identity, limits/settings links, totals, and claim/payment counters. +**Donor** / **Steward** — participant-level donation/support state. +**DonorCollective** / **StewardCollective** — join entities tying participants to a collective. + +### Claim and support flow + +**Claim** and **ClaimEvent** — GoodCollective **reward** claim lifecycle and per-claim reward events (not daily UBI from UBIScheme). +**SupportEvent** — support/donation change events across donor/collective links. + +### Metadata and policy entities + +**IpfsCollective** — IPFS metadata projection. +**PoolSettings**, **UBILimits**, **SafetyLimits** — pool policy and operational bounds. +**ProvableNFT** — NFT linkage used in claim/reward flows. + +--- + +## Typical Questions This Subgraph Answers + +- Which donors/stewards are attached to a collective? +- How much was donated/rewarded per collective and per participant? +- Which claim events occurred and what reward quantities were emitted? +- What limits and settings govern a specific collective pool? + +--- + +## Query Discipline + +- Use authenticated gateway access for programmatic queries. +- Lowercase address-like identifiers where applicable. +- Validate `_meta` before operational dashboards or reporting exports. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql b/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql new file mode 100644 index 00000000..e503db9c --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql @@ -0,0 +1,200 @@ +scalar BigDecimal + +scalar BigInt + +scalar Boolean + +scalar Bytes + +type Claim { + id: String! + collective: Collective! + txHash: String! + networkFee: BigInt! + totalRewards: BigInt! + events: [ClaimEvent!]! + timestamp: Int! +} + +type ClaimEvent { + id: String! + eventType: Int! + timestamp: Int! + quantity: BigInt! + rewardPerContributor: BigInt! + contributors: [Steward!]! + nft: ProvableNFT + claim: Claim! +} + +type Collective { + id: String! + pooltype: PoolType! + ipfs: IpfsCollective + settings: PoolSettings! + ubiLimits: UBILimits + limits: SafetyLimits + donors: [DonorCollective!] + stewards: [StewardCollective!] + projectId: String! + isVerified: Boolean! + poolFactory: String! + timestamp: Int! + paymentsMade: Int! + totalDonations: BigInt! + totalRewards: BigInt! + claims: [Claim!]! +} + +type Donor { + id: String! + timestamp: Int! + totalDonated: BigInt! + collectives: [DonorCollective!]! +} + +""" +Represents the relationship between a Donor and a Collective + +""" + +type DonorCollective { + id: String! + donor: Donor! + collective: Collective! + contribution: BigInt! + flowRate: BigInt! + timestamp: Int! + events: [SupportEvent!]! +} + +scalar Float + +scalar ID + +""" +4 bytes signed integer + +""" + +scalar Int + +""" +8 bytes signed integer + +""" + +scalar Int8 + +type IpfsCollective { + id: String! + name: String! + description: String! + rewardDescription: String + goodidDescription: String + email: String + website: String + twitter: String + instagram: String + threads: String + infoLabel: String + headerImage: String! + logo: String! + images: [String!] +} + +type PoolSettings { + id: String! + nftType: BigInt! + manager: Bytes! + membersValidator: Bytes! + uniquenessValidator: Bytes! + rewardToken: Bytes! +} + +enum PoolType { + DirectPayments + UBI +} + +type ProvableNFT { + id: String! + owner: String! + hash: String! + stewards: [Steward!]! + collective: Collective +} + +type SafetyLimits { + id: String! + maxTotalPerMonth: BigInt! + maxMemberPerMonth: BigInt! + maxMemberPerDay: BigInt! +} + +type Steward { + """ + { user address} + + """ + id: String! + """ + Number of actions performed + + """ + actions: Int! + totalEarned: BigInt! + totalUBIEarned: BigInt! + """ + NFT's minted to steward + + """ + nfts: [ProvableNFT!]! + """ + Collectives the steward is apart of + + """ + collectives: [StewardCollective!]! +} + +""" +Represents the relationship between a Steward and a Collective + +""" + +type StewardCollective { + id: String! + steward: Steward! + collective: Collective! + actions: Int! + totalEarned: BigInt! +} + +scalar String + +type SupportEvent { + id: String! + networkFee: BigInt! + donor: Donor! + collective: Collective! + donorCollective: DonorCollective! + contribution: BigInt! + previousContribution: BigInt! + isFlowUpdate: Boolean! + flowRate: BigInt! + previousFlowRate: BigInt! + timestamp: Int! +} + +scalar Timestamp + +type UBILimits { + id: String! + cycleLengthDays: BigInt! + claimPeriodDays: BigInt! + minActiveUsers: BigInt! + claimForEnabled: Boolean! + maxClaimAmount: BigInt! + maxClaimers: BigInt! + onlyMembers: Boolean! +} diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md new file mode 100644 index 00000000..347951f4 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md @@ -0,0 +1,104 @@ +# GoodDollar Celo — Subgraph Usage Guide + +Companion to `gooddollar-celo.graphql`. + +## Endpoint + +- Explorer: [GoodDollarCelo](https://thegraph.com/explorer/subgraphs/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw?view=Query&chain=arbitrum-one) +- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw` + +--- + +## Entity Overview + +### UBI and usage statistics + +**DailyUBI** — day-level UBI pool/quota activity and cycle fields. +**WalletStat** — wallet behavior aggregates: tx counts/values, claim stats, active/whitelist indicators. +**TransactionStat** — day-level transaction totals and circulation view. +**GlobalStatistics** — global claim and distribution rollups. + +### Additional UBI history entities + +**UBICollected** — collected UBI/community-pool values by block event. +**UBIHistory** — timeline totals for daily UBI/community-pool. + +--- + +## Field Availability Reference (use before drafting queries) + +Use this section to validate what the subgraph already provides before switching data sources. + +### DailyUBI + +- `id` — day index key (`unix / 86400`) +- `pool` — UBI cycle pool for that day +- `quota` — daily UBI amount per eligible claimer +- `activeUsers` — active users count in scheme context +- `totalUBIDistributed` — amount actually claimed/distributed that day +- `totalClaims` — claim tx count for that day +- `newClaimers` — newly whitelisted users for that day +- `timestamp` — last update timestamp for the record +- `ubiSchemeAddress` — UBIScheme address used for the record +- `balance` — G$ balance held by UBIScheme +- `cycleLength` — current cycle length +- `dayInCycle` — current day position inside cycle + +### WalletStat + +- `id` — wallet address +- `dateAppeared` — first indexed wallet activity timestamp +- `balance` — running token balance from transfers +- `inTransactionsCount`, `inTransactionsValue` — incoming tx count and value +- `outTransactionsCount`, `outTransactionsValue` — outgoing tx count and value +- `totalTransactionsCount`, `totalTransactionsValue` — total tx count and value +- `inTransactionsCountClean`, `inTransactionsValueClean` — incoming metrics excluding contract-address flows +- `outTransactionsCountClean`, `outTransactionsValueClean` — outgoing metrics excluding contract-address flows +- `totalTransactionsCountClean`, `totalTransactionsValueClean` — total clean traffic metrics +- `lastClaimed` — timestamp of latest UBI claim +- `totalClaimedCount`, `totalClaimedValue` — total claims and cumulative claimed value +- `claimStreak`, `longestClaimStreak` — current and best historical streaks +- `isWhitelisted` — current whitelist status +- `isActiveUser` — current active-user status +- `dateJoined` — first-whitelist timestamp +- `lastTransactionFrom`, `lastTransactionTo` — latest outgoing and incoming tx timestamps + +### TransactionStat + +- `id` — bucket key (day key or `"aggregated"`) +- `dayStartBlockNumber` — first block in bucket +- `transactionsCount`, `transactionsValue` — all transfer tx count and value +- `transactionsCountClean`, `transactionsValueClean` — transfer metrics excluding contract-address flows +- `totalInCirculation` — inferred circulating supply from mint or burn behavior + +### GlobalStatistics + +- `id` — fixed key (`"statistics"`) +- `TransactionStat` — link to aggregated transaction stats +- `totalUBIDistributed` — lifetime distributed UBI +- `uniqueClaimers` — tracked unique claimers via whitelist add or remove +- `totalClaims` — lifetime UBI claim events + +### Explorer naming + +- Singular names (`dailyUBI`, `walletStat`) fetch by `id` +- Plural names (`dailyUBIs`, `walletStats`) query lists with filters and pagination + +--- + +## Typical Questions This Subgraph Answers + +- How much UBI was distributed on a day/cycle? +- Which wallets are active or recently claiming? +- What are aggregate tx/circulation trends? +- How did collected UBI/community-pool values evolve over time? + +--- + +## Query Discipline + +- Use lowercase address strings in filters. +- Use string-safe handling for large integer values. +- Use `_meta` to validate freshness before cross-day analytics. +- Use authenticated gateway access for programmatic queries. +- Before claiming a field or entity is missing, verify availability from this guide and schema first. diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql new file mode 100644 index 00000000..e544a0dc --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql @@ -0,0 +1,113 @@ +scalar BigDecimal + +scalar BigInt + +scalar Boolean + +scalar Bytes + +type DailyUBI { + id: ID! + pool: BigInt! + quota: BigInt! + activeUsers: BigInt! + totalUBIDistributed: BigInt! + totalClaims: BigInt! + newClaimers: BigInt! + timestamp: BigInt! + ubiSchemeAddress: Bytes + balance: BigInt! + cycleLength: BigInt! + dayInCycle: BigInt! +} + +scalar Float + +type GlobalStatistics { + id: ID! + TransactionStat: TransactionStat + totalUBIDistributed: BigInt! + uniqueClaimers: BigInt! + totalClaims: BigInt! +} + +scalar ID + +""" +4 bytes signed integer +""" + +scalar Int + +""" +8 bytes signed integer +""" + +scalar Int8 + +""" +Defines the order direction, either ascending or descending +""" + +scalar String + +""" +A string representation of microseconds UNIX timestamp (16 digits) +""" + +scalar Timestamp + +type TransactionStat { + id: ID! + dayStartBlockNumber: BigInt! + transactionsCount: BigInt! + transactionsCountClean: BigInt! + transactionsValue: BigInt! + transactionsValueClean: BigInt! + totalInCirculation: BigInt! +} + +type UBICollected { + id: ID! + contract: Bytes + block: BigInt! + blockTimestamp: BigInt! + ubi: BigDecimal! + communityPool: BigDecimal! +} + +type UBIHistory { + id: ID! + block: BigInt! + blockTimestamp: BigInt! + totalDailyUBI: BigDecimal! + totalDailyCommunityPool: BigDecimal! +} + +type WalletStat { + id: ID! + dateAppeared: BigInt! + balance: BigInt! + inTransactionsCount: BigInt! + inTransactionsCountClean: BigInt! + inTransactionsValue: BigInt! + inTransactionsValueClean: BigInt! + outTransactionsCount: BigInt! + outTransactionsCountClean: BigInt! + outTransactionsValue: BigInt! + outTransactionsValueClean: BigInt! + totalTransactionsCount: BigInt! + totalTransactionsCountClean: BigInt! + totalTransactionsValue: BigInt! + totalTransactionsValueClean: BigInt! + lastClaimed: BigInt! + totalClaimedCount: BigInt! + totalClaimedValue: BigInt! + claimStreak: BigInt! + longestClaimStreak: BigInt! + isWhitelisted: Boolean! + isActiveUser: Boolean! + dateJoined: BigInt! + lastTransactionFrom: BigInt! + lastTransactionTo: BigInt! +} diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md new file mode 100644 index 00000000..9f55b734 --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md @@ -0,0 +1,41 @@ +# Reserve Celo — Subgraph Usage Guide + +Companion to `reserve-celo.graphql`. This guide is for reserve pricing and broker swap indexing on Celo. + +## Endpoint + +- Goldsky: `https://api.goldsky.com/api/public/project_cmizuamdtfouu01x4csuk5dk1/subgraphs/reserve_celo/1.0/gn` + +--- + +## Entity Overview + +### Core entity + +**ReservePrice** — one indexed reserve pricing point produced from broker swap flow plus exchange-provider price read. +Key fields: `exchangeId`, `exchangeProvider`, `price`, `timestamp`, `day`, `tokenIn`, `tokenOut`, `amountIn`, `amountOut`, `user`, `blockNumber`, `transactionHash`. + +--- + +## Typical Questions This Subgraph Answers + +- What are the most recent reserve prices? +- What was the reserve price on a specific day window? +- Which token pair and user triggered a pricing point? +- Which tx hash/block produced a given price point? + +--- + +## Query Discipline + +- Lowercase all address values in filters. +- Use string values for large integer variables. +- Use `_meta` before analytics queries when stale indexing is suspected. + +--- + +## Practical Start + +1. Check `_meta` block height and `hasIndexingErrors`. +2. Pull latest `ReservePrice` records sorted by `timestamp desc`. +3. Add `day`-based narrowing for historical windows. diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql new file mode 100644 index 00000000..56cac37b --- /dev/null +++ b/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql @@ -0,0 +1,90 @@ +scalar BigDecimal + +scalar BigInt + +scalar Boolean + +scalar Bytes + +scalar Float + +scalar ID + +""" +4 bytes signed integer +""" + +scalar Int + +""" +8 bytes signed integer +""" + +scalar Int8 + +""" +Defines the order direction, either ascending or descending +""" + +type ReservePrice { + """ + Exchange ID combined with timestamp + """ + id: ID! + """ + The exchange ID from the Swap event + """ + exchangeId: Bytes! + """ + The exchange provider address + """ + exchangeProvider: Bytes! + """ + The current price from IBancorExchangeProvider.currentPrice() + """ + price: BigInt! + """ + Transaction timestamp + """ + timestamp: BigInt! + """ + Day ID (timestamp / 86400) + """ + day: BigInt! + """ + Token in address from the swap + """ + tokenIn: Bytes! + """ + Token out address from the swap + """ + tokenOut: Bytes! + """ + Amount in from the swap + """ + amountIn: BigInt! + """ + Amount out from the swap + """ + amountOut: BigInt! + """ + User who initiated the swap + """ + user: Bytes! + """ + Block number + """ + blockNumber: BigInt! + """ + Transaction hash + """ + transactionHash: Bytes! +} + +scalar String + +""" +A string representation of microseconds UNIX timestamp (16 digits) +""" + +scalar Timestamp diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..9b92c725 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "gooddollar": { + "source": "GoodDollar/GoodSkills", + "sourceType": "github", + "skillPath": "skills/gooddollar/SKILL.md", + "computedHash": "d5f8cc5079f386e7170925e61214fb3334176aa6d0fc6bdc50400e535692b377" + } + } +} From 463cbd615c28d1581cc561cb0b8a0c7a4984166f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:19:49 +0000 Subject: [PATCH 3/7] fix: make mp bridge history sync reliable --- .../bridge/mpbridge/TransactionHistory.tsx | 52 +- .../feature/useMPBBridgeViewController.ts | 11 +- .../src/apps/bridge/mpbridge/hooks.ts | 13 +- .../hooks/useMPBBridgeHistory.helpers.test.ts | 128 +++++ .../hooks/useMPBBridgeHistory.helpers.ts | 105 ++++ .../sdk/mpbridge/hooks/useMPBBridgeHistory.ts | 535 ++++++++++-------- 6 files changed, 614 insertions(+), 230 deletions(-) create mode 100644 packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts create mode 100644 packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts diff --git a/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx b/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx index 441a3f39..f78a45b8 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx +++ b/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx @@ -1,23 +1,67 @@ import React from "react"; -import { Box, Spinner, Text, VStack } from "native-base"; +import { Box, Button, HStack, Spinner, Text, VStack } from "native-base"; import { BridgeTransactionList } from "./MPBBridgeTransactionCard"; +import { capitalizeChain, getChainName } from "./utils"; interface TransactionHistoryProps { realTransactionHistory: any[]; historyLoading: boolean; + historyRefreshing: boolean; + historyErrorsByChain: Record; + onRefresh: () => void; onTxDetailsPress: (tx: any) => void; } export const TransactionHistory: React.FC = ({ realTransactionHistory, historyLoading, + historyRefreshing, + historyErrorsByChain, + onRefresh, onTxDetailsPress }) => { + const errorEntries = Object.entries(historyErrorsByChain || {}); + return ( - - Recent Transactions - + + + Recent Transactions + + + + {historyRefreshing && !historyLoading ? ( + + + + Refreshing transaction history... + + + ) : null} + {errorEntries.length > 0 ? ( + + + + Some networks could not refresh right now. + + {errorEntries.map(([chainId, message]) => ( + + {capitalizeChain(getChainName(Number(chainId)))}: {message} + + ))} + + + ) : null} {historyLoading ? ( diff --git a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts index ca01b079..ae62f5c1 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts @@ -113,6 +113,9 @@ export interface MPBBridgeViewModel { transactionHistoryProps: { realTransactionHistory: any[]; historyLoading: boolean; + historyRefreshing: boolean; + historyErrorsByChain: Record; + onRefresh: () => void; onTxDetailsPress: (tx: any) => void; }; } @@ -162,7 +165,8 @@ export const useMPBBridgeViewController = ({ closeAllDropdowns } = useMPBBridgeUiState(); - const { realTransactionHistory, historyLoading } = useDebouncedTransactionHistory(TRANSACTION_HISTORY_DEBOUNCE_MS); + const { realTransactionHistory, historyLoading, historyRefreshing, historyErrorsByChain, refreshHistory } = + useDebouncedTransactionHistory(TRANSACTION_HISTORY_DEBOUNCE_MS); const { getBalanceForChain } = useChainBalances(); const gdValue = getBalanceForChain(sourceChain); @@ -380,6 +384,7 @@ export const useMPBBridgeViewController = ({ successHandled.current = true; setBridgingStatus(effectiveFlow.statusLabel || "Bridge completed successfully!"); setBridging(false); + refreshHistory?.(); if (!successModalOpen && !successModalDismissedRef.current) { setSuccessModalOpen(true); @@ -435,6 +440,7 @@ export const useMPBBridgeViewController = ({ successModalOpen, onBridgeSuccess, onBridgeFailed, + refreshHistory, setBridging, setBridgingStatus, setSuccessModalOpen, @@ -598,6 +604,9 @@ export const useMPBBridgeViewController = ({ transactionHistoryProps: { realTransactionHistory: recentTransactions, historyLoading, + historyRefreshing, + historyErrorsByChain, + onRefresh: refreshHistory, onTxDetailsPress } }; diff --git a/packages/good-design/src/apps/bridge/mpbridge/hooks.ts b/packages/good-design/src/apps/bridge/mpbridge/hooks.ts index e110f186..eee5e6cb 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/hooks.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/hooks.ts @@ -176,7 +176,13 @@ export const useChainBalances = () => { }; export const useDebouncedTransactionHistory = (delay = 1000) => { - const { historySorted: realTransactionHistory } = useMPBBridgeHistory() ?? {}; + const { + historySorted: realTransactionHistory, + initialLoading, + refreshing, + errorsByChain, + refreshHistory + } = useMPBBridgeHistory() ?? {}; const [debouncedHistory, setDebouncedHistory] = useState(realTransactionHistory); const timeoutRef = useRef(); @@ -190,7 +196,10 @@ export const useDebouncedTransactionHistory = (delay = 1000) => { return { realTransactionHistory: debouncedHistory, - historyLoading: !realTransactionHistory + historyLoading: Boolean(initialLoading), + historyRefreshing: Boolean(refreshing), + historyErrorsByChain: errorsByChain || {}, + refreshHistory: refreshHistory || (() => undefined) }; }; diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts new file mode 100644 index 00000000..5f3e620c --- /dev/null +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts @@ -0,0 +1,128 @@ +/* eslint-env jest */ + +import { + createBlockChunks, + getErrorsByChain, + mergeBridgeHistoryCache, + MPBBridgeHistoryCache +} from "./useMPBBridgeHistory.helpers"; + +describe("useMPBBridgeHistory helpers", () => { + it("splits log ranges into 500-block chunks", () => { + expect(createBlockChunks(100, 1201)).toEqual([ + { fromBlock: 100, toBlock: 599 }, + { fromBlock: 600, toBlock: 1099 }, + { fromBlock: 1100, toBlock: 1201 } + ]); + }); + + it("merges history rows, prunes old cache entries, and keeps chain sync state", () => { + const nowMs = new Date("2026-06-29T00:00:00.000Z").getTime(); + const recentTimestamp = Math.floor(nowMs / 1000) - 60; + const oldTimestamp = Math.floor(nowMs / 1000) - 31 * 24 * 60 * 60; + const currentCache: MPBBridgeHistoryCache = { + BridgeRequest: [ + { + transactionHash: "0xold", + blockHash: "0xblock-old", + blockNumber: 1, + transactionIndex: 0, + removed: false, + sourceChainId: 122, + from: "0xfrom", + to: "0xto", + targetChainId: "42220", + amount: "10", + timestamp: oldTimestamp.toString(), + id: "1" + }, + { + transactionHash: "0xkeep", + blockHash: "0xblock-keep", + blockNumber: 10, + transactionIndex: 0, + removed: false, + sourceChainId: 122, + from: "0xfrom", + to: "0xto", + targetChainId: "42220", + amount: "20", + timestamp: recentTimestamp.toString(), + id: "2" + } + ], + ExecutedTransfer: [], + chains: { + 122: { + lastSyncedBlock: 20, + error: { + message: "old error", + updatedAt: 1 + } + } + } + }; + + const nextCache = mergeBridgeHistoryCache( + currentCache, + { + BridgeRequest: [ + { + transactionHash: "0xkeep-updated", + blockHash: "0xblock-keep-updated", + blockNumber: 12, + transactionIndex: 1, + removed: false, + sourceChainId: 122, + from: "0xfrom", + to: "0xto", + targetChainId: "42220", + amount: "30", + timestamp: recentTimestamp.toString(), + id: "2" + } + ], + ExecutedTransfer: [ + { + transactionHash: "0xcompleted", + blockHash: "0xblock-completed", + blockNumber: 30, + transactionIndex: 0, + removed: false, + sourceChainId: 42220, + from: "0xfrom", + to: "0xto", + targetChainId: "122", + amount: "30", + timestamp: recentTimestamp.toString(), + id: "2" + } + ] + }, + { + 122: { + lastSyncedBlock: 40, + lastSuccessfulSyncAt: nowMs + }, + 42220: { + error: { + message: "rpc failed", + updatedAt: nowMs + } + } + }, + nowMs + ); + + expect(nextCache.BridgeRequest).toHaveLength(1); + expect(nextCache.BridgeRequest?.[0].transactionHash).toBe("0xkeep-updated"); + expect(nextCache.ExecutedTransfer).toHaveLength(1); + expect(nextCache.chains?.[122]).toEqual({ + lastSyncedBlock: 40, + lastSuccessfulSyncAt: nowMs + }); + expect(getErrorsByChain(nextCache)).toEqual({ + 42220: "rpc failed" + }); + }); +}); diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts new file mode 100644 index 00000000..8d590103 --- /dev/null +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts @@ -0,0 +1,105 @@ +export type BridgeEventName = "BridgeRequest" | "ExecutedTransfer"; + +export type CachedBridgeEvent = { + transactionHash: string; + blockHash: string; + blockNumber: number; + transactionIndex: number; + removed: boolean; + sourceChainId: number; + from?: string; + to?: string; + targetChainId: string; + amount: string; + timestamp: string; + bridge?: string; + id?: string; +}; + +export type ChainSyncErrorState = { + message: string; + updatedAt: number; +}; + +export type ChainSyncState = { + lastSyncedBlock?: number; + lastSuccessfulSyncAt?: number; + error?: ChainSyncErrorState; +}; + +export type MPBBridgeHistoryCache = { + BridgeRequest?: CachedBridgeEvent[]; + ExecutedTransfer?: CachedBridgeEvent[]; + chains?: Partial>; +}; + +export const HISTORY_WINDOW_DAYS = 30; +export const HISTORY_WINDOW_SECONDS = HISTORY_WINDOW_DAYS * 24 * 60 * 60; +export const HISTORY_BLOCK_CHUNK_SIZE = 500; + +const getEventCacheKey = (event: CachedBridgeEvent) => + event.id ? `${event.sourceChainId}:${event.id}` : `${event.sourceChainId}:${event.transactionHash}`; + +const sortBridgeEvents = (events: CachedBridgeEvent[]) => + events.sort((a, b) => + a.blockNumber === b.blockNumber ? a.transactionIndex - b.transactionIndex : a.blockNumber - b.blockNumber + ); + +export const createBlockChunks = (fromBlock: number, toBlock: number, chunkSize = HISTORY_BLOCK_CHUNK_SIZE) => { + if (fromBlock > toBlock) { + return []; + } + + const chunks: Array<{ fromBlock: number; toBlock: number }> = []; + + for (let cursor = fromBlock; cursor <= toBlock; cursor += chunkSize) { + chunks.push({ + fromBlock: cursor, + toBlock: Math.min(cursor + chunkSize - 1, toBlock) + }); + } + + return chunks; +}; + +export const pruneExpiredEvents = (events: CachedBridgeEvent[], minTimestamp: number) => + events.filter(event => Number(event.timestamp || 0) >= minTimestamp); + +export const getHistoryWindowStartTimestamp = (nowMs = Date.now()) => Math.floor(nowMs / 1000) - HISTORY_WINDOW_SECONDS; + +export const mergeBridgeHistoryCache = ( + current: MPBBridgeHistoryCache, + nextEvents: Partial>, + nextChains: Partial>, + nowMs = Date.now() +): MPBBridgeHistoryCache => { + const minTimestamp = getHistoryWindowStartTimestamp(nowMs); + const mergedRequests = new Map(); + const mergedTransfers = new Map(); + + pruneExpiredEvents((current.BridgeRequest || []).concat(nextEvents.BridgeRequest || []), minTimestamp).forEach( + event => mergedRequests.set(getEventCacheKey(event), event) + ); + + pruneExpiredEvents((current.ExecutedTransfer || []).concat(nextEvents.ExecutedTransfer || []), minTimestamp).forEach( + event => mergedTransfers.set(getEventCacheKey(event), event) + ); + + return { + BridgeRequest: sortBridgeEvents(Array.from(mergedRequests.values())), + ExecutedTransfer: sortBridgeEvents(Array.from(mergedTransfers.values())), + chains: { + ...(current.chains || {}), + ...nextChains + } + }; +}; + +export const getErrorsByChain = (cache: MPBBridgeHistoryCache) => + Object.entries(cache.chains || {}).reduce((result, [chainId, state]) => { + if (state?.error?.message) { + result[Number(chainId)] = state.error.message; + } + + return result; + }, {} as Record); diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts index 47d826c3..8f35dc23 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts @@ -1,38 +1,25 @@ -import { useEffect, useMemo, useState } from "react"; -import { useEthers, useLogs, ChainId } from "@usedapp/core"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEthers } from "@usedapp/core"; import { ethers } from "ethers"; import { first, groupBy, sortBy } from "lodash"; -import { useRefreshOrNever } from "../../../hooks"; import { AsyncStorage } from "../../storage"; import { SupportedChains, formatAmount } from "../../constants"; import { useGetContract } from "../../base/react"; - -type BridgeEventName = "BridgeRequest" | "ExecutedTransfer"; - -type CachedBridgeEvent = { - transactionHash: string; - blockHash: string; - blockNumber: number; - transactionIndex: number; - removed: boolean; - sourceChainId: number; - from?: string; - to?: string; - targetChainId: string; - amount: string; - timestamp: string; - bridge?: string; - id?: string; -}; - -type MPBBridgeHistoryCache = Partial>; - -const HISTORY_CACHE_VERSION = 1; - -// Keep the live RPC scan intentionally small. XDC public RPCs reject wider -// eth_getLogs windows, and the cache below is what gives us persistence. -const HISTORY_BLOCK_WINDOW = 500; +import { + BridgeEventName, + CachedBridgeEvent, + ChainSyncState, + MPBBridgeHistoryCache, + HISTORY_BLOCK_CHUNK_SIZE, + HISTORY_WINDOW_SECONDS, + createBlockChunks, + getErrorsByChain, + mergeBridgeHistoryCache +} from "./useMPBBridgeHistory.helpers"; + +const HISTORY_CACHE_VERSION = 2; const CHAIN_IDS = [SupportedChains.FUSE, SupportedChains.CELO, SupportedChains.MAINNET, SupportedChains.XDC]; +const MAX_PARALLEL_CHUNKS = 3; const hydrateCachedEvent = (event: CachedBridgeEvent) => { const targetChainId = ethers.BigNumber.from(event.targetChainId); @@ -66,41 +53,192 @@ const hydrateCachedEvent = (event: CachedBridgeEvent) => { }; }; -const normalizeLiveEvents = (items: Array<{ sourceChainId: SupportedChains; events: any[] }>) => - items.flatMap(({ sourceChainId, events }) => - events.map((event: any) => { - const targetChainId = event.data?.targetChainId || event.data?.[2]; - const amount = event.data?.amount || event.data?.[3]; - const timestamp = event.data?.timestamp || event.data?.[4]; - const bridge = event.data?.bridge || event.data?.[5]; - const id = event.data?.id || event.data?.[6]; +const getErrorMessage = (error: unknown) => { + if (error instanceof Error && error.message) { + return error.message; + } - // useLogs returns decoded ethers values such as BigNumber. Store only - // plain JSON strings/numbers so AsyncStorage round-trips cleanly. - return { - transactionHash: event.transactionHash, - blockHash: event.blockHash, - blockNumber: event.blockNumber, - transactionIndex: event.transactionIndex, - removed: event.removed, - sourceChainId, - from: event.data?.from || event.data?.[0], - to: event.data?.to || event.data?.[1], - targetChainId: targetChainId?.toString?.() || SupportedChains.CELO.toString(), - amount: amount?.toString?.() || "0", - timestamp: timestamp?.toString?.() || "0", - bridge, - id: id ? id.toString() : undefined - }; - }) + if (typeof error === "string") { + return error; + } + + return "Failed to load bridge history from RPC"; +}; + +const runWithConcurrency = async (tasks: Array<() => Promise>, concurrency: number): Promise => { + const results: T[] = []; + + for (let index = 0; index < tasks.length; index += concurrency) { + const nextResults = await Promise.all(tasks.slice(index, index + concurrency).map(task => task())); + results.push(...nextResults); + } + + return results; +}; + +const findHistoryStartBlock = async ( + provider: ethers.providers.Provider, + latestBlockNumber: number, + targetTimestamp: number +) => { + const latestBlock = await provider.getBlock(latestBlockNumber); + + if (!latestBlock || latestBlock.timestamp <= targetTimestamp) { + return latestBlockNumber; + } + + let low = 0; + let high = latestBlockNumber; + + // Binary search keeps the 30-day backfill exact without relying on chain-specific block-time guesses. + while (low < high) { + const mid = Math.floor((low + high) / 2); + const block = await provider.getBlock(mid); + + if (!block) { + throw new Error(`Failed to fetch block ${mid} while backfilling bridge history`); + } + + if (block.timestamp < targetTimestamp) { + low = mid + 1; + } else { + high = mid; + } + } + + return low; +}; + +const normalizeProviderLogs = ( + contract: ethers.Contract, + sourceChainId: SupportedChains, + logs: ethers.providers.Log[] +): CachedBridgeEvent[] => + logs.flatMap(log => { + try { + const parsedLog = contract.interface.parseLog(log); + const targetChainId = parsedLog.args?.targetChainId || parsedLog.args?.[2]; + const amount = parsedLog.args?.amount || parsedLog.args?.[3]; + const timestamp = parsedLog.args?.timestamp || parsedLog.args?.[4]; + const bridge = parsedLog.args?.bridge || parsedLog.args?.[5]; + const id = parsedLog.args?.id || parsedLog.args?.[6]; + + return [ + { + transactionHash: log.transactionHash, + blockHash: log.blockHash, + blockNumber: log.blockNumber, + transactionIndex: log.transactionIndex, + removed: log.removed, + sourceChainId, + from: parsedLog.args?.from || parsedLog.args?.[0], + to: parsedLog.args?.to || parsedLog.args?.[1], + targetChainId: targetChainId?.toString?.() || SupportedChains.CELO.toString(), + amount: amount?.toString?.() || "0", + timestamp: timestamp?.toString?.() || "0", + bridge, + id: id ? id.toString() : undefined + } + ]; + } catch (error) { + console.warn("Failed to parse bridge history log", error); + return []; + } + }); + +const filterEventsForAccount = (events: CachedBridgeEvent[], account?: string) => { + if (!account) { + return events; + } + + const normalizedAccount = account.toLowerCase(); + + return events.filter( + event => event.from?.toLowerCase() === normalizedAccount || event.to?.toLowerCase() === normalizedAccount ); +}; + +const fetchEventLogs = async ( + contract: ethers.Contract, + eventName: BridgeEventName, + fromBlock: number, + toBlock: number +) => { + if (fromBlock > toBlock) { + return [] as ethers.providers.Log[]; + } + + const provider = contract.provider as ethers.providers.Provider; + const topic = contract.interface.getEventTopic(eventName); + const chunks = createBlockChunks(fromBlock, toBlock, HISTORY_BLOCK_CHUNK_SIZE); + + // Public RPCs are sensitive to large eth_getLogs windows, so every request stays within 500 blocks. + const logsByChunk = await runWithConcurrency( + chunks.map( + chunk => () => + provider.getLogs({ + address: contract.address, + topics: [topic], + fromBlock: chunk.fromBlock, + toBlock: chunk.toBlock + }) + ), + MAX_PARALLEL_CHUNKS + ); + + return logsByChunk.flat(); +}; + +const syncChainHistory = async ( + chainId: SupportedChains, + contract: ethers.Contract, + currentCache: MPBBridgeHistoryCache, + account?: string +) => { + const provider = contract.provider as ethers.providers.Provider; + const latestBlock = await provider.getBlockNumber(); + const chainState = currentCache.chains?.[chainId]; + const targetTimestamp = Math.floor(Date.now() / 1000) - HISTORY_WINDOW_SECONDS; + const fromBlock = + chainState?.lastSyncedBlock !== undefined + ? chainState.lastSyncedBlock + 1 + : await findHistoryStartBlock(provider, latestBlock, targetTimestamp); + + if (fromBlock > latestBlock) { + return { + chainId, + bridgeRequests: [] as CachedBridgeEvent[], + executedTransfers: [] as CachedBridgeEvent[], + chainState: { + lastSyncedBlock: latestBlock, + lastSuccessfulSyncAt: Date.now() + } satisfies ChainSyncState + }; + } + + const [bridgeRequests, executedTransfers] = await Promise.all([ + fetchEventLogs(contract, "BridgeRequest", fromBlock, latestBlock), + fetchEventLogs(contract, "ExecutedTransfer", fromBlock, latestBlock) + ]); + + return { + chainId, + bridgeRequests: filterEventsForAccount(normalizeProviderLogs(contract, chainId, bridgeRequests), account), + executedTransfers: filterEventsForAccount(normalizeProviderLogs(contract, chainId, executedTransfers), account), + chainState: { + lastSyncedBlock: latestBlock, + lastSuccessfulSyncAt: Date.now() + } satisfies ChainSyncState + }; +}; export const useMPBBridgeHistory = () => { const { account } = useEthers(); - const refresh = useRefreshOrNever(5); - const refreshFaster = useRefreshOrNever(2); const [cacheLoaded, setCacheLoaded] = useState(false); const [historyCache, setHistoryCache] = useState({}); + const [refreshTick, setRefreshTick] = useState(0); + const [syncing, setSyncing] = useState(false); + const historyCacheRef = useRef({}); const fuseBridgeContract = useGetContract("MpbBridge", true, "base", SupportedChains.FUSE); const celoBridgeContract = useGetContract("MpbBridge", true, "base", SupportedChains.CELO); @@ -117,85 +255,9 @@ export const useMPBBridgeHistory = () => { [celoBridgeContract, fuseBridgeContract, mainnetBridgeContract, xdcBridgeContract] ); - // These are the only live RPC queries. Each useLogs call scans only the - // recent 500-block window; older discovered events come from AsyncStorage. - const fuseBridgeRequests = useLogs( - fuseBridgeContract ? { contract: fuseBridgeContract, event: "BridgeRequest", args: [] } : undefined, - { - chainId: SupportedChains.FUSE as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh - } - ); - - const celoBridgeRequests = useLogs( - celoBridgeContract ? { contract: celoBridgeContract, event: "BridgeRequest", args: [] } : undefined, - { - chainId: SupportedChains.CELO as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh - } - ); - - const mainnetBridgeRequests = useLogs( - mainnetBridgeContract ? { contract: mainnetBridgeContract, event: "BridgeRequest", args: [] } : undefined, - { - chainId: SupportedChains.MAINNET as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh - } - ); - - const xdcBridgeRequests = useLogs( - xdcBridgeContract ? { contract: xdcBridgeContract, event: "BridgeRequest", args: [] } : undefined, - { - chainId: SupportedChains.XDC as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh - } - ); - - const fuseBridgeCompleted = useLogs( - fuseBridgeContract ? { contract: fuseBridgeContract, event: "ExecutedTransfer", args: [] } : undefined, - { - chainId: SupportedChains.FUSE as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh: refreshFaster - } - ); - - const celoBridgeCompleted = useLogs( - celoBridgeContract ? { contract: celoBridgeContract, event: "ExecutedTransfer", args: [] } : undefined, - { - chainId: SupportedChains.CELO as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh: refreshFaster - } - ); - - const mainnetBridgeCompleted = useLogs( - mainnetBridgeContract ? { contract: mainnetBridgeContract, event: "ExecutedTransfer", args: [] } : undefined, - { - chainId: SupportedChains.MAINNET as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh: refreshFaster - } - ); - - const xdcBridgeCompleted = useLogs( - xdcBridgeContract ? { contract: xdcBridgeContract, event: "ExecutedTransfer", args: [] } : undefined, - { - chainId: SupportedChains.XDC as unknown as ChainId, - fromBlock: -HISTORY_BLOCK_WINDOW, - refresh: refreshFaster - } - ); - const cacheKey = useMemo(() => { if (!account) return undefined; - // Include contract addresses in the key so deployments/env changes do not - // reuse stale logs from an older bridge contract. const contractAddresses = CHAIN_IDS.map(chainId => contracts[chainId]?.address?.toLowerCase() || "missing").join( ":" ); @@ -203,11 +265,16 @@ export const useMPBBridgeHistory = () => { return `GD_MPBBridgeHistory_v${HISTORY_CACHE_VERSION}_${account.toLowerCase()}_${contractAddresses}`; }, [account, contracts]); + useEffect(() => { + historyCacheRef.current = historyCache; + }, [historyCache]); + useEffect(() => { let cancelled = false; setCacheLoaded(false); setHistoryCache({}); + historyCacheRef.current = {}; if (!cacheKey) { setCacheLoaded(true); @@ -216,17 +283,18 @@ export const useMPBBridgeHistory = () => { }; } - // Load local history first so the UI can show previously discovered bridge - // events immediately, then merge fresh useLogs results in the effect below. + // Cache hydrate keeps the first paint fast while a background sync fetches new chain deltas. AsyncStorage.getItem(cacheKey) .then(cached => { if (!cancelled) { - setHistoryCache(cached || {}); + const hydratedCache = cached || {}; + setHistoryCache(hydratedCache); + historyCacheRef.current = hydratedCache; setCacheLoaded(true); } }) - .catch(e => { - console.warn("Failed to read MPB bridge history cache", e); + .catch(error => { + console.warn("Failed to read MPB bridge history cache", error); if (!cancelled) setCacheLoaded(true); }); @@ -235,113 +303,129 @@ export const useMPBBridgeHistory = () => { }; }, [cacheKey]); - const freshCache = useMemo(() => { - // Normalize all live logs into a single plain-object structure. The cache - // does not care which chain produced the event; sourceChainId keeps that. - const bridgeRequests = normalizeLiveEvents([ - { sourceChainId: SupportedChains.FUSE, events: fuseBridgeRequests?.value || [] }, - { sourceChainId: SupportedChains.CELO, events: celoBridgeRequests?.value || [] }, - { sourceChainId: SupportedChains.MAINNET, events: mainnetBridgeRequests?.value || [] }, - { sourceChainId: SupportedChains.XDC, events: xdcBridgeRequests?.value || [] } - ]); - - const completedTransfers = normalizeLiveEvents([ - { sourceChainId: SupportedChains.FUSE, events: fuseBridgeCompleted?.value || [] }, - { sourceChainId: SupportedChains.CELO, events: celoBridgeCompleted?.value || [] }, - { sourceChainId: SupportedChains.MAINNET, events: mainnetBridgeCompleted?.value || [] }, - { sourceChainId: SupportedChains.XDC, events: xdcBridgeCompleted?.value || [] } - ]); + useEffect(() => { + void refreshTick; - return { - BridgeRequest: bridgeRequests, - ExecutedTransfer: completedTransfers - }; - }, [ - fuseBridgeRequests, - celoBridgeRequests, - mainnetBridgeRequests, - xdcBridgeRequests, - fuseBridgeCompleted, - celoBridgeCompleted, - mainnetBridgeCompleted, - xdcBridgeCompleted - ]); + if (!cacheLoaded || !cacheKey) { + return; + } - useEffect(() => { - if (!cacheLoaded || !cacheKey) return; - if (!freshCache.BridgeRequest.length && !freshCache.ExecutedTransfer.length) return; - - setHistoryCache(current => { - // Fresh useLogs results are merged into the persisted cache. This turns a - // rolling 500-block scan into local history that survives page/app reloads. - const bridgeRequests = new Map(); - const completedTransfers = new Map(); - - (current.BridgeRequest || []).concat(freshCache.BridgeRequest).forEach(event => { - // Bridge ids are stable across source/target chains. Fall back to tx - // hash for malformed or older cached entries that do not contain an id. - const key = event.id ? `${event.sourceChainId}:${event.id}` : `${event.sourceChainId}:${event.transactionHash}`; - bridgeRequests.set(key, event); - }); + const chainContracts = CHAIN_IDS.flatMap(chainId => + contracts[chainId] ? [{ chainId, contract: contracts[chainId] as ethers.Contract }] : [] + ); - (current.ExecutedTransfer || []).concat(freshCache.ExecutedTransfer).forEach(event => { - const key = event.id ? `${event.sourceChainId}:${event.id}` : `${event.sourceChainId}:${event.transactionHash}`; - completedTransfers.set(key, event); - }); + if (!chainContracts.length) { + return; + } - const next = { - BridgeRequest: Array.from(bridgeRequests.values()).sort((a, b) => - a.blockNumber === b.blockNumber ? a.transactionIndex - b.transactionIndex : a.blockNumber - b.blockNumber - ), - ExecutedTransfer: Array.from(completedTransfers.values()).sort((a, b) => - a.blockNumber === b.blockNumber ? a.transactionIndex - b.transactionIndex : a.blockNumber - b.blockNumber - ) - }; + let cancelled = false; + + setSyncing(true); + + const syncHistory = async () => { + const currentCache = historyCacheRef.current; + const settledChains = await Promise.allSettled( + chainContracts.map(({ chainId, contract }) => syncChainHistory(chainId, contract, currentCache, account)) + ); + + if (cancelled) { + return; + } + + const nextChains: Partial> = {}; + const nextBridgeRequests: CachedBridgeEvent[] = []; + const nextExecutedTransfers: CachedBridgeEvent[] = []; - // Persist only the normalized event shape; ethers BigNumber instances do - // not survive JSON cleanly. - void AsyncStorage.setItem(cacheKey, next).catch(e => console.warn("Failed to store MPB bridge history cache", e)); + settledChains.forEach((result, index) => { + const { chainId } = chainContracts[index]; + + if (result.status === "fulfilled") { + nextChains[chainId] = result.value.chainState; + nextBridgeRequests.push(...result.value.bridgeRequests); + nextExecutedTransfers.push(...result.value.executedTransfers); + return; + } + + nextChains[chainId] = { + ...(currentCache.chains?.[chainId] || {}), + error: { + message: getErrorMessage(result.reason), + updatedAt: Date.now() + } + }; + }); + + const nextCache = mergeBridgeHistoryCache( + currentCache, + { + BridgeRequest: nextBridgeRequests, + ExecutedTransfer: nextExecutedTransfers + }, + nextChains + ); + + setHistoryCache(nextCache); + historyCacheRef.current = nextCache; + void AsyncStorage.setItem(cacheKey, nextCache).catch(error => + console.warn("Failed to store MPB bridge history cache", error) + ); + }; - return next; + void syncHistory().finally(() => { + if (!cancelled) { + setSyncing(false); + } }); - }, [cacheKey, cacheLoaded, freshCache]); + + return () => { + cancelled = true; + }; + }, [account, cacheKey, cacheLoaded, contracts, refreshTick]); + + const refreshHistory = useCallback(() => { + setRefreshTick(current => current + 1); + }, []); return useMemo(() => { + const errorsByChain = getErrorsByChain(historyCache); + const hasCachedRows = Boolean( + (historyCache.BridgeRequest || []).length || (historyCache.ExecutedTransfer || []).length + ); + if (!cacheLoaded) { - return { historySorted: undefined }; + return { + history: undefined, + historySorted: undefined, + initialLoading: true, + refreshing: false, + errorsByChain, + refreshHistory + }; } - // Rebuild the minimal decoded-event shape the existing MP bridge UI - // expects. This keeps the render path compatible with the original useLogs - // result while storing only JSON-safe values in AsyncStorage. const bridgeRequests = (historyCache.BridgeRequest || []).map(hydrateCachedEvent); const completedTransfers = (historyCache.ExecutedTransfer || []).map(hydrateCachedEvent); - const getEventId = (e: any) => { - const id = e.data?.id || e.data?.[6]; + const getEventId = (event: any) => { + const id = event.data?.id || event.data?.[6]; - return id ? id.toString() : e.transactionHash; + return id ? id.toString() : event.transactionHash; }; const completedByChain = groupBy(completedTransfers, event => event.data.sourceChainId.toNumber()); const completedByTargetChain = CHAIN_IDS.reduce((result, sourceChainId) => { - // Completion events are looked up by chain and bridge id so source - // requests can be marked complete when the target-chain event is cached. result[sourceChainId] = groupBy(completedByChain[sourceChainId] || [], getEventId); - return result; }, {} as Record>); - const processBridgeRequestEvent = (e: any) => { - type BridgeEvent = typeof e & { completedEvent: any; amount: string }; - const extended = e as BridgeEvent; - const amountBN = e.data?.amount || ethers.BigNumber.from(0); - const requestId = e.data?.id?.toString(); - const sourceChainId = e.data.sourceChainId.toNumber(); + const processBridgeRequestEvent = (event: any) => { + type BridgeEvent = typeof event & { completedEvent: any; amount: string }; + const extended = event as BridgeEvent; + const amountBN = event.data?.amount || ethers.BigNumber.from(0); + const requestId = event.data?.id?.toString(); + const sourceChainId = event.data.sourceChainId.toNumber(); - // Completion happens on the opposite chain, so match request IDs against - // all other chains. const completedEventsMap = CHAIN_IDS.filter(chainId => chainId !== sourceChainId).reduce((result, chainId) => { return { ...result, ...completedByTargetChain[chainId] }; }, {} as Record); @@ -357,8 +441,6 @@ export const useMPBBridgeHistory = () => { const historyFiltered = account ? historyCombined.filter( - // Keep only the connected wallet's bridge requests. Cached events are - // wallet-scoped by key, but this guards against old/stale cache data. (tx: any) => tx.data?.from?.toLowerCase() === account?.toLowerCase() || tx.data?.to?.toLowerCase() === account?.toLowerCase() @@ -367,6 +449,13 @@ export const useMPBBridgeHistory = () => { const historySorted = sortBy(historyFiltered, (tx: any) => tx.data?.timestamp?.toNumber?.() || 0).reverse(); - return { historySorted }; - }, [account, cacheLoaded, historyCache]); + return { + history: historySorted, + historySorted, + initialLoading: syncing && !hasCachedRows, + refreshing: syncing, + errorsByChain, + refreshHistory + }; + }, [account, cacheLoaded, historyCache, refreshHistory, syncing]); }; From 3a372f75d026163c6e039d9b6e1b6303cd979ac8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:13:21 +0000 Subject: [PATCH 4/7] chore: remove committed skills artifacts --- .agents/skills/gooddollar/CONTRIBUTING.md | 121 -- .agents/skills/gooddollar/SKILL.md | 215 ---- .../contracts/BuyGDCloneFactory.abi.yaml | 205 ---- .../BuyGDCloneFactory.selectors.yaml | 27 - .../contracts/BuyGDCloneV2.abi.yaml | 282 ----- .../contracts/BuyGDCloneV2.selectors.yaml | 36 - .../contracts/CFAv1Forwarder.abi.yaml | 237 ---- .../contracts/CFAv1Forwarder.selectors.yaml | 19 - .../ConstantFlowAgreementV1.abi.yaml | 560 --------- .../ConstantFlowAgreementV1.selectors.yaml | 33 - .../contracts/GoodDollarOFTAdapter.abi.yaml | 100 -- .../GoodDollarOFTAdapter.selectors.yaml | 12 - .../GooddollarSavingsStream.abi.yaml | 210 ---- .../GooddollarSavingsStream.selectors.yaml | 27 - .../contracts/GovernanceStakingV2.abi.yaml | 202 ---- .../GovernanceStakingV2.selectors.yaml | 26 - .../references/contracts/IdentityV3.abi.yaml | 390 ------- .../contracts/IdentityV3.selectors.yaml | 49 - .../references/contracts/IdentityV4.abi.yaml | 424 ------- .../contracts/IdentityV4.selectors.yaml | 51 - .../references/contracts/InvitesV2.abi.yaml | 333 ------ .../contracts/InvitesV2.selectors.yaml | 41 - .../references/contracts/MentoBroker.abi.yaml | 259 ----- .../contracts/MentoBroker.selectors.yaml | 24 - .../contracts/MessagePassingBridge.abi.yaml | 542 --------- .../MessagePassingBridge.selectors.yaml | 52 - .../references/contracts/NameService.abi.yaml | 104 -- .../contracts/NameService.selectors.yaml | 11 - .../references/contracts/SuperToken.abi.yaml | 1033 ----------------- .../contracts/SuperToken.selectors.yaml | 99 -- .../references/contracts/Superfluid.abi.yaml | 757 ------------ .../contracts/Superfluid.selectors.yaml | 51 - .../references/contracts/UBISchemeV2.abi.yaml | 305 ----- .../contracts/UBISchemeV2.selectors.yaml | 40 - .../contracts/_rich-abi-yaml-format.md | 93 -- .../deep-researches/faucet-flows.md | 42 - .../fuse-to-celo-staking-migration.md | 38 - .../gooddao-daostack-surface.md | 37 - .../deep-researches/how-ubi-is-minted.md | 68 -- .../inviter-invitee-reward-model.md | 58 - .../mento-reserve-economics.md | 27 - .../deep-researches/on-off-ramp-service.md | 69 -- .../gooddollar/references/guides/bridge.md | 187 --- .../references/guides/check-identity.md | 87 -- .../gooddollar/references/guides/claim.md | 83 -- .../gooddollar/references/guides/faucet.md | 76 -- .../gooddollar/references/guides/gooddocs.md | 37 - .../gooddollar/references/guides/goodsdks.md | 110 -- .../references/guides/hypersync-hyperrpc.md | 110 -- .../references/guides/invite-bounties.md | 115 -- .../migrate-fuse-staking-to-celo-savings.md | 116 -- .../references/guides/on-off-ramp.md | 77 -- .../gooddollar/references/guides/save.md | 93 -- .../gooddollar/references/guides/stream.md | 135 --- .../gooddollar/references/guides/swap.md | 83 -- .../references/subgraphs/_query-patterns.md | 35 - .../subgraphs/goodcollective-guide.md | 54 - .../subgraphs/goodcollective.graphql | 200 ---- .../subgraphs/gooddollar-celo-guide.md | 104 -- .../subgraphs/gooddollar-celo.graphql | 113 -- .../subgraphs/reserve-celo-guide.md | 41 - .../references/subgraphs/reserve-celo.graphql | 90 -- .gitignore | 4 +- skills-lock.json | 11 - 64 files changed, 3 insertions(+), 9267 deletions(-) delete mode 100644 .agents/skills/gooddollar/CONTRIBUTING.md delete mode 100644 .agents/skills/gooddollar/SKILL.md delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/NameService.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/NameService.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml delete mode 100644 .agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/faucet-flows.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md delete mode 100644 .agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md delete mode 100644 .agents/skills/gooddollar/references/guides/bridge.md delete mode 100644 .agents/skills/gooddollar/references/guides/check-identity.md delete mode 100644 .agents/skills/gooddollar/references/guides/claim.md delete mode 100644 .agents/skills/gooddollar/references/guides/faucet.md delete mode 100644 .agents/skills/gooddollar/references/guides/gooddocs.md delete mode 100644 .agents/skills/gooddollar/references/guides/goodsdks.md delete mode 100644 .agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md delete mode 100644 .agents/skills/gooddollar/references/guides/invite-bounties.md delete mode 100644 .agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md delete mode 100644 .agents/skills/gooddollar/references/guides/on-off-ramp.md delete mode 100644 .agents/skills/gooddollar/references/guides/save.md delete mode 100644 .agents/skills/gooddollar/references/guides/stream.md delete mode 100644 .agents/skills/gooddollar/references/guides/swap.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/_query-patterns.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/goodcollective.graphql delete mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql delete mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md delete mode 100644 .agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql delete mode 100644 skills-lock.json diff --git a/.agents/skills/gooddollar/CONTRIBUTING.md b/.agents/skills/gooddollar/CONTRIBUTING.md deleted file mode 100644 index ab855d38..00000000 --- a/.agents/skills/gooddollar/CONTRIBUTING.md +++ /dev/null @@ -1,121 +0,0 @@ -# Contributing to GoodSkills - -This repository is an AI skill pack. The goal of each update is to make agent behavior more reliable, more explicit, and easier to audit. - -## Update workflow - -1. Define the user-facing problem first. -2. For contract-related updates, add or update Rich ABI first (`references/contracts/*.abi.yaml`), then refresh selectors. -3. Decide the remaining artifact types: - - `references/guides/` for "what to do" - - `references/deep-researches/` for "why it works this way" - - `scripts/` for deterministic and repeatable execution -4. Update `SKILL.md` routing so the new artifact is discoverable. -5. Validate consistency (paths, naming, links, selectors, assumptions). - -If a change touches contract behavior, treat Rich ABI update/add as mandatory first step before guides, deep-research, or scripts. - -## Add or update a guide - -Use guides for execution playbooks and operator workflows. - -Required structure: - -- title and one-line usage trigger -- `## Goal` -- `## Required inputs` -- `## Execution flow` as numbered steps -- deterministic snippet when execution is non-trivial -- failure handling and output contract - -Guide rules: - -- prefer explicit pre-checks before state-changing actions -- include only one primary workflow per file -- use [GoodProtocol `releases/deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) as the **only** source for contract addresses (rich ABI `meta.deployments` mirrors those rows); use GoodDocs for behavior and UX, not for resolving addresses; use on-chain `NameService.getAddress` only when the deployment documents the string key -- avoid implementation-deep theory; keep that in deep-research files - -After adding a guide: - -- add it to `SKILL.md` in `Guides` -- add an entry in `Use-case to guide map` - -## Add or update a deep-research note - -Use deep-research files for architecture, rationale, tradeoffs, and root-cause logic. - -Deep-research rules: - -- explain causality, not only API surfaces -- distinguish current behavior from legacy behavior -- link source contracts/docs for traceability -- keep language natural and decision-oriented - -Do not turn deep-research files into step-by-step runbooks; move operational steps into guides. - -## Add or update Rich ABI YAML - -Location: `references/contracts/`. - -For each contract: - -- create or update `Foo.abi.yaml` -- generate or refresh `Foo.selectors.yaml` -- include function-level notes for non-obvious behavior -- when `meta.deployments` lists concrete addresses, add **`creationBlock`** next to each **`address`** (placement: `references/contracts/_rich-abi-yaml-format.md`; using it as **`fromBlock`** for log or HyperSync fetches: `references/guides/hypersync-hyperrpc.md`) - -Minimum ABI documentation quality: - -- correct mutability, inputs, outputs -- access pattern (`owner`, `avatar`, `anyone`, etc.) where relevant -- emitted events and practical errors -- notes for routing/edge-case semantics - -Source-of-truth policy: - -- prefer canonical contract repos (GoodProtocol, GoodBridge, mento-core) -- avoid inferred behavior when source is unclear -- update notes when protocol behavior changed - -Selector generation: - -```bash -node scripts/selectors.mjs generate Foo.abi.yaml -``` - -## Add or update scripts - -Location: `scripts/`. - -Use scripts when: - -- a workflow is repeated -- deterministic output is needed -- manual querying is error-prone - -Script standards: - -- require inputs through env vars or explicit args -- fail loudly with actionable messages -- print structured output for easy reuse -- keep script intent narrow - -When a script supports a guide: - -- reference it from that guide -- document expected inputs and outputs in the guide - -## Naming and organization - -- use lowercase kebab-case for guides and deep-research files -- keep one topic per file -- avoid duplicate guidance across files -- prefer updating existing files over creating near-duplicates - -## Update checklist before merge - -- `SKILL.md` routing updated -- links resolve and point to public sources -- guides and deep-research files respect "what" vs "why" separation -- ABI + selectors pairs are in sync -- new behavior is reflected in notes where needed diff --git a/.agents/skills/gooddollar/SKILL.md b/.agents/skills/gooddollar/SKILL.md deleted file mode 100644 index 2d59116b..00000000 --- a/.agents/skills/gooddollar/SKILL.md +++ /dev/null @@ -1,215 +0,0 @@ ---- -name: gooddollar -description: > - Knowledge base for GoodProtocol action execution and GoodDollar (G$) integrations. - Use this skill BEFORE ad-hoc web search for claim, save/stake, swap, bridge, - stream, and identity tasks. Prefer GoodDocs (https://docs.gooddollar.org/) for - narrative; contract addresses only from GoodProtocol releases/deployment.json. -metadata: - version: 1.0.0 -license: MIT ---- - -# GoodDollar Skill Pack - -Routing index for GoodProtocol. This repo complements [GoodDocs](https://docs.gooddollar.org/) for behavior and user flows. **Contract addresses** come only from [GoodProtocol/releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (and `meta.deployments` in `references/contracts/*.abi.yaml`, which mirror those rows)—not from GoodDocs pages. - -Repository maintenance and update process is documented in `CONTRIBUTING.md`. - -## Protocol snapshot (from GoodDocs) - -- G$ is reserve-backed; issuance and pricing tie to the reserve and bonding-curve mechanics described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). -- The stack is multi-chain; which contracts exist per environment is defined only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `GoodDollar`, `Identity`, `NameService`, `UBIScheme`, Mento keys, `MpbBridge`, and related entries under `production`, `production-celo`, and `production-xdc`). -- UBI is daily for verified users; identity verification and connected accounts are documented under [user guides](https://docs.gooddollar.org/user-guides). - -## Guides (single location for action playbooks) - -All task-specific instructions live under `references/guides/`. - -- `references/guides/claim.md` — daily UBI (`claim` / UBIScheme). -- `references/guides/save.md` — stake, rewards, unstake. -- `references/guides/swap.md` — buy or sell G$ (Mento on supported chains). -- `references/guides/bridge.md` — MessagePassingBridge (GoodDocs); optional OFT path via ABI refs. -- `references/guides/stream.md` — Superfluid streams (Celo-oriented in GoodDocs). -- `references/guides/check-identity.md` — whitelist and connected-address semantics. -- `references/guides/goodsdks.md` — SDK-first integration routing for GoodSDKs packages. -- `references/guides/gooddocs.md` — hub links to [GoodDocs](https://docs.gooddollar.org/). -- `references/guides/hypersync-hyperrpc.md` — Envio HyperSync/HyperRPC data-source routing for high-volume historical reads. -- `references/guides/faucet.md` — Faucet gas top-up execution flow and preflight checks. -- `references/guides/on-off-ramp.md` — stable-token ramp service flow into and out of G$. -- `references/guides/invite-bounties.md` — verify and execute inviter-invitee bounty payouts. -- `references/guides/migrate-fuse-staking-to-celo-savings.md` — migrate Fuse governance stake to CELO savings flow. - -## Subgraphs (indexed chain history) - -Use this folder with the same pattern as the protocol subgraph references: one `*-guide.md` plus one companion `.graphql` per deployment. - -For historical on-chain data, **start with the subgraph**: confirm the deployment covers the question (entities and fields in the guide, freshness via `_meta`). If the subgraph does not work for the request—missing schema coverage, stale or lagging indexing, query limits, or endpoint errors—**then** move to **HyperSync** or **HyperRPC** using `references/guides/hypersync-hyperrpc.md`. - -- `references/subgraphs/_query-patterns.md` — cross-cutting query discipline. -- `references/subgraphs/reserve-celo-guide.md` + `references/subgraphs/reserve-celo.graphql` — reserve pricing and swap history. -- `references/subgraphs/gooddollar-celo-guide.md` + `references/subgraphs/gooddollar-celo.graphql` — GoodDollar Celo schema discovery and starter probes. -- `references/subgraphs/goodcollective-guide.md` + `references/subgraphs/goodcollective.graphql` — GoodCollective schema discovery and starter probes. - -For Superfluid protocol subgraphs (streams, pools, vesting schedulers), see [Superfluid documentation](https://docs.superfluid.finance/) and [subgraph endpoints](https://subgraph-endpoints.superfluid.dev/). - -## Historical data routing policy (strict) - -1. Query subgraphs first for all historical/indexed requests. -2. Validate required entities and fields against the target subgraph schema and guide before declaring a gap. -3. Use **HyperSync** or **HyperRPC** fallback only when at least one of these is true: - - required entities or fields are not available in subgraph schema - - indexing lag makes subgraph data stale for the requested range - - query limits or endpoint instability block reliable retrieval -4. Do not start with HyperSync or HyperRPC when subgraph data is available and fresh. -5. HyperRPC fallback requires a valid Envio API key; if missing, **explicitly ask the user** to provide `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or paste a full `HYPERRPC_URL`); do not treat anonymous HyperRPC as production. -6. When **HyperSync** is the best option for the query and no Envio API token is available (`ENVIO_API_TOKEN` or equivalent per `references/guides/hypersync-hyperrpc.md`), **explicitly ask the user** to provide the token before proceeding; do not silently substitute anonymous HyperSync usage. -7. When fallback is used, report reason explicitly (schema gap, lag, or reliability issue). - -## Data source decision table - -| Query type | Primary source | Secondary source | Notes | -|---|---|---|---| -| Current on-chain state (latest balances, allowances, config, flags, view calls) | RPC | None | Use direct contract RPC reads for latest state. | -| Historical indexed entity data (time-series, aggregates, protocol entities, event-derived analytics) | Subgraph | HyperSync/HyperRPC | Prefer subgraph first; fall back when it cannot answer. | -| Historical raw on-chain data when subgraph is missing fields/entities or stale | HyperSync | HyperRPC | Prefer HyperSync for bulk scans and data pipelines. | -| Historical data for existing JSON-RPC integrations | HyperRPC | HyperSync | Use HyperRPC when strict JSON-RPC compatibility is required. | - -Decision rule: - -1. If request is current state -> use RPC. -2. If request is historical/indexed -> query subgraph first. -3. If subgraph cannot satisfy request -> fallback to HyperSync or HyperRPC per compatibility and scale needs. -4. HyperRPC fallback requires Envio API key credentials. -5. HyperSync client usage requires an Envio API token; if HyperSync is chosen and the token is missing, explicitly ask the user to provide it (see `references/guides/hypersync-hyperrpc.md`). - -## Mapping data retrieval rule - -Solidity mappings are not iterable on-chain by keyspace scan. Do not assume full-key enumeration is possible from RPC alone. - -When data is stored in mapping-like structures: - -1. Check contract source and ABI for key-discovery paths first: - - events emitted on set or update - - arrays, counters, linked lists, or index getters storing keys - - dedicated pagination or enumerable view functions -2. If key discovery exists, reconstruct key set from those sources and then read mapping entries. -3. If key discovery does not exist, report that complete iteration is not possible from chain state alone. -4. For historical reconstruction, prefer subgraph indexing first; if unavailable, use HyperSync or HyperRPC log scans with explicit limitations. - -## Use-case to guide map - -- Claim requests -> `references/guides/claim.md` -- Eligibility or connected-address questions -> `references/guides/check-identity.md` -- Stake, save, unstake -> `references/guides/save.md` -- Buy or sell G$ against reserve rails -> `references/guides/swap.md` -- Cross-chain bridge -> `references/guides/bridge.md` -- Stream management -> `references/guides/stream.md` -- SDK app integration tasks -> `references/guides/goodsdks.md` -- Bulk historical reads or data-engineering fetches -> `references/guides/hypersync-hyperrpc.md` -- Faucet top-up tasks -> `references/guides/faucet.md` -- On-/off-ramp service flow tasks -> `references/guides/on-off-ramp.md` -- Invite bounty eligibility and payout tasks -> `references/guides/invite-bounties.md` -- Fuse to CELO staking migration tasks -> `references/guides/migrate-fuse-staking-to-celo-savings.md` -- Indexed history, analytics, or GraphQL against GoodDollar subgraphs -> `references/subgraphs/_query-patterns.md` -- Historical on-chain fetch when subgraph data is insufficient -> subgraphs first, then HyperSync or HyperRPC per `references/guides/hypersync-hyperrpc.md`; if HyperSync is best and `ENVIO_API_TOKEN` is missing, ask the user for it explicitly. - -## Ambiguous prompts and incomplete inputs - -Stop and **ask the user** whenever the task is underspecified or required facts are missing. List what you need in short, concrete questions (for example chain, contract, address, amount, account, RPC or signer access, time or block range, prior tx hash, approval scope). - -- **Ambiguous** means the goal, environment, contract surface, or acceptance criteria are not clear enough to choose a safe path. -- **Incomplete** means you lack inputs that would change what you build, call, or sign next. - -**Do not invent** chain, address, amount, or policy details that affect correctness, funds, or eligibility. For **information-only** work you may state a single explicit assumption, label it, and ask the user to confirm or correct it before going further. - -**Execution work** (writing or editing runnable code, sending transactions, migrations, or anything that can move funds or alter on-chain state) has **no guessing**: settle every required input with the user, then implement or run. - -## Execution rules - -1. Collect missing required inputs before sending transactions. -2. Run pre-checks first (allowance, whitelist, quotes, bridge **amount** limits, peer wiring when using OFT paths). -3. If a pre-check fails, stop and return the exact corrective action. -4. Return tx hash and key output values. -5. Never fabricate addresses, amounts, or ABI behavior. -6. Resolve decimals and units per chain as in [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (for example 18 decimals on Celo, 2 on Fuse and Ethereum where applicable). - -## Pre-check matrix - -- Claim: verify identity whitelist status before `claim()`. -- Save or stake: verify balance and allowance before `stake()`. -- Swap: fetch quote, apply slippage bounds, verify allowance; confirm Mento contract keys for the active chain exist in `deployment.json` (for example `MentoBroker` under `production-celo` or `production-xdc`). -- Bridge (MessagePassingBridge): on the **source** chain approve G$ to the bridge; optionally preflight `canBridge(from, amount)` on that same contract (outbound `_bridgeTo` does not call it internally). For LZ use `estimateSendFee` with the **normalized** burn amount per `references/guides/bridge.md`, then `bridgeToWithLz` with nonzero `msg.value` for the **cross-chain transport** fee only (distinct from destination **`bridgeFees`** on minted G$; see **Bridge fee context** in that guide). Read **`bridgeLimits`** / daily trackers when debugging **amount** caps; see **Bridge amount limit context** in that guide. Respect `isClosed`, `LZ_FEE`, `MISSING_FEE`, and `UNSUPPORTED_CHAIN`. **Destination** mint applies `_enforceLimits` and can still revert. Use **Axelar** only when `toAxelarChainId` returns a route (implementation maps 1, 5, 42220, 44787); for Fuse or XDC style targets prefer LZ unless mapping is extended on-chain. -- Bridge (OFT adapter path): verify peer wiring and `quoteSend` fee data. -- Stream: confirm Celo (or documented Superfluid network) and correct Super Token and forwarder or host addresses. -- Identity: resolve Identity from NameService; remember connected addresses do not multiply daily claims ([connect wallet guide](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity)). - -## Output format requirements - -For any state-changing action return: - -- network and key contract addresses used -- normalized input amounts and min or max guards -- tx hash -- key post-state output when available -- follow-up action if user intervention is required - -## Rich contract ABI references - -Convention: each `Foo.abi.yaml` has a companion `Foo.selectors.yaml` (function, event, and custom error selectors). Schema: `references/contracts/_rich-abi-yaml-format.md`. - -GoodDollar / Mento: - -- `references/contracts/NameService.abi.yaml` -- `references/contracts/IdentityV3.abi.yaml` -- `references/contracts/IdentityV4.abi.yaml` -- `references/contracts/InvitesV2.abi.yaml` -- `references/contracts/BuyGDCloneFactory.abi.yaml` -- `references/contracts/BuyGDCloneV2.abi.yaml` -- `references/contracts/GovernanceStakingV2.abi.yaml` -- `references/contracts/GooddollarSavingsStream.abi.yaml` (Ubeswap Superfluid stream savings; Celo deployment) -- `references/contracts/UBISchemeV2.abi.yaml` -- `references/contracts/MentoBroker.abi.yaml` -- `references/contracts/MessagePassingBridge.abi.yaml` -- `references/contracts/GoodDollarOFTAdapter.abi.yaml` -- `references/contracts/CFAv1Forwarder.abi.yaml` -- `references/contracts/ConstantFlowAgreementV1.abi.yaml` -- `references/contracts/Superfluid.abi.yaml` -- `references/contracts/SuperToken.abi.yaml` - -Superfluid (CFA, CFAv1Forwarder, Host, full ABI library): use [Superfluid docs](https://docs.superfluid.finance/), npm packages such as `@superfluid-finance/ethereum-contracts` and `@sfpro/sdk`, and contract ABIs published with those packages. - -## Deep researches - -- `references/deep-researches/on-off-ramp-service.md` -- `references/deep-researches/how-ubi-is-minted.md` -- `references/deep-researches/inviter-invitee-reward-model.md` -- `references/deep-researches/mento-reserve-economics.md` -- `references/deep-researches/gooddao-daostack-surface.md` -- `references/deep-researches/faucet-flows.md` -- `references/deep-researches/fuse-to-celo-staking-migration.md` - -## Revert debugging quick map - -- Identity or eligibility errors -> Identity and UBIScheme ABIs; live addresses from `deployment.json` only; GoodDocs for whitelist and claim behavior. -- Approval or transfer failures -> token approvals and balances; see integration guide for `transferAndCall` vs `approve` plus `transferFrom`. -- Swap bound failures -> quote freshness and slippage settings. -- MessagePassingBridge failures -> `canBridge`; **`BRIDGE_LIMITS`** (amount caps, whitelist, **`closed`**, and related policy strings); transport `msg.value` (`MISSING_FEE`, `LZ_FEE`) vs destination protocol fee (`bridgeFees`, `feeRecipient`); correct `bridgeTo` arguments; [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). -- OFT path failures -> peer wiring and `quoteSend` fee data. -- Stream failures -> CFA forwarder or host agreement calls, buffer and flow-rate limits per Superfluid docs linked from GoodDocs. -- Faucet top-up failures -> `canTop`, `onlyAuthorized`, daily or weekly caps; `references/deep-researches/faucet-flows.md`. -- DAO-gated reverts -> caller is not avatar; scheme not registered; `references/deep-researches/gooddao-daostack-surface.md`. - -## Library usage discipline - -1. Open `references/guides/gooddocs.md` when unsure which GoodDocs page applies. -2. Start at this file to classify intent. -3. Open one guide under `references/guides/` unless the user requests a multi-step workflow. For subgraph or indexed-data tasks, start at `references/subgraphs/_query-patterns.md`. -4. Read only the ABI references and matching `.selectors.yaml` files needed for the chosen action. -5. Prefer GoodDocs for documented behavior; use only `deployment.json` (and rich ABI `meta.deployments` aligned with it) for contract addresses—never infer addresses from GoodDocs. -6. For large historical reads, prefer `references/guides/hypersync-hyperrpc.md` and choose HyperSync over HyperRPC unless strict JSON-RPC compatibility is required. -7. Historical data routing is strict: subgraphs first; HyperSync or HyperRPC only with an explicit fallback reason. -8. HyperRPC usage requires Envio API key credentials; when absent, **explicitly ask the user** for `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` (or a full `HYPERRPC_URL`) and do not attempt anonymous production flow. -9. When HyperSync is the best historical-data path and no Envio API token is available, explicitly ask the user to provide `ENVIO_API_TOKEN` (or the token your client expects) before continuing; see `references/guides/hypersync-hyperrpc.md`. -10. For subgraph tasks, validate field availability from the relevant `references/subgraphs/*-guide.md` and companion `.graphql` before guessing alternate entities. -11. For local shells repeating HyperRPC log pulls (for example last N whitelist events), from the **GoodSkills repository root** run `scripts/fetch-whitelist-events-hyperrpc.mjs` per `references/guides/hypersync-hyperrpc.md` instead of re-deriving JSON-RPC setup each time; that script ships with **defaults for production Celo** (HyperRPC host + `Identity` contract from `deployment.json`) and URL composition from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN` unless you override `CONTRACT_ADDRESS` / `HYPERRPC_URL`. HyperSync remains a separate client install path documented in the same guide. diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml deleted file mode 100644 index 40226b3b..00000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.abi.yaml +++ /dev/null @@ -1,205 +0,0 @@ -meta: - name: BuyGDCloneFactory - version: "1" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol - note: > - Deterministic clone factory for BuyGDCloneV2 and DonateGDClone. Used to create per-owner - executors and optional create+swap flows for ramp services. - deployments: - mainnet: - production-celo: - BuyGDFactoryV2: - networkId: 42220 - address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" - creationBlock: 22909812 - BuyGDFactory: - networkId: 42220 - address: "0x00e533B7d6255D05b7f15034B1c989c21F51b91C" - creationBlock: 21006324 - related: - - references/deep-researches/on-off-ramp-service.md - - references/guides/on-off-ramp.md - -create: - notes: - - "Deploys deterministic BuyGDCloneV2 clone for owner and initializes it." - - "Difference vs createAndSwap: this only deploys; no swap is executed." - mutability: nonpayable - access: anyone - inputs: - - owner: address - outputs: - - clone: address - -createDonation: - notes: - - "Deploys deterministic DonateGDClone with donation target and call payload." - - "Difference vs create: deploys donation-capable implementation instead of plain swap clone." - mutability: nonpayable - access: anyone - inputs: - - owner: address - - donateOrExecTo: address - - callData: bytes - outputs: - - clone: address - -createAndSwap: - notes: - - "One-transaction helper: create BuyGDCloneV2 then immediately call clone.swap(minAmount, msg.sender)." - - "Difference vs create: bundles deployment and first swap for faster ramp UX." - mutability: nonpayable - access: anyone - inputs: - - owner: address - - minAmount: uint256 - outputs: - - clone: address - -createDonationAndSwap: - notes: - - "One-transaction helper for DonateGDClone: deploy, then execute donation flow with optional pre-swap." - - "Difference vs createDonation: can execute immediate swap and/or target call instead of deployment only." - mutability: nonpayable - access: anyone - inputs: - - owner: address - - donateOrExecTo: address - - withSwap: bool - - minAmount: uint256 - - callData: bytes - outputs: - - clone: address - -predict: - notes: - - "Computes deterministic address for create(owner) salt." - mutability: view - inputs: - - owner: address - outputs: - - clone: address - -predictDonation: - notes: - - "Computes deterministic address for createDonation(owner, donateOrExecTo, callData) salt." - mutability: view - inputs: - - owner: address - - donateOrExecTo: address - - callData: bytes - outputs: - - clone: address - -getBaseFee: - mutability: view - inputs: [] - outputs: - - baseFee: uint256 - -quoter: - mutability: pure - inputs: [] - outputs: - - addr: address - -CUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -celo: - mutability: pure - inputs: [] - outputs: - - token: address - -USDC: - mutability: pure - inputs: [] - outputs: - - token: address - -GLOUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -PERIOD: - mutability: pure - inputs: [] - outputs: - - seconds: uint24 - -impl: - mutability: view - inputs: [] - outputs: - - addr: address - -donateImpl: - mutability: view - inputs: [] - outputs: - - addr: address - -gd: - mutability: view - inputs: [] - outputs: - - token: address - -stable: - mutability: view - inputs: [] - outputs: - - token: address - -oracle: - mutability: view - inputs: [] - outputs: - - addr: address - -router: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoBroker: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeProvider: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeId: - mutability: view - inputs: [] - outputs: - - id: bytes32 - -events: - GDSwapToCusd: - indexed: [] - data: - - from: address - - to: address - - amountIn: uint256 - - amountOut: uint256 - - note: bytes - -errors: - NOT_GD_TOKEN: "onTokenTransfer caller is not G$ token." - INVALID_TWAP: "TWAP validation failed." - RECIPIENT_ZERO: "Recipient cannot be zero address." - ZERO_MINAMOUNT: "Minimum amount cannot be zero." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml deleted file mode 100644 index 16b13f22..00000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneFactory.selectors.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - create(address): 0x9ed93318 - createDonation(address,address,bytes): 0x48199e6b - createAndSwap(address,uint256): 0x89643a29 - createDonationAndSwap(address,address,bool,uint256,bytes): 0xeb5621b8 - predict(address): 0x901b96e7 - predictDonation(address,address,bytes): 0x3895de30 - getBaseFee(): 0x15e812ad - quoter(): 0xc6bbd5a7 - CUSD(): 0x758316c9 - celo(): 0x051ed8ef - USDC(): 0x89a30271 - GLOUSD(): 0x4b5b02d6 - PERIOD(): 0xb4d1d795 - impl(): 0x8abf6077 - donateImpl(): 0x21fc2eef - gd(): 0xa5e598fc - stable(): 0x22be3de1 - oracle(): 0x7dc0d1d0 - router(): 0xf887ea40 - mentoBroker(): 0x7b89f117 - mentoExchangeProvider(): 0x4f62feec - mentoExchangeId(): 0xd373b333 -events: - GDSwapToCusd(address,address,uint256,uint256,bytes): 0x252bc23e3fb01f9986fa157af621236fb8a706ea12622da296e7f2f30d4f1a56 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml deleted file mode 100644 index 5f35ee91..00000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.abi.yaml +++ /dev/null @@ -1,282 +0,0 @@ -meta: - name: BuyGDCloneV2 - version: "2" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/BuyGDClone.sol - inherits: - - Initializable - note: > - Clone implementation used by BuyGDCloneFactory for deterministic per-owner swap executors. - Supports CELO and cUSD flows, with cUSD route selection between Uniswap and Mento when configured. - deployments: - mainnet: - production-celo: - BuyGDFactoryV2: - networkId: 42220 - address: "0x1F60C4C7037C6766924A43666B781ED1479587a2" - creationBlock: 22909812 - related: - - references/deep-researches/on-off-ramp-service.md - - references/guides/on-off-ramp.md - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _owner: address - outputs: [] - -getSwapPath: - mutability: pure - inputs: - - tokens: address[] - - fees: uint24[] - outputs: - - path: bytes - -swap: - notes: - - "Dispatcher entrypoint: if clone holds native CELO balance it executes CELO route, otherwise uses cUSD route." - - "Difference vs swapCelo/swapCusd: this function chooses route by contract balances; specialized functions force a specific asset path." - mutability: payable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - outputs: - - bought: uint256 - emits: [Bought] - errors: [NO_BALANCE] - -swapCelo: - notes: - - "CELO-only convenience path using the default preconfigured Uniswap route." - - "Difference vs swapCeloWithPath: same Uniswap execution, but caller cannot override token path and fee tiers." - - "Difference vs cUSD paths: CELO path does not use Mento route selection." - mutability: payable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - outputs: - - bought: uint256 - emits: [BoughtFromUniswap] - errors: [REFUND_FAILED] - -swapCeloWithPath: - notes: - - "CELO-only path with caller-supplied Uniswap multi-hop route." - - "Difference vs swapCelo: allows custom path optimization when default route is not preferred." - - "Difference vs cUSD paths: still Uniswap-only and does not compare against Mento." - mutability: payable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - - _path: (address[],uint24[]) - outputs: - - bought: uint256 - emits: [BoughtFromUniswap] - errors: [REFUND_FAILED] - -swapCusd: - notes: - - "cUSD-only convenience path that compares expected output from default-path Uniswap and Mento, then executes the better quote." - - "Difference vs swapCusdWithPath: this uses hardcoded default Uniswap path for comparison." - - "Difference vs CELO paths: includes dual-route best-execution logic with optional Mento." - mutability: nonpayable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - outputs: - - bought: uint256 - emits: [BoughtFromMento, BoughtFromUniswap] - errors: [MENTO_NOT_CONFIGURED] - -swapCusdWithPath: - notes: - - "cUSD path with caller-supplied Uniswap route; still compares custom Uniswap quote against Mento quote and picks larger expected output." - - "Difference vs swapCusd: custom path changes only the Uniswap side of the comparison." - - "Difference vs swapCeloWithPath: this function performs route competition (Uniswap vs Mento), not only route customization." - mutability: nonpayable - access: anyone - inputs: - - _minAmount: uint256 - - refundGas: address - - _path: (address[],uint24[]) - outputs: - - bought: uint256 - emits: [BoughtFromMento, BoughtFromUniswap] - errors: [MENTO_NOT_CONFIGURED] - -getExpectedReturnFromUniswapPath: - notes: - - "Quote helper for Uniswap path expected output used by cUSD route selection and preflight checks." - mutability: nonpayable - inputs: - - amountIn: uint256 - - _path: (address[],uint24[]) - outputs: - - expectedReturn: uint256 - -getExpectedReturnFromMento: - notes: - - "Quote helper for Mento expected output for cUSD->G$ used in best-route decision." - mutability: view - inputs: - - cusdAmount: uint256 - outputs: - - expectedReturn: uint256 - errors: [MENTO_NOT_CONFIGURED] - -minAmountByTWAP: - mutability: view - inputs: - - baseAmount: uint256 - - baseToken: address - - period: uint32 - outputs: - - minTwap: uint256 - - quote: uint256 - -recover: - mutability: nonpayable - access: anyone - inputs: - - token: address - outputs: [] - errors: [REFUND_FAILED] - -router: - mutability: view - inputs: [] - outputs: - - addr: address - -celo: - mutability: pure - inputs: [] - outputs: - - token: address - -CUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -USDC: - mutability: pure - inputs: [] - outputs: - - token: address - -GLOUSD: - mutability: pure - inputs: [] - outputs: - - token: address - -GD_FEE_TIER: - mutability: pure - inputs: [] - outputs: - - tier: uint24 - -CUSD_STABLE_FEE_TIER: - mutability: pure - inputs: [] - outputs: - - tier: uint24 - -CELO_STABLE_FEE_TIER: - mutability: pure - inputs: [] - outputs: - - tier: uint24 - -twapPeriod: - mutability: view - inputs: [] - outputs: - - period: uint32 - -stable: - mutability: view - inputs: [] - outputs: - - token: address - -gd: - mutability: view - inputs: [] - outputs: - - token: address - -oracle: - mutability: view - inputs: [] - outputs: - - addr: address - -quoter: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoBroker: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeProvider: - mutability: view - inputs: [] - outputs: - - addr: address - -mentoExchangeId: - mutability: view - inputs: [] - outputs: - - id: bytes32 - -owner: - mutability: view - inputs: [] - outputs: - - addr: address - -CUSD_GAS_COSTS: - mutability: pure - inputs: [] - outputs: - - amount: uint256 - -events: - Bought: - indexed: [] - data: - - inToken: address - - inAmount: uint256 - - outAmount: uint256 - BoughtFromMento: - indexed: [] - data: - - inToken: address - - inAmount: uint256 - - outAmount: uint256 - BoughtFromUniswap: - indexed: [] - data: - - inToken: address - - inAmount: uint256 - - outAmount: uint256 - -errors: - REFUND_FAILED: "Refund call failed." - NO_BALANCE: "No CELO or cUSD available on clone." - MENTO_NOT_CONFIGURED: "Mento broker path not configured." diff --git a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml deleted file mode 100644 index 1476ae35..00000000 --- a/.agents/skills/gooddollar/references/contracts/BuyGDCloneV2.selectors.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address): 0xc4d66de8 - getSwapPath(address[],uint24[]): 0xf0036d5d - swap(uint256,address): 0xd3986f08 - swapCelo(uint256,address): 0xaa6bfa9d - swapCeloWithPath(uint256,address,(address[],uint24[])): 0x81b7d2e0 - swapCusd(uint256,address): 0xb1a5fa9f - swapCusdWithPath(uint256,address,(address[],uint24[])): 0x170ba915 - getExpectedReturnFromUniswapPath(uint256,(address[],uint24[])): 0x12194320 - getExpectedReturnFromMento(uint256): 0xdbb15eb2 - minAmountByTWAP(uint256,address,uint32): 0x821dc910 - recover(address): 0x0cd865ec - router(): 0xf887ea40 - celo(): 0x051ed8ef - CUSD(): 0x758316c9 - USDC(): 0x89a30271 - GLOUSD(): 0x4b5b02d6 - GD_FEE_TIER(): 0xe00e8fdd - CUSD_STABLE_FEE_TIER(): 0xba428926 - CELO_STABLE_FEE_TIER(): 0x60e4bf4b - twapPeriod(): 0xf6207326 - stable(): 0x22be3de1 - gd(): 0xa5e598fc - oracle(): 0x7dc0d1d0 - quoter(): 0xc6bbd5a7 - mentoBroker(): 0x7b89f117 - mentoExchangeProvider(): 0x4f62feec - mentoExchangeId(): 0xd373b333 - owner(): 0x8da5cb5b - CUSD_GAS_COSTS(): 0x32f90ac3 -events: - Bought(address,uint256,uint256): 0xa9a40dec7a304e5915d11358b968c1e8d365992abf20f82285d1df1b30c8e24c - BoughtFromMento(address,uint256,uint256): 0x8e2ac24d7ef5662ee242823a19dbd1c952b3e96ae127228f4bbce83e2816e3fb - BoughtFromUniswap(address,uint256,uint256): 0xdb1f2a6cbbfd964f19c648b140a35992aa80f482df8f519d3211d0bd86c9f335 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml deleted file mode 100644 index 32ba0de5..00000000 --- a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.abi.yaml +++ /dev/null @@ -1,237 +0,0 @@ -# CFAv1Forwarder — convenience wrapper for ConstantFlowAgreementV1 -# Allows direct interaction with CFA functions without manually routing through -# the Host's batchCall / forwardBatchCall. Operates as a trusted forwarder, -# preserving msg.sender via EIP-2771. -# -# Each forwarder call is a standalone transaction — forwarder calls cannot be -# combined in a Host.batchCall. To batch multiple operations atomically (e.g. -# wrap tokens + create stream), use Host.batchCall with the raw CFA agreement -# (operationType 201). See Superfluid.abi.yaml for batch operation details. -# Tradeoff: forwarder calls produce human-readable descriptions in wallets -# (e.g. "setFlowrate(token, receiver, flowrate)"), while Host.batchCall -# shows encoded tuples that are difficult for users to verify. -# -# This contract has no events — all events are emitted by the underlying CFA. -# Errors from the CFA (and Host) propagate through to the caller. - -meta: - name: CFAv1Forwarder - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/CFAv1Forwarder.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/utils/ForwarderBase.sol - inherits: [ForwarderBase] - deployments: - # Same address on all networks except avalanche-fuji - mainnet: - eth-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - polygon-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - xdai-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - base-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - optimism-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - arbitrum-one: "0xcfA132E353cB4E398080B9700609bb008eceB125" - bsc-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - avalanche-c: "0xcfA132E353cB4E398080B9700609bb008eceB125" - celo-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - scroll-mainnet: "0xcfA132E353cB4E398080B9700609bb008eceB125" - degenchain: "0xcfA132E353cB4E398080B9700609bb008eceB125" - testnet: - avalanche-fuji: "0x2CDd45c5182602a36d391F7F16DD9f8386C3bD8D" - base-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - eth-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - optimism-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - scroll-sepolia: "0xcfA132E353cB4E398080B9700609bb008eceB125" - deploymentCreationBlocks: - mainnet: - celo-mainnet: 17404156 - -# == Glossary == -# flowrate — token transfer rate in wad/second (int96) -# flowOperator — account authorized to create/update/delete flows on behalf of another -# buffer — deposit locked as solvency collateral while a flow is active -# permissions — bitmask of create | update | delete rights for an operator -# flowrateAllowance — max flowrate an operator may set per individual flow - -# == Flow Management == -# High-level functions that automatically create, update, or delete flows as needed. - -setFlowrate: - # Smart setter: creates a new flow if none exists, updates if flowrate changed, - # deletes if flowrate is zero. No-ops if the current rate already matches. - notes: - - "Gotcha: Negative flowrate values revert with CFA_FWD_INVALID_FLOW_RATE." - mutability: nonpayable - access: anyone # flow from msg.sender - inputs: - - token: address - - receiver: address - - flowrate: int96 # must be >= 0 - outputs: - - bool - errors: [CFA_FWD_INVALID_FLOW_RATE] - -setFlowrateFrom: - # Same as setFlowrate but can be called by a flow operator on behalf of `sender`. - # msg.sender must have sufficient operator permissions and flowrateAllowance. - mutability: nonpayable - access: sender | operator - inputs: - - token: address - - sender: address - - receiver: address - - flowrate: int96 - outputs: - - bool - errors: [CFA_FWD_INVALID_FLOW_RATE] - -# == Low-Level Flow Operations == -# Direct wrappers around CFA functions. These give full control (including userData) -# but require the caller to know whether a flow already exists. -# If sender != msg.sender, the *ByOperator variant is used internally. - -createFlow: - # Create a new flow. Reverts if a flow already exists between sender and receiver. - mutability: nonpayable - access: sender | operator - inputs: - - token: address - - sender: address - - receiver: address - - flowrate: int96 - - userData: bytes - outputs: - - bool - -updateFlow: - # Update an existing flow's rate. Reverts if no flow exists. - mutability: nonpayable - access: sender | operator - inputs: - - token: address - - sender: address - - receiver: address - - flowrate: int96 - - userData: bytes - outputs: - - bool - -deleteFlow: - # Delete an existing flow. Can be called by sender, receiver, or an operator. - # If msg.sender is neither sender nor receiver, deleteFlowByOperator is used. - mutability: nonpayable - access: sender | receiver | operator - inputs: - - token: address - - sender: address - - receiver: address - - userData: bytes - outputs: - - bool - -# == ACL (Operator Permissions) == - -grantPermissions: - # Grant full create/update/delete permissions with max flowrateAllowance to an operator. - # Convenience wrapper — equivalent to updateFlowOperatorPermissions with full control. - mutability: nonpayable - access: anyone # grants on msg.sender's flows - inputs: - - token: address - - flowOperator: address - outputs: - - bool - -revokePermissions: - # Revoke all permissions from an operator. Does not affect existing flows. - mutability: nonpayable - access: anyone # revokes on msg.sender's flows - inputs: - - token: address - - flowOperator: address - outputs: - - bool - -updateFlowOperatorPermissions: - # Set granular operator permissions and flowrate allowance. - notes: - - "Gotcha: flowrateAllowance limits per-flow rate, NOT aggregate net flow." - mutability: nonpayable - access: anyone # grants on msg.sender's flows - inputs: - - token: address - - flowOperator: address - - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete - - flowrateAllowance: int96 - outputs: - - bool - -# == Flow Queries == - -getFlowrate: - mutability: view - inputs: - - token: address - - sender: address - - receiver: address - outputs: - - flowrate: int96 - -getFlowInfo: - mutability: view - inputs: - - token: address - - sender: address - - receiver: address - outputs: - - lastUpdated: uint256 - - flowrate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -getBufferAmountByFlowrate: - # Returns the deposit/buffer required for a given flowrate. - notes: - - "Gotcha: This value is governance-configurable and may change over time. Changes only affect newly created/updated flows." - mutability: view - inputs: - - token: address - - flowrate: int96 - outputs: - - bufferAmount: uint256 - -getAccountFlowrate: - # Net aggregate flowrate for an account (incoming minus outgoing). - mutability: view - inputs: - - token: address - - account: address - outputs: - - flowrate: int96 - -getAccountFlowInfo: - mutability: view - inputs: - - token: address - - account: address - outputs: - - lastUpdated: uint256 - - flowrate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -# == ACL Queries == - -getFlowOperatorPermissions: - mutability: view - inputs: - - token: address - - sender: address - - flowOperator: address - outputs: - - permissions: uint8 - - flowrateAllowance: int96 - -# == Errors == - -errors: - - CFA_FWD_INVALID_FLOW_RATE # flowrate argument was negative diff --git a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml b/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml deleted file mode 100644 index f98e21ea..00000000 --- a/.agents/skills/gooddollar/references/contracts/CFAv1Forwarder.selectors.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - setFlowrate(address,address): 0x2f57fb6f - setFlowrateFrom(address,address,address,int96): 0xc5ad5c1a - createFlow(address,address,address,int96,bytes): 0xe15536b6 - updateFlow(address,address,address,int96,bytes): 0x0c033991 - deleteFlow(address,address,address,bytes): 0xb4b333c6 - grantPermissions(address,address): 0x7243fb93 - revokePermissions(address,address): 0x0bd0728d - updateFlowOperatorPermissions(address,address): 0x42294caf - getFlowrate(address,address,address): 0x1d8b6526 - getFlowInfo(address,address,address): 0x2860fd93 - getBufferAmountByFlowrate(address,int96): 0x09f0b495 - getAccountFlowrate(address,address): 0x22c904d9 - getAccountFlowInfo(address,address): 0x0f1ac495 - getFlowOperatorPermissions(address,address,address): 0x4d3f60f9 -events: -{} -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml deleted file mode 100644 index afaec5dc..00000000 --- a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.abi.yaml +++ /dev/null @@ -1,560 +0,0 @@ -# Superfluid Constant Flow Agreement (CFA) v1 -# Manages continuous per-second token streams between accounts. -# -# NOTE: emits/errors mappings are traced from source code — verify against implementation. -# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID) -# are omitted — they belong to the UUPSProxiable / AgreementBase layer. -# Pure helpers addPermissions/removePermissions are omitted — they were made public -# for testability only and are not part of the protocol interface. - -meta: - name: ConstantFlowAgreementV1 - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/ConstantFlowAgreementV1.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/agreements/AgreementBase.sol - implements: [IConstantFlowAgreementV1, ISuperAgreement] - inherits: [AgreementBase] - deployments: - mainnet: - eth-mainnet: "0x2844c1BBdA121E9E43105630b9C8310e5c72744b" - polygon-mainnet: "0x6EeE6060f715257b970700bc2656De21dEdF074C" - xdai-mainnet: "0xEbdA4ceF883A7B12c4E669Ebc58927FBa8447C7D" - base-mainnet: "0x19ba78B9cDB05A877718841c574325fdB53601bb" - optimism-mainnet: "0x204C6f131bb7F258b2Ea1593f5309911d8E458eD" - arbitrum-one: "0x731FdBB12944973B500518aea61942381d7e240D" - bsc-mainnet: "0x49c38108870e74Cb9420C0991a85D3edd6363F75" - avalanche-c: "0x6946c5B38Ffea373b0a2340b4AEf0De8F6782e58" - celo-mainnet: "0x9d369e78e1a682cE0F8d9aD849BeA4FE1c3bD3Ad" - scroll-mainnet: "0xB3bcD6da1eeB6c97258B3806A853A6dcD3B6C00c" - degenchain: "0x82cc052d1b17aC554a22A88D5876B56c6b51e95c" - testnet: - avalanche-fuji: "0x16843ac25Ccc58Aa7960ba05f61cBB17b36b130A" - base-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" - eth-sepolia: "0x6836F23d6171D74Ef62FcF776655aBcD2bcd62Ef" - optimism-sepolia: "0x8a3170AdbC67233196371226141736E4151e7C26" - scroll-sepolia: "0xbc46B4Aa41c055578306820013d4B65fff42711E" - deploymentCreationBlocks: - mainnet: - celo-mainnet: 16393492 - -# == Abbreviations == -# 3Ps — Three Periods: liquidation, patrician, and pleb (solvency model) -# ACL — Access Control List (flow operator permissions) -# CFA — Constant Flow Agreement -# ctx — context (Superfluid call context bytes, carries msg.sender and userData) -# GDA — General Distribution Agreement (the other Superfluid agreement) -# PPP — Patrician-Pleb-Pirate (the 3Ps solvency periods) - -# == Glossary == -# flow / stream — continuous per-second token transfer, used interchangeably -# flowRate — tokens per second (int96, wei-denominated); positive = outgoing -# deposit — buffer locked when opening a flow; protects against insolvency -# owed deposit — portion of deposit owed back to the sender by a Super App receiver -# flow operator — an address authorized to create/update/delete flows on behalf of a sender -# flow rate allowance — maximum net flow rate an operator may allocate (int96); decrements on use -# permissions bitmask — uint8: bit 0 = create (1), bit 1 = update (2), bit 2 = delete (4) -# liquidation — closing an insolvent sender's flow; rewards go to the liquidator or bond -# patrician period — grace window after insolvency where reward goes to the bond account -# pleb period — window after patrician where reward goes to the liquidator -# pirate / bailout — state where total deposit cannot cover the deficit; protocol bails out -# Super App — a contract registered with the Host that receives agreement callbacks -# app credit — deposit credit enabling "zero-balance Super Apps" to relay flows -# without pre-funded tokens. 1:1 relay (one in, one out at same rate) -# always works at zero balance. Fan-out (1:N) needs the app to hold -# tokens. Credit is settled as "owed deposit" on the original sender. -# critical — an account whose available balance is negative (eligible for liquidation) -# jailed — a Super App penalized for violating protocol rules; loses callbacks -# -# == Time Conventions == -# Conventional seconds-per-period used in Superfluid apps for flow rate conversion: -# month: 2628000 (365.25 / 12 * 86400) -# year: 31536000 (365 * 86400) -# Flow rate from a monthly amount: flowRate = monthlyAmount / 2628000 -# Flow rate from a yearly amount: flowRate = yearlyAmount / 31536000 - -# == Flow Management == -# Core operations for creating, updating, and deleting token streams. -# Functions with ctx are called through the Host — either via -# Host.callAgreement (single op) or Host.batchCall (operationType 201). -# For batchCall, the data field is abi.encode(callData, userData) where -# callData is the full ABI-encoded function call with an empty ctx ("0x") -# as placeholder. The Host replaces the placeholder with the real context. -# The access field reflects who can initiate the call, not the direct caller. - -createFlow: - # Start a new stream from ctx.msgSender to receiver. - # The deposit amount depends on the governance-configured liquidation period - # and minimum deposit. The lower 32 bits of the deposit are clipped (rounded up). - notes: - - "Gotcha: A deposit is taken as a safety margin for solvency agents." - mutability: nonpayable - access: anyone # flow from ctx.msgSender - inputs: - - token: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] - -updateFlow: - # Change the flow rate of an existing stream from ctx.msgSender to receiver. - # Note: owedDeposit accumulates when a Super App receiver uses app credit to - # open outgoing streams. The sender bears the deposit cost for those streams. - notes: - - "Gotcha: Deposit is adjusted to match the new flow rate but never refunds owed deposit — that adjustment happens separately via the app credit system." - mutability: nonpayable - access: anyone # flow from ctx.msgSender - inputs: - - token: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] - -deleteFlow: - # Stop a stream between sender and receiver. - # During liquidation the reward distribution depends on the solvency period: - # patrician → bond account gets reward; pleb → liquidator gets reward; - # pirate → liquidator gets full single deposit, protocol covers the bailout. - notes: - - "Gotcha: Third-party callers (not sender, receiver, or operator) only succeed if the sender is critical (negative available balance) or either party is jailed." - mutability: nonpayable - access: sender | receiver | operator | anyone(if-critical-or-jailed) - inputs: - - token: address - - sender: address - - receiver: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] - -# == Operator Flow Management == -# Act on behalf of another account. Requires ACL permissions granted by the sender. -# The operator's flow rate allowance is decremented on create/update. -# Functions with ctx are called through the Host. - -createFlowByOperator: - # Create a flow on behalf of sender. Consumes flow rate allowance. - notes: - - "Gotcha: Reverts if ctx.msgSender IS the sender — use createFlow instead." - mutability: nonpayable - access: operator - inputs: - - token: address - - sender: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ACL_NO_SENDER_CREATE, CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_ALREADY_EXISTS, CFA_INSUFFICIENT_BALANCE] - -updateFlowByOperator: - # Update a flow on behalf of sender. Only consumes allowance if flow rate increases. - # If flowRateAllowance is type(int96).max, it is treated as unlimited. - notes: - - "Gotcha: If the new rate is lower than the old rate, no allowance is consumed." - mutability: nonpayable - access: operator - inputs: - - token: address - - sender: address - - receiver: address - - flowRate: int96 # must be > 0 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ACL_NO_SENDER_UPDATE, CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS, CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED, CFA_ZERO_ADDRESS_RECEIVER, CFA_NO_SELF_FLOW, CFA_INVALID_FLOW_RATE, CFA_FLOW_DOES_NOT_EXIST, CFA_INSUFFICIENT_BALANCE] - -deleteFlowByOperator: - # Delete a flow on behalf of sender. Does not consume flow rate allowance. - mutability: nonpayable - access: operator - inputs: - - token: address - - sender: address - - receiver: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] - errors: [CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS, CFA_ZERO_ADDRESS_SENDER, CFA_ZERO_ADDRESS_RECEIVER, CFA_FLOW_DOES_NOT_EXIST, CFA_NON_CRITICAL_SENDER] - -# == Flow Queries == - -realtimeBalanceOf: - # Compute the real-time CFA balance contribution for an account. - # Returns the dynamic balance delta (flowRate * elapsed), total deposit held, - # and total owed deposit. This is called by the token's realtimeBalanceOfNow - # to assemble the full balance across all agreements. - # To get an account's actual token balance, use SuperToken.realtimeBalanceOfNow - # (or SuperToken.balanceOf for ERC-20 compatible, clamped to zero). - # It can be deeply negative for net-outflow accounts as time progresses. - notes: - - "Gotcha: Returns only the CFA portion of the balance, not the total." - - "Gotcha: The returned dynamicBalance is a delta, not an absolute balance." - mutability: view - inputs: - - token: address - - account: address - - time: uint256 - outputs: - - dynamicBalance: int256 # flowRate * (time - lastUpdate); can be negative - - deposit: uint256 # total deposit locked across all outflows - - owedDeposit: uint256 # total owed deposit from Super App receivers - -getFlow: - # Get flow data between a specific sender-receiver pair. - mutability: view - inputs: - - token: address - - sender: address - - receiver: address - outputs: - - timestamp: uint256 # last update time - - flowRate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -getFlowByID: - # Get flow data using the agreement ID (keccak256 of sender and receiver). - mutability: view - inputs: - - token: address - - flowId: bytes32 # keccak256(abi.encode(sender, receiver)) - outputs: - - timestamp: uint256 - - flowRate: int96 - - deposit: uint256 - - owedDeposit: uint256 - -getAccountFlowInfo: - # Aggregated flow state for an account across all its CFA flows. - mutability: view - inputs: - - token: address - - account: address - outputs: - - timestamp: uint256 # last time any flow was updated for this account - - flowRate: int96 # net flow rate (inflows - outflows) - - deposit: uint256 # sum of deposits across all outflows - - owedDeposit: uint256 # sum of owed deposits across all outflows - -getNetFlow: - # Net flow rate for an account (sum of inflows minus outflows). - mutability: view - inputs: - - token: address - - account: address - outputs: - - flowRate: int96 - -# == ACL Management == -# Manage flow operator permissions and flow rate allowances. -# The caller (ctx.msgSender) is always the permission granter — they control -# who can operate on their own flows. -# Functions with ctx are called through the Host. - -updateFlowOperatorPermissions: - # Set exact permissions and flow rate allowance for a flow operator. - mutability: nonpayable - access: anyone # grants on ctx.msgSender's flows - inputs: - - token: address - - flowOperator: address - - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete - - flowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -authorizeFlowOperatorWithFullControl: - # Grant all permissions (create+update+delete) with unlimited allowance. - # Shorthand for updateFlowOperatorPermissions(token, op, 7, type(int96).max, ctx). - mutability: nonpayable - access: anyone # grants on ctx.msgSender's flows - inputs: - - token: address - - flowOperator: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -revokeFlowOperatorWithFullControl: - # Revoke all permissions and set allowance to zero. - # Shorthand for updateFlowOperatorPermissions(token, op, 0, 0, ctx). - mutability: nonpayable - access: anyone # revokes on ctx.msgSender's flows - inputs: - - token: address - - flowOperator: address - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -increaseFlowRateAllowance: - # Increase flow rate allowance for an operator by a delta. - # Delegates to increaseFlowRateAllowanceWithPermissions with permissionsToAdd=0. - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - addedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -decreaseFlowRateAllowance: - # Decrease flow rate allowance for an operator by a delta. - # Delegates to decreaseFlowRateAllowanceWithPermissions with permissionsToRemove=0. - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - subtractedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -increaseFlowRateAllowanceWithPermissions: - # Increase flow rate allowance and add permission bits in one call. - # permissionsToAdd is OR'd with existing permissions. - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - permissionsToAdd: uint8 # bitmask OR'd onto existing permissions - - addedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -decreaseFlowRateAllowanceWithPermissions: - # Decrease flow rate allowance and remove permission bits in one call. - # permissionsToRemove is AND-NOT'd from existing permissions. - notes: - - "Gotcha: Reverts if the resulting allowance would go negative." - mutability: nonpayable - access: anyone # ctx.msgSender's allowance - inputs: - - token: address - - flowOperator: address - - permissionsToRemove: uint8 # bitmask AND-NOT'd from existing permissions - - subtractedFlowRateAllowance: int96 - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowOperatorUpdated] - errors: [CFA_ACL_UNCLEAN_PERMISSIONS, CFA_ACL_NO_SENDER_FLOW_OPERATOR, CFA_ACL_NO_NEGATIVE_ALLOWANCE] - -# == ACL Queries == - -getFlowOperatorData: - # Get permissions and allowance for a flow operator. - mutability: view - inputs: - - token: address - - sender: address # the permission granter - - flowOperator: address # the permission grantee - outputs: - - flowOperatorId: bytes32 # keccak256(abi.encode("flowOperator", sender, flowOperator)) - - permissions: uint8 # bitmask: 1=create, 2=update, 4=delete - - flowRateAllowance: int96 - -getFlowOperatorDataByID: - # Get permissions and allowance using the pre-computed operator ID. - mutability: view - inputs: - - token: address - - flowOperatorId: bytes32 - outputs: - - permissions: uint8 - - flowRateAllowance: int96 - -# == Solvency Queries == -# Check whether an account is in the patrician period (grace window for liquidation). - -isPatricianPeriodNow: - # Check patrician period status using the Host's current timestamp. - mutability: view - inputs: - - token: address - - account: address - outputs: - - isCurrentlyPatricianPeriod: bool - - timestamp: uint256 # the Host's block.timestamp used for the check - -isPatricianPeriod: - # Check patrician period status at a specific timestamp. - mutability: view - inputs: - - token: address - - account: address - - timestamp: uint256 - outputs: - - bool - -# == Deposit Helpers == - -getMaximumFlowRateFromDeposit: - # Calculate the maximum flow rate achievable with a given deposit. - notes: - - "Gotcha: Deposit is clipped (lower 32 bits zeroed) and rounded down." - mutability: view - inputs: - - token: address # needed to look up liquidation period from governance - - deposit: uint256 - outputs: - - flowRate: int96 - errors: [CFA_DEPOSIT_TOO_BIG] - -getDepositRequiredForFlowRate: - # Calculate the deposit required for a given flow rate. - # Returns max(minimumDeposit, flowRate * liquidationPeriod) with rounding. - mutability: view - inputs: - - token: address # needed to look up liquidation period and minimum deposit - - flowRate: int96 - outputs: - - deposit: uint256 - errors: [CFA_INVALID_FLOW_RATE, CFA_FLOW_RATE_TOO_BIG] - -# == Protocol Constants == - -agreementType: - # Returns keccak256("org.superfluid-finance.agreements.ConstantFlowAgreement.v1") - mutability: pure - outputs: - - bytes32 - -DEFAULT_MINIMUM_DEPOSIT: - # Minimum deposit floor: uint96(1 << 32) ≈ 4.29 * 10^9 wei. - # Governance may set a higher per-token minimum; this is the absolute floor. - mutability: view - outputs: - - uint256 - -MAXIMUM_DEPOSIT: - # Maximum deposit cap: type(int96).max - mutability: view - outputs: - - uint256 - -MAXIMUM_FLOW_RATE: - # Maximum flow rate: type(int96).max - mutability: view - outputs: - - uint256 - -CFA_HOOK_GAS_LIMIT: - # Gas limit for external hook calls (Super App callbacks): 250,000 - mutability: view - outputs: - - uint64 - -# == Events == - -events: - FlowUpdated: - # Emitted on every create, update, and delete flow operation. - # totalSenderFlowRate and totalReceiverFlowRate are the NET flow rates - # after the operation, not the individual flow rate. - notes: - - "Gotcha: Always emitted together with FlowUpdatedExtension." - indexed: - - token: address - - sender: address - - receiver: address - data: - - flowRate: int96 # new rate for this specific flow (0 on delete) - - totalSenderFlowRate: int256 # sender's net flow rate after this operation - - totalReceiverFlowRate: int256 - - userData: bytes - - FlowUpdatedExtension: - # Companion event to FlowUpdated. Carries the operator and new deposit. - # Emitted immediately after FlowUpdated so indexers can correlate them. - indexed: - - flowOperator: address # ctx.msgSender who initiated the operation - data: - - deposit: uint256 # new deposit for this specific flow - - FlowOperatorUpdated: - # Emitted when operator permissions or flow rate allowance change. - indexed: - - token: address - - sender: address # the permission granter - - flowOperator: address # the permission grantee - data: - - permissions: uint8 # updated bitmask - - flowRateAllowance: int96 # updated allowance - - # Inherited events (from AgreementBase / UUPSProxiable): - # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) - # Initialized — emitted on proxy initialization (version) - -# == Errors == - -errors: - # Flow validation - - CFA_FLOW_ALREADY_EXISTS # createFlow when flow exists - - CFA_FLOW_DOES_NOT_EXIST # update/delete when no flow - - CFA_INVALID_FLOW_RATE # flowRate <= 0 - - CFA_NO_SELF_FLOW # sender == receiver - - CFA_ZERO_ADDRESS_SENDER # sender is address(0) - - CFA_ZERO_ADDRESS_RECEIVER # receiver is address(0) - # Solvency - - CFA_INSUFFICIENT_BALANCE # sender cannot cover deposit - - CFA_NON_CRITICAL_SENDER # third-party delete but sender is solvent - - CFA_DEPOSIT_TOO_BIG # deposit > MAXIMUM_DEPOSIT - - CFA_FLOW_RATE_TOO_BIG # flowRate * liquidationPeriod overflows - # ACL - - CFA_ACL_FLOW_RATE_ALLOWANCE_EXCEEDED # operator exceeds granted allowance - - CFA_ACL_NO_NEGATIVE_ALLOWANCE # resulting allowance would be < 0 - - CFA_ACL_NO_SENDER_CREATE # operator cannot be the sender (use createFlow) - - CFA_ACL_NO_SENDER_FLOW_OPERATOR # cannot set yourself as your own operator - - CFA_ACL_NO_SENDER_UPDATE # operator cannot be the sender (use updateFlow) - - CFA_ACL_OPERATOR_NO_CREATE_PERMISSIONS - - CFA_ACL_OPERATOR_NO_DELETE_PERMISSIONS - - CFA_ACL_OPERATOR_NO_UPDATE_PERMISSIONS - - CFA_ACL_UNCLEAN_PERMISSIONS # permission bits outside valid range - # Super App / Host - - CFA_HOOK_OUT_OF_GAS # Super App callback exceeded gas limit - - APP_RULE: # Super App rule violation - inputs: - - _code: uint256 - - AGREEMENT_BASE_ONLY_HOST # call not routed through the Host contract - # SafeCast (inherited from OpenZeppelin) - - SafeCastOverflowedIntToUint: # int256 value overflows on cast to uint256 - inputs: - - value: int256 - - SafeCastOverflowedUintToInt: # uint256 value overflows on cast to int256 - inputs: - - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml b/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml deleted file mode 100644 index a929e06c..00000000 --- a/.agents/skills/gooddollar/references/contracts/ConstantFlowAgreementV1.selectors.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - createFlow(address,address): 0x7deac86a - updateFlow(address,address): 0xb6214437 - deleteFlow(address,address,address,bytes): 0xb4b333c6 - createFlowByOperator(address,address,address): 0x97b0b745 - updateFlowByOperator(address,address,address): 0xaea4dc29 - deleteFlowByOperator(address,address,address,bytes): 0x4c8b181f - realtimeBalanceOf(address,address,uint256): 0x9b2e48bc - getFlow(address,address,address): 0xe6a1e888 - getFlowByID(address): 0x4eccd8ac - getAccountFlowInfo(address,address): 0x0f1ac495 - getNetFlow(address,address): 0xe8e7e2d1 - updateFlowOperatorPermissions(address,address): 0x42294caf - authorizeFlowOperatorWithFullControl(address,address,bytes): 0x54b770e3 - revokeFlowOperatorWithFullControl(address,address,bytes): 0x062e56ec - increaseFlowRateAllowance(address,address,int96,bytes): 0xac5f5d00 - decreaseFlowRateAllowance(address,address,int96,bytes): 0x5f51fb23 - increaseFlowRateAllowanceWithPermissions(address,address): 0x5e6a4dc3 - decreaseFlowRateAllowanceWithPermissions(address,address): 0x8de39d9b - getFlowOperatorData(address): 0x5f13dbbc - getFlowOperatorDataByID(address,bytes32): 0x09d256ef - isPatricianPeriodNow(address,address): 0x4fe9c291 - isPatricianPeriod(address,address,uint256): 0x4b839e0b - getMaximumFlowRateFromDeposit(): 0x4ba52c34 - getDepositRequiredForFlowRate(): 0xcc111b9e - CFA_HOOK_GAS_LIMIT(): 0xbf3fbc28 -events: - FlowUpdated(address,address,address,int256,bytes): 0x8e3b8f31fe09d2ca20fa7f76ec574bc9fea49f16a91f1ca828154ea76e76a20a - FlowUpdatedExtension(): 0x9bf0b3e0199b411ca09607d040b7672430af5dae9d159b715c9f8004c876065f - FlowOperatorUpdated(address): 0x401d2cb05b3db80617d70aabf976f52d2a5dac64298eadeed7a81ad4bb5d4fdd - # Inherited events (from AgreementBase / UUPSProxiable)(): 0xcabfd4ad95e3bb803739ac513717fccee724fc0c5741e01cf84319a96120eefa -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml deleted file mode 100644 index c60d4985..00000000 --- a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.abi.yaml +++ /dev/null @@ -1,100 +0,0 @@ -# GoodDollarOFTAdapter — LayerZero V2 OFT adapter pattern bridging underlying G$ -# Exact bytecode lives in the GoodDollar OFT deployment; this documents the surface agents call. - -meta: - name: GoodDollarOFTAdapter - version: LayerZero-OFT-adapter (generic) - source: - - https://docs.layerzero.network/ - - https://github.com/LayerZero-Labs/devtools - note: > - Flow: optional peers(dstEid) check -> approve underlying to minterBurner if - MessagingFee, refundAddress) paying nativeFee in msg.value when applicable. - SendParam mirrors LayerZero OFT: dstEid, to (bytes32 padded recipient), - amountLD, minAmountLD, extraOptions, composeMsg, oftCmd. - Verify tuple field order and OFT revision against your deployed artifact before mainnet use. - Event names and topic0 hashes vary by OFT package version — pull from the deployment ABI. - related: - - references/guides/bridge.md - -token: - mutability: view - inputs: [] - outputs: - - underlyingToken: address - -minterBurner: - notes: - - "Underlying G$ must approve this spender when burning for cross-chain send." - mutability: view - inputs: [] - outputs: - - minterBurner: address - -oftVersion: - mutability: view - inputs: [] - outputs: - - interfaceId: bytes4 - - version: uint64 - -peers: - notes: - - "Returns bytes32 peer address configured for destination endpoint id." - mutability: view - inputs: - - dstEid: uint32 - outputs: - - peer: bytes32 - -endpoint: - mutability: view - inputs: [] - outputs: - - lzEndpoint: address - -owner: - mutability: view - inputs: [] - outputs: - - account: address - -quoteSend: - notes: - - "Simulates messaging + bridge fee; use nativeFee as msg.value on send when paying in native gas token." - mutability: view - inputs: - - sendParam: SendParam - - payInLzToken: bool - outputs: - - nativeFee: uint256 - - lzTokenFee: uint256 - errors: - - NO_PEER - - LZ_INVALID_OPTIONS - -send: - notes: - - "Payable: include MessagingFee.nativeFee in msg.value when fee is native." - - "Consumes sendParam.amountLD from sender on source chain and emits cross-chain message to dstEid peer." - - "sendParam.to must be destination receiver encoded as bytes32." - mutability: payable - access: sender - inputs: - - sendParam: SendParam - - fee: MessagingFee - - refundAddress: address - outputs: - - msgReceipt: MessageReceipt - - oftReceipt: OFTReceipt - errors: - - NO_PEER - - SLIPPAGE_OR_AMOUNT - - LZ_INVALID_OPTIONS - -events: {} - -errors: - NO_PEER: "Destination peer not configured for dstEid." - LZ_INVALID_OPTIONS: "Composer/options payload rejected by LayerZero." - SLIPPAGE_OR_AMOUNT: "Bridged amount violates minAmount or available balance." diff --git a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml deleted file mode 100644 index e5615a1e..00000000 --- a/.agents/skills/gooddollar/references/contracts/GoodDollarOFTAdapter.selectors.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - token(): 0xfc0c546a - minterBurner(): 0x2ef8c5a4 - approvalRequired(): 0x9f68b964 - oftVersion(): 0x156a0d0f - peers(uint32): 0xbb0b6a53 - endpoint(): 0x5e280f11 - owner(): 0x8da5cb5b -events: -{} -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml deleted file mode 100644 index d736bc19..00000000 --- a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.abi.yaml +++ /dev/null @@ -1,210 +0,0 @@ -# GooddollarSavingsStream (Ubeswap) — G$ native Super Token stake with Superfluid GDA reward stream - -meta: - name: GooddollarSavingsStream - version: "1" - source: - - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/GooddollarSavingsStream.sol - - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/interfaces/IGooddollarSavingsStream.sol - - https://raw.githubusercontent.com/Ubeswap/gooddollar-contracts/main/contracts/StakingVault.sol - inherits: - - IGooddollarSavingsStream - - Ownable - - ERC2771Context - - ReentrancyGuard - note: > - Celo production savings. Staked principal sits in StakingVault; this contract holds reward - balance and streams via a Superfluid GDA distribution pool (`distributeFlow`). Effective rate is - min(rewardRate, maxRewardRatePerToken * totalSupply / 1e18), throttled when reward balance is low. - Staking token is the G$ native Super Token (`superToken`). Fuse->CELO migration uses `stakeFor` - after bridge. Ubeswap lists this as Celo mainnet savings (streaming). - deployments: - mainnet: - production-celo: - GooddollarSavingsStream: - networkId: 42220 - address: "0x059ee811414230d1Fb157878D2b491240F4D8d3B" - creationBlock: 66685884 - related: - - https://github.com/Ubeswap/gooddollar-contracts/blob/main/README.md - - https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B - - references/contracts/SuperToken.abi.yaml - - references/guides/migrate-fuse-staking-to-celo-savings.md - - references/deep-researches/fuse-to-celo-staking-migration.md - -totalSupply: - mutability: view - inputs: [] - outputs: - - supply: uint256 - -balanceOf: - mutability: view - inputs: - - account: address - outputs: - - balance: uint256 - -getDailyRewards: - mutability: view - inputs: [] - outputs: - - daily: uint256 - -getEffectiveFlowRate: - notes: - - "Returns 0 when totalSupply is 0, rewardRate is 0, or reward balance cannot sustain MIN_STREAM_BUFFER_SECONDS." - - "Otherwise min(global rewardRate, APR cap) as int96 for Superfluid distributeFlow." - mutability: view - inputs: [] - outputs: - - flowRate: int96 - -periodFinish: - notes: - - "Estimated timestamp when current reward balance is drained at getEffectiveFlowRate; 0 if not streaming." - mutability: view - inputs: [] - outputs: - - finish: uint256 - -getUnits: - mutability: view - inputs: - - account: address - outputs: - - units: uint128 - -getTotalUnits: - mutability: view - inputs: [] - outputs: - - units: uint128 - -stake: - notes: - - "Pulls G$ Super Token from msg.sender, deposits principal to StakingVault, updates GDA pool units, syncFlowRate." - - "Requires prior superToken.approve(this, amount)." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - outputs: [] - emits: [Staked] - -stakeFor: - notes: - - "Migrator path: caller funds transfer; stake credits `_balances[recipient]` and pool units for recipient." - - "recipient must not be zero, this, superToken, or vault." - - "Used after Fuse->CELO bridge when backend wallet holds bridged G$." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - - recipient: address - outputs: [] - emits: [Staked] - -withdraw: - notes: - - "Reduces stake and pool units, syncFlowRate, then vault.withdraw to msg.sender." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - outputs: [] - emits: [Withdrawn] - -exit: - notes: - - "Full withdraw of caller principal via withdraw(balance)." - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - emits: [Withdrawn] - -addToReward: - notes: - - "Pulls reward Super Token from caller; re-syncs flow rate when balance was throttling the stream." - mutability: nonpayable - access: anyone - inputs: - - reward: uint256 - outputs: [] - emits: [RewardAdded] - -syncFlowRate: - notes: - - "Permissionless; call when tokens were sent directly to the contract so distributeFlow picks up balance." - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - emits: [FlowRateUpdated] - -setDailyRewards: - mutability: nonpayable - access: owner - inputs: - - _dailyRewards: uint256 - outputs: [] - emits: [DailyRewardsUpdated] - -setMaxRewardRatePerToken: - mutability: nonpayable - access: owner - inputs: - - _value: uint256 - outputs: [] - emits: [MaxRewardRateUpdated] - -recoverERC20: - notes: - - "Cannot recover the G$ Super Token (staking/reward asset)." - mutability: nonpayable - access: owner - inputs: - - tokenAddress: address - - tokenAmount: uint256 - outputs: [] - emits: [Recovered] - -events: - Staked: - indexed: - - user: address - data: - - amount: uint256 - Withdrawn: - indexed: - - user: address - data: - - amount: uint256 - RewardAdded: - data: - - reward: uint256 - DailyRewardsUpdated: - data: - - rewardRate: uint256 - - givenDailyRewards: uint256 - MaxRewardRateUpdated: - data: - - newMaxRate: uint256 - FlowRateUpdated: - data: - - newFlowRate: int96 - Recovered: - indexed: - - token: address - data: - - amount: uint256 - - receiver: address - -errors: - - CannotStakeZero - - CannotWithdrawZero - - InsufficientStake - - InvalidAddress - - NoRewardToAdd - - CannotRecoverStakingToken diff --git a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml deleted file mode 100644 index f04d75a4..00000000 --- a/.agents/skills/gooddollar/references/contracts/GooddollarSavingsStream.selectors.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - totalSupply(): 0x18160ddd - balanceOf(address): 0x70a08231 - getDailyRewards(): 0x68527008 - getEffectiveFlowRate(): 0xa2efe857 - periodFinish(): 0xebe2b12b - getUnits(address): 0x0fefbc09 - getTotalUnits(): 0xa754a702 - stake(uint256): 0xa694fc3a - stakeFor(uint256,address): 0x51746bb2 - withdraw(uint256): 0x2e1a7d4d - exit(): 0xe9fad8ee - addToReward(uint256): 0x2ce618fa - syncFlowRate(): 0x0e83284a - setDailyRewards(uint256): 0x4adef718 - setMaxRewardRatePerToken(uint256): 0x6a03a9e2 - recoverERC20(address,uint256): 0x8980f11f -events: - Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d - Withdrawn(address,uint256): 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5 - RewardAdded(uint256): 0xde88a922e0d3b88b24e9623efeb464919c6bf9f66857a65e2bfcf2ce87a9433d - DailyRewardsUpdated(uint256,uint256): 0x54f5b7ef058007cebca11af0127cd80c0bac4e968788eabb3c1e70d1bfb78edd - MaxRewardRateUpdated(uint256): 0x9041b23d05af9bceefc73becffefd1887907ad55c3d6c6655230b37ef87dbb07 - FlowRateUpdated(int96): 0xd02fcf6b96acb60ac68942c7d5075e11a8a0b38cc865c1916934f4e1f129ded0 - Recovered(address,uint256,address): 0xb197f0a554c4d7840105e6ae65f0e275e9e8605a969dffa8caa7f1f118a2e1f5 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml deleted file mode 100644 index 5adccb32..00000000 --- a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.abi.yaml +++ /dev/null @@ -1,202 +0,0 @@ -# GovernanceStakingV2 (Fuse) — G$ staking share token with GDAO reward accrual -# Deployment key in GoodProtocol release metadata is GovernanceStakingV2. - -meta: - name: GovernanceStakingV2 - version: "2" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/governance/GovernanceStaking.sol - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/releases/deployment.json - inherits: - - ERC20Upgradeable - - MultiBaseGovernanceShareField - - DAOUpgradeableContract - - ReentrancyGuardUpgradeable - note: > - Fuse governance staking contract where users stake G$ and receive staking shares (`sG$`) with - GDAO-style rewards minted on reward withdrawal paths. `withdrawStake(0)` means full unstake. - In GoodProtocol `releases/deployment.json` this address is listed as GovernanceStakingV2 on Fuse `production`. - This is the old staking contract in Fuse->CELO migration flows. - deployments: - mainnet: - production: - GovernanceStakingV2: - networkId: 122 - address: "0xB7C3e738224625289C573c54d402E9Be46205546" - creationBlock: 15956809 - related: - - references/guides/migrate-fuse-staking-to-celo-savings.md - - references/deep-researches/fuse-to-celo-staking-migration.md - -getChainBlocksPerMonth: - mutability: pure - inputs: [] - outputs: - - blocks: uint256 - -stake: - notes: - - "Requires prior G$ ERC20 approval to staking contract." - - "Mints staking share token and updates reward productivity before event emission." - mutability: nonpayable - access: anyone - inputs: - - _amount: uint256 - outputs: [] - emits: [Staked] - -withdrawStake: - notes: - - "If _amount is 0, contract interprets it as full user unstake." - - "Burns staking shares, updates productivity, mints pending rewards, then transfers G$ out." - mutability: nonpayable - access: anyone - inputs: - - _amount: uint256 - outputs: [] - emits: [StakeWithdraw] - -withdrawRewards: - notes: - - "Claims rewards without changing staked principal." - mutability: nonpayable - access: anyone - inputs: [] - outputs: - - minted: uint256 - emits: [ReputationEarned] - -setMonthlyRewards: - notes: - - "DAO-controlled update of rewardsPerBlock schedule via avatar authorization." - mutability: nonpayable - access: avatar - inputs: - - _monthlyAmount: uint256 - outputs: [] - -getRewardsPerBlock: - mutability: view - inputs: [] - outputs: - - amount: uint256 - -getProductivity: - mutability: view - inputs: - - _user: address - outputs: - - productivity: uint256 - - rewardDebt: uint256 - -getUserPendingReward: - mutability: view - inputs: - - _user: address - outputs: - - pending: uint256 - -users: - notes: - - "UserInfo getter for internal staking productivity record." - mutability: view - inputs: - - _user: address - outputs: - - amount: uint256 - - rewardDebt: uint256 - -totalRewardsPerShare: - mutability: view - inputs: [] - outputs: - - value: uint256 - -decimals: - mutability: view - inputs: [] - outputs: - - d: uint8 - -totalSupply: - mutability: view - inputs: [] - outputs: - - supply: uint256 - -balanceOf: - mutability: view - inputs: - - account: address - outputs: - - balance: uint256 - -allowance: - mutability: view - inputs: - - owner: address - - spender: address - outputs: - - amount: uint256 - -approve: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - amount: uint256 - outputs: - - ok: bool - emits: [Approval] - -transfer: - mutability: nonpayable - access: anyone - inputs: - - to: address - - amount: uint256 - outputs: - - ok: bool - emits: [Transfer] - -transferFrom: - mutability: nonpayable - access: anyone - inputs: - - from: address - - to: address - - amount: uint256 - outputs: - - ok: bool - emits: [Transfer, Approval] - -events: - ReputationEarned: - indexed: - - staker: address - data: - - amount: uint256 - Staked: - indexed: - - staker: address - data: - - amount: uint256 - StakeWithdraw: - indexed: - - staker: address - data: - - amount: uint256 - Transfer: - indexed: - - from: address - - to: address - data: - - value: uint256 - Approval: - indexed: - - owner: address - - spender: address - data: - - value: uint256 - -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml deleted file mode 100644 index 37388ba0..00000000 --- a/.agents/skills/gooddollar/references/contracts/GovernanceStakingV2.selectors.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - getChainBlocksPerMonth(): 0x213b329e - stake(uint256): 0xa694fc3a - withdrawStake(uint256): 0x25d5971f - withdrawRewards(): 0xc7b8981c - setMonthlyRewards(uint256): 0xc76279a2 - getRewardsPerBlock(): 0x0c1cd7f3 - getProductivity(address): 0x28e964e9 - getUserPendingReward(address): 0xc6710629 - users(address): 0xa87430ba - totalRewardsPerShare(): 0xbf8e9b6e - decimals(): 0x313ce567 - totalSupply(): 0x18160ddd - balanceOf(address): 0x70a08231 - allowance(address,address): 0xdd62ed3e - approve(address,uint256): 0x095ea7b3 - transfer(address,uint256): 0xa9059cbb - transferFrom(address,address,uint256): 0x23b872dd -events: - ReputationEarned(address,uint256): 0x43848d0574703c28d68ae8958e0571521618f60c4bcacfb094cff2156eaae0f1 - Staked(address,uint256): 0x9e71bc8eea02a63969f509818f2dafb9254532904319f9dbda79b67bd34a5f3d - StakeWithdraw(address,uint256): 0x1248d48e2de900a1010c7fce73506969ecec243600bfc08b641b158f26d857cd - Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml deleted file mode 100644 index aea2efe9..00000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV3.abi.yaml +++ /dev/null @@ -1,390 +0,0 @@ -# IdentityV3 — whitelist, blacklist, DID, and connected-account graph for GoodDollar -# UBI eligibility uses getWhitelistedRoot; connectAccount links extra wallets to a root. - -meta: - name: IdentityV3 - version: v3 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV3.sol - inherits: - - DAOUpgradeableContract - - AccessControlUpgradeable - - PausableUpgradeable - - EIP712Upgradeable - note: > - isWhitelisted enforces authenticationPeriod against dateAuthenticated and status==1, - with fallback to oldIdentity. getWhitelistedRoot returns the root address for a - connected wallet or the wallet itself when directly whitelisted. - deployments: - mainnet: - production: - Identity: - networkId: 122 - address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" - creationBlock: 22022901 - IdentityOld: - networkId: 122 - address: "0xFa8d865A962ca8456dF331D78806152d3aC5B84F" - creationBlock: 6246324 - production-celo: - Identity: - networkId: 42220 - address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" - creationBlock: 17237952 - production-xdc: - Identity: - networkId: 50 - address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" - creationBlock: 95143058 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/identity - - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _owner: address - - _oldIdentity: address - outputs: [] - -initDAO: - notes: - - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." - mutability: nonpayable - access: DEFAULT_ADMIN_ROLE - inputs: - - _ns: address - outputs: [] - errors: [ALREADY_INITIALIZED] - -setAuthenticationPeriod: - mutability: nonpayable - access: avatar - inputs: - - period: uint256 - outputs: [] - errors: [WHEN_NOT_PAUSED_REVERT] - -authenticate: - notes: - - "Public wrapper; forwards to authenticateWithTimestamp(account, block.timestamp)." - mutability: nonpayable - inputs: - - account: address - outputs: [] - -authenticateWithTimestamp: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - timestamp: uint256 - outputs: [] - emits: [WhitelistedAuthenticated] - errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedAdded] - errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDIDAndChain: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - - orgChain: uint256 - - dateAuthenticated: uint256 - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDID: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -renounceWhitelisted: - mutability: nonpayable - access: whitelisted sender - inputs: [] - outputs: [] - emits: [WhitelistedRemoved] - errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] - -isWhitelisted: - notes: - - "Combines local Identity record with optional oldIdentity.isWhitelisted try/catch." - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -lastAuthenticated: - mutability: view - inputs: - - account: address - outputs: - - ts: uint256 - -addBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistAdded, WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -removeBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -addContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractAdded] - errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -isDAOContract: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -isBlacklisted: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -connectAccount: - notes: - - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." - mutability: nonpayable - access: whitelisted sender - inputs: - - account: address - outputs: [] - emits: [AccountConnected] - errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] - -disconnectAccount: - notes: - - "Caller must be either the linked root or the connected wallet." - mutability: nonpayable - inputs: - - connected: address - outputs: [] - emits: [AccountDisconnected] - errors: [UNAUTHORIZED_DISCONNECT] - -getWhitelistedRoot: - notes: - - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." - mutability: view - inputs: - - account: address - outputs: - - whitelisted: address - -pause: - mutability: nonpayable - access: PAUSER_ROLE - inputs: - - toPause: bool - outputs: [] - -setDID: - mutability: nonpayable - inputs: - - account: address - - did: string - outputs: [] - errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] - -addrToDID: - mutability: view - inputs: - - account: address - outputs: - - did: string - -getWhitelistedOnChainId: - mutability: view - inputs: - - account: address - outputs: - - chainId: uint256 - -isRegistered: - notes: - - "Compatibility shim; always returns true." - mutability: pure - inputs: [] - outputs: - - ok: bool - -IDENTITY_ADMIN_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -PAUSER_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -TYPED_STRUCTURE: - mutability: view - inputs: [] - outputs: - - schema: string - -whitelistedCount: - mutability: view - inputs: [] - outputs: - - n: uint256 - -whitelistedContracts: - mutability: view - inputs: [] - outputs: - - n: uint256 - -authenticationPeriod: - mutability: view - inputs: [] - outputs: - - seconds: uint256 - -identities: - mutability: view - inputs: - - account: address - outputs: - - dateAuthenticated: uint256 - - dateAdded: uint256 - - did: string - - whitelistedOnChainId: uint256 - - status: uint8 - -didHashToAddress: - mutability: view - inputs: - - hash: bytes32 - outputs: - - account: address - -connectedAccounts: - mutability: view - inputs: - - account: address - outputs: - - root: address - -oldIdentity: - mutability: view - inputs: [] - outputs: - - addr: address - -events: - BlacklistAdded: - indexed: - - account: address - data: [] - BlacklistRemoved: - indexed: - - account: address - data: [] - WhitelistedAdded: - indexed: - - account: address - data: [] - WhitelistedRemoved: - indexed: - - account: address - data: [] - WhitelistedAuthenticated: - indexed: - - account: address - data: - - timestamp: uint256 - ContractAdded: - indexed: - - account: address - data: [] - ContractRemoved: - indexed: - - account: address - data: [] - AccountConnected: - indexed: - - connected: address - - to: address - data: [] - AccountDisconnected: - indexed: - - disconnected: address - - from: address - data: [] - -errors: - ALREADY_INITIALIZED: "already initialized — initDAO twice." - NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." - NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." - NOT_WHITELISTED_FOR_DID: "not whitelisted — setDID internal." - ALREADY_HAS_STATUS: "already has status — _addWhitelisted." - DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." - DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." - NOT_CONTRACT: "Given address is not a contract — addContract." - INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." - ALREADY_CONNECTED: "already connected — connectAccount." - UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." - NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." - DID_EMPTY: "did empty — _setDID." - WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml deleted file mode 100644 index 9b53f001..00000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV3.selectors.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,address): 0x485cc955 - initDAO(address): 0x2b14dda8 - setAuthenticationPeriod(uint256): 0xfff930d4 - authenticate(address): 0x08e0d29d - authenticateWithTimestamp(address,uint256): 0x96a1ef79 - addWhitelisted(address): 0x10154bad - addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a - addWhitelistedWithDID(address,string): 0x1b027099 - removeWhitelisted(address): 0x291d9549 - renounceWhitelisted(): 0xd6cd9473 - isWhitelisted(address): 0x3af32abf - lastAuthenticated(address): 0xe1e360ba - addBlacklisted(address): 0x188efc16 - removeBlacklisted(address): 0xc6a276c2 - addContract(address): 0x5f539d69 - removeContract(address): 0xc375c2ef - isDAOContract(address): 0xc73cc4ae - isBlacklisted(address): 0xfe575a87 - connectAccount(address): 0xd21685f4 - disconnectAccount(address): 0x37a1a987 - getWhitelistedRoot(address): 0x2d0e9b46 - pause(bool): 0x02329a29 - setDID(address,string): 0xd3262816 - addrToDID(address): 0x54f9f7a3 - getWhitelistedOnChainId(address): 0xa061922d - isRegistered(): 0x22366844 - IDENTITY_ADMIN_ROLE(): 0x1aaff63c - PAUSER_ROLE(): 0xe63ab1e9 - TYPED_STRUCTURE(): 0x2cec5330 - whitelistedCount(): 0xb2a1de22 - whitelistedContracts(): 0xb30f7e7f - authenticationPeriod(): 0x31b376e2 - identities(address): 0xf653b81e - didHashToAddress(bytes32): 0x67c75937 - connectedAccounts(address): 0x61320040 - oldIdentity(): 0x4125f0f2 -events: - BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 - BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 - WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f - WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 - WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 - ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 - ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 - AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c - AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml deleted file mode 100644 index 485254d7..00000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV4.abi.yaml +++ /dev/null @@ -1,424 +0,0 @@ -# IdentityV4 — whitelist, blacklist, DID, connected accounts, and reverification schedule -# isWhitelisted uses shouldReverify against reverifyDaysOptions and authCount; oldIdentity fallback preserved. - -meta: - name: IdentityV4 - version: v4 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/identity/IdentityV4.sol - inherits: - - DAOUpgradeableContract - - AccessControlUpgradeable - - PausableUpgradeable - - EIP712Upgradeable - note: > - authenticationPeriod() returns the largest reverify interval in days (last entry of reverifyDaysOptions), - not seconds. isWhitelisted is false when shouldReverify is true for the current elapsed days since - dateAuthenticated. Post-upgrade, accounts with dateAuthenticated before 1772697574 are treated as - having authCount at the last schedule step for reverify logic. - deployments: - mainnet: - production: - Identity: - networkId: 122 - address: "0x2F9C28de9e6d44b71B91b8BA337A5D82e308E7BE" - creationBlock: 22022901 - production-celo: - Identity: - networkId: 42220 - address: "0xC361A6E67822a0EDc17D899227dd9FC50BD62F42" - creationBlock: 17237952 - production-xdc: - Identity: - networkId: 50 - address: "0x27a4a02C9ed591E1a86e2e5D05870292c34622C9" - creationBlock: 95143058 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/identity - - https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _owner: address - - _oldIdentity: address - outputs: [] - -initDAO: - notes: - - "Wires NameService via setDAO and transfers DEFAULT_ADMIN_ROLE, PAUSER_ROLE, IDENTITY_ADMIN_ROLE to avatar." - mutability: nonpayable - access: DEFAULT_ADMIN_ROLE - inputs: - - _ns: address - outputs: [] - errors: [ALREADY_INITIALIZED] - -setReverifyDaysOptions: - notes: - - "Replaces the full schedule; values must be strictly ascending uint8 days; non-empty array." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - options: uint8[] - outputs: [] - errors: [EMPTY_REVERIFY_OPTIONS, OPTIONS_NOT_ASCENDING, WHEN_NOT_PAUSED_REVERT] - -authenticate: - notes: - - "Forwards to authenticateWithTimestamp(account, block.timestamp); requires IDENTITY_ADMIN_ROLE on the inner call." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] - -authenticateWithTimestamp: - notes: - - "Refreshes authentication timestamp for status-1 account and advances authCount when reverification threshold is reached." - - "Legacy accounts before cutoff are evaluated at last schedule step for reverify math." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - timestamp: uint256 - outputs: [] - emits: [WhitelistedAuthenticated] - errors: [NOT_WHITELISTED_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedAdded] - errors: [ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDIDAndChain: - notes: - - "Adds whitelisted account and stores DID plus originating chain id and authentication timestamp." - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - - orgChain: uint256 - - dateAuthenticated: uint256 - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -addWhitelistedWithDID: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - - did: string - outputs: [] - emits: [WhitelistedAdded] - errors: [DID_ALREADY_REGISTERED, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeWhitelisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -renounceWhitelisted: - mutability: nonpayable - access: whitelisted sender - inputs: [] - outputs: [] - emits: [WhitelistedRemoved] - errors: [NOT_WHITELISTED_SENDER, WHEN_NOT_PAUSED_REVERT] - -shouldReverify: - notes: - - "Compares daysSinceAuth against current reverifyDaysOptions step selected by authCount." - mutability: view - inputs: - - account: address - - daysSinceAuth: uint256 - outputs: - - needed: bool - -isWhitelisted: - notes: - - "Local status 1 with shouldReverify false; else oldIdentity.isWhitelisted try/catch." - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -lastAuthenticated: - notes: - - "Returns local dateAuthenticated or falls back to oldIdentity when local record has no timestamp." - mutability: view - inputs: - - account: address - outputs: - - ts: uint256 - -addBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistAdded, WhitelistedRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -removeBlacklisted: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [BlacklistRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -addContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractAdded] - errors: [NOT_CONTRACT, ALREADY_HAS_STATUS, WHEN_NOT_PAUSED_REVERT] - -removeContract: - mutability: nonpayable - access: IDENTITY_ADMIN_ROLE - inputs: - - account: address - outputs: [] - emits: [ContractRemoved] - errors: [WHEN_NOT_PAUSED_REVERT] - -isDAOContract: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -isBlacklisted: - mutability: view - inputs: - - account: address - outputs: - - ok: bool - -connectAccount: - notes: - - "Caller must be whitelisted; target must not already be whitelisted or blacklisted; stores connectedAccounts[account]=msg.sender." - mutability: nonpayable - access: whitelisted sender - inputs: - - account: address - outputs: [] - emits: [AccountConnected] - errors: [INVALID_ACCOUNT_FOR_CONNECT, ALREADY_CONNECTED, WHEN_NOT_PAUSED_REVERT] - -disconnectAccount: - notes: - - "Caller must be either the linked root or the connected wallet." - mutability: nonpayable - inputs: - - connected: address - outputs: [] - emits: [AccountDisconnected] - errors: [UNAUTHORIZED_DISCONNECT] - -getWhitelistedRoot: - notes: - - "Returns account if directly whitelisted; returns connected root if connected account is whitelisted; else zero." - mutability: view - inputs: - - account: address - outputs: - - whitelisted: address - -pause: - mutability: nonpayable - access: PAUSER_ROLE - inputs: - - toPause: bool - outputs: [] - -setDID: - notes: - - "Sets DID for account by admin or account itself when whitelisted; enforces uniqueness across local and old identity." - mutability: nonpayable - inputs: - - account: address - - did: string - outputs: [] - errors: [NOT_AUTHORIZED_SET_DID, NOT_WHITELISTED_FOR_DID, DID_EMPTY, DID_ALREADY_REGISTERED, DID_ALREADY_REGISTERED_OLD, WHEN_NOT_PAUSED_REVERT] - -addrToDID: - mutability: view - inputs: - - account: address - outputs: - - did: string - -getWhitelistedOnChainId: - notes: - - "Returns chain id recorded during whitelisting for cross-chain eligibility checks." - mutability: view - inputs: - - account: address - outputs: - - chainId: uint256 - -isRegistered: - notes: - - "Compatibility shim; always returns true." - mutability: pure - inputs: [] - outputs: - - ok: bool - -IDENTITY_ADMIN_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -PAUSER_ROLE: - mutability: view - inputs: [] - outputs: - - role: bytes32 - -TYPED_STRUCTURE: - mutability: view - inputs: [] - outputs: - - schema: string - -whitelistedCount: - mutability: view - inputs: [] - outputs: - - n: uint256 - -whitelistedContracts: - mutability: view - inputs: [] - outputs: - - n: uint256 - -authenticationPeriod: - notes: - - "Returns reverifyDaysOptions[last] in days, not seconds." - mutability: view - inputs: [] - outputs: - - days: uint256 - -reverifyDaysOptions: - mutability: view - inputs: - - index: uint256 - outputs: - - days: uint32 - -identities: - mutability: view - inputs: - - account: address - outputs: - - dateAuthenticated: uint256 - - dateAdded: uint256 - - did: string - - whitelistedOnChainId: uint256 - - status: uint8 - - authCount: uint32 - -didHashToAddress: - mutability: view - inputs: - - hash: bytes32 - outputs: - - account: address - -connectedAccounts: - mutability: view - inputs: - - account: address - outputs: - - root: address - -oldIdentity: - mutability: view - inputs: [] - outputs: - - addr: address - -events: - BlacklistAdded: - indexed: - - account: address - data: [] - BlacklistRemoved: - indexed: - - account: address - data: [] - WhitelistedAdded: - indexed: - - account: address - data: [] - WhitelistedRemoved: - indexed: - - account: address - data: [] - WhitelistedAuthenticated: - indexed: - - account: address - data: - - timestamp: uint256 - ContractAdded: - indexed: - - account: address - data: [] - ContractRemoved: - indexed: - - account: address - data: [] - AccountConnected: - indexed: - - connected: address - - to: address - data: [] - AccountDisconnected: - indexed: - - disconnected: address - - from: address - data: [] - -errors: - ALREADY_INITIALIZED: "already initialized — initDAO twice." - EMPTY_REVERIFY_OPTIONS: "empty options — setReverifyDaysOptions." - OPTIONS_NOT_ASCENDING: "options not in ascending order — setReverifyDaysOptions." - NOT_WHITELISTED_STATUS: "not whitelisted — authenticateWithTimestamp." - NOT_WHITELISTED_SENDER: "not whitelisted — renounceWhitelisted modifier." - NOT_WHITELISTED_FOR_DID: "not whitelisted — _setDID." - ALREADY_HAS_STATUS: "already has status — _addWhitelisted." - DID_ALREADY_REGISTERED: "DID already registered — _addWhitelistedWithDID or _setDID." - DID_ALREADY_REGISTERED_OLD: "DID already registered oldIdentity — _setDID conflict path." - NOT_CONTRACT: "Given address is not a contract — addContract." - INVALID_ACCOUNT_FOR_CONNECT: "invalid account — connectAccount target checks." - ALREADY_CONNECTED: "already connected — connectAccount." - UNAUTHORIZED_DISCONNECT: "unauthorized — disconnectAccount." - NOT_AUTHORIZED_SET_DID: "not authorized — setDID caller." - DID_EMPTY: "did empty — _setDID." - WHEN_NOT_PAUSED_REVERT: "OpenZeppelin Pausable whenNotPaused on gated calls." diff --git a/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml b/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml deleted file mode 100644 index 3440a5ec..00000000 --- a/.agents/skills/gooddollar/references/contracts/IdentityV4.selectors.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,address): 0x485cc955 - initDAO(address): 0x2b14dda8 - setReverifyDaysOptions(uint8[]): 0x925e8c1a - authenticate(address): 0x08e0d29d - authenticateWithTimestamp(address,uint256): 0x96a1ef79 - addWhitelisted(address): 0x10154bad - addWhitelistedWithDIDAndChain(address,string,uint256,uint256): 0xe737031a - addWhitelistedWithDID(address,string): 0x1b027099 - removeWhitelisted(address): 0x291d9549 - renounceWhitelisted(): 0xd6cd9473 - shouldReverify(address,uint256): 0x4a03813f - isWhitelisted(address): 0x3af32abf - lastAuthenticated(address): 0xe1e360ba - addBlacklisted(address): 0x188efc16 - removeBlacklisted(address): 0xc6a276c2 - addContract(address): 0x5f539d69 - removeContract(address): 0xc375c2ef - isDAOContract(address): 0xc73cc4ae - isBlacklisted(address): 0xfe575a87 - connectAccount(address): 0xd21685f4 - disconnectAccount(address): 0x37a1a987 - getWhitelistedRoot(address): 0x2d0e9b46 - pause(bool): 0x02329a29 - setDID(address,string): 0xd3262816 - addrToDID(address): 0x54f9f7a3 - getWhitelistedOnChainId(address): 0xa061922d - isRegistered(): 0x22366844 - IDENTITY_ADMIN_ROLE(): 0x1aaff63c - PAUSER_ROLE(): 0xe63ab1e9 - TYPED_STRUCTURE(): 0x2cec5330 - whitelistedCount(): 0xb2a1de22 - whitelistedContracts(): 0xb30f7e7f - authenticationPeriod(): 0x31b376e2 - reverifyDaysOptions(uint256): 0xcadef5ac - identities(address): 0xf653b81e - didHashToAddress(bytes32): 0x67c75937 - connectedAccounts(address): 0x61320040 - oldIdentity(): 0x4125f0f2 -events: - BlacklistAdded(address): 0x44d5fe68b00f68950fb9c1ff0a61ef7f747b1a36359a7e3a7f3324db4b878967 - BlacklistRemoved(address): 0x1747ca720b1a174a464b6513ace29b1d3190b5f632b9f34147017c81425bfde8 - WhitelistedAdded(address): 0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f - WhitelistedRemoved(address): 0x270d9b30cf5b0793bbfd54c9d5b94aeb49462b8148399000265144a8722da6b6 - WhitelistedAuthenticated(address,uint256): 0xb2a82fce6d8c7a633efe9579f77b4edb96bfdf171a49bfc2ce666dc543a1f500 - ContractAdded(address): 0x89c66952b48f3e96bf1d8ba1b63189520fd988a6979b8b740bd5c5d8dc53e205 - ContractRemoved(address): 0x8d30d41865a0b811b9545d879520d2dde9f4cc49e4241f486ad9752bc904b565 - AccountConnected(address,address): 0x18f7736ef54539debd9afd3c9500b106e12ae7c70e685f5a5efd727b1ce1d54c - AccountDisconnected(address,address): 0x7cdef5f9c5cb8ce728661ede956fef26cb91eb4d7d2180cc041b73f9fef568d2 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml deleted file mode 100644 index d69e3d60..00000000 --- a/.agents/skills/gooddollar/references/contracts/InvitesV2.abi.yaml +++ /dev/null @@ -1,333 +0,0 @@ -# InvitesV2 — invite codes, inviter levels, and G$ bounties (UUPS upgradeable) -# Bounty amounts on Level use G$ cents (2 decimals). Invitee receives half the inviter bounty on payout. - -meta: - name: InvitesV2 - version: "2.4" - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/invite/InvitesV2.sol - inherits: - - DAOUpgradeableContract - note: > - join binds invite code and optional inviter; bounty eligibility uses minimumDays, minimumClaims, - whitelist on invitee (and inviter when present), and same-chain whitelisting via Identity - getWhitelistedOnChainId. collectBounties and bountyFor pay invitee bountyToPay/2 and inviter - bountyToPay when applicable. Campaign inviter is address(this) when inviter code maps to contract. - deployments: - mainnet: - production: - Invites: - networkId: 122 - address: "0xCa2F09c3ccFD7aD5cB9276918Bd1868f2b922ea0" - creationBlock: 8853311 - production-celo: - Invites: - networkId: 42220 - address: "0x36829D1Cda92FFF5782d5d48991620664FC857d3" - creationBlock: 18003063 - production-xdc: - Invites: - networkId: 50 - address: "0x6bd698566632bf2e81e2278f1656CB24aAF06D2e" - creationBlock: 95144756 - related: - - references/deep-researches/inviter-invitee-reward-model.md - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _ns: address - - _level0Bounty: uint256 - - _owner: address - outputs: [] - -updateAvatar: - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - -nativeToken: - mutability: view - inputs: [] - outputs: - - token: address - -dao: - mutability: view - inputs: [] - outputs: - - controller: address - -avatar: - mutability: view - inputs: [] - outputs: - - addr: address - -nameService: - mutability: view - inputs: [] - outputs: - - ns: address - -upgradeTo: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - newImplementation: address - outputs: [] - -upgradeToAndCall: - mutability: payable - access: ownerOrAvatar - inputs: - - newImplementation: address - - data: bytes - outputs: [] - -proxiableUUID: - mutability: view - inputs: [] - outputs: - - slot: bytes32 - -setLevelExpirationEnabled: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _isEnabled: bool - outputs: [] - -getIdentity: - mutability: view - inputs: [] - outputs: - - identity: address - -join: - notes: - - "Registers invite code; may set inviter; may trigger _bountyFor when canCollectBountyFor is already true." - mutability: nonpayable - access: anyone - inputs: - - _myCode: bytes32 - - _inviterCode: bytes32 - outputs: [] - emits: [InviteeJoined] - errors: [NOT_ACTIVE, INVITE_CODE_IN_USE, SELF_INVITE, USER_ALREADY_JOINED] - -canCollectBountyFor: - notes: - - "Primary eligibility gate: checks active state, whitelist, minimumDays, minimumClaims, unpaid bounty, and chain constraints." - mutability: view - inputs: - - _invitee: address - outputs: - - ok: bool - -getInvitees: - mutability: view - inputs: - - _inviter: address - outputs: - - invitees: address[] - -getPendingInvitees: - mutability: view - inputs: - - _inviter: address - outputs: - - pending: address[] - -getPendingBounties: - mutability: view - inputs: - - _inviter: address - outputs: - - count: uint256 - -bountyFor: - notes: - - "Single-invitee payout path; revalidates eligibility and transfers bounty shares to inviter/invitee on success." - mutability: nonpayable - access: anyone - inputs: - - _invitee: address - outputs: - - bounty: uint256 - emits: [InviterBounty] - errors: [NOT_ACTIVE, NOT_ELIGIBLE_BOUNTY] - -collectBounties: - notes: - - "Batch payout path over caller pending invitees; useful for claiming multiple matured bounties in one tx." - mutability: nonpayable - access: anyone - inputs: [] - outputs: [] - emits: [InviterBounty] - errors: [NOT_ACTIVE] - -setLevel: - notes: - - "Configures inviter level progression rule and bounty amount (bounty units are G$ cents)." - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _lvl: uint256 - - _toNext: uint256 - - _bounty: uint256 - - _daysToComplete: uint256 - outputs: [] - -setActive: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _active: bool - outputs: [] - -setCampaignCode: - notes: - - "Maps a campaign code to contract-address inviter flow (address(this)) used by join." - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _code: bytes32 - outputs: [] - -setMinimums: - notes: - - "Sets minimumClaims and minimumDays thresholds used by canCollectBountyFor." - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _minClaims: uint8 - - _minDays: uint8 - outputs: [] - -end: - mutability: nonpayable - access: ownerOrAvatar - inputs: [] - outputs: [] - errors: [NOT_ACTIVE] - -setOwner: - mutability: nonpayable - access: ownerOrAvatar - inputs: - - _owner: address - outputs: [] - -version: - mutability: pure - inputs: [] - outputs: - - v: string - -codeToUser: - mutability: view - inputs: - - code: bytes32 - outputs: - - user: address - -users: - notes: - - "Public getter omits dynamic invitees/pending arrays; use getInvitees and getPendingInvitees." - mutability: view - inputs: - - user: address - outputs: - - invitedBy: address - - inviteCode: bytes32 - - bountyPaid: bool - - level: uint256 - - levelStarted: uint256 - - totalApprovedInvites: uint256 - - totalEarned: uint256 - - joinedAt: uint256 - - bountyAtJoin: uint256 - -levels: - notes: - - "bounty is in G$ cents (2 decimals). Reserved struct slots are not exposed on the getter." - mutability: view - inputs: - - lvl: uint256 - outputs: - - toNext: uint256 - - bounty: uint256 - - daysToComplete: uint256 - -owner: - mutability: view - inputs: [] - outputs: - - addr: address - -goodDollar: - mutability: view - inputs: [] - outputs: - - token: address - -active: - mutability: view - inputs: [] - outputs: - - ok: bool - -stats: - notes: - - "Reserved Stats struct slots are not exposed on the getter." - mutability: view - inputs: [] - outputs: - - totalApprovedInvites: uint256 - - totalBountiesPaid: uint256 - - totalInvited: uint256 - -levelExpirationEnabled: - mutability: view - inputs: [] - outputs: - - ok: bool - -minimumClaims: - mutability: view - inputs: [] - outputs: - - n: uint8 - -minimumDays: - mutability: view - inputs: [] - outputs: - - n: uint8 - -events: - InviteeJoined: - indexed: - - inviter: address - - invitee: address - data: [] - InviterBounty: - indexed: - - inviter: address - - invitee: address - data: - - bountyPaid: uint256 - - inviterLevel: uint256 - - earnedLevel: bool - -errors: - NOT_ACTIVE: "not active — isActive modifier." - ONLY_OWNER_OR_AVATAR: "Only owner or avatar can perform this action — ownerOrAvatar." - INVITE_CODE_IN_USE: "invite code already in use — join." - SELF_INVITE: "self invite — join." - USER_ALREADY_JOINED: "user already joined — join." - NOT_ELIGIBLE_BOUNTY: "user not elligble for bounty yet — bountyFor." diff --git a/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml deleted file mode 100644 index d7179550..00000000 --- a/.agents/skills/gooddollar/references/contracts/InvitesV2.selectors.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,uint256,address): 0xc350a1b5 - updateAvatar(): 0x1b3c90a8 - nativeToken(): 0xe1758bd8 - dao(): 0x4162169f - avatar(): 0x5aef7de6 - nameService(): 0x3e6326fc - upgradeTo(address): 0x3659cfe6 - upgradeToAndCall(address,bytes): 0x4f1ef286 - proxiableUUID(): 0x52d1902d - setLevelExpirationEnabled(bool): 0x21132aad - getIdentity(): 0x36afc6fa - join(bytes32,bytes32): 0x5b419a65 - canCollectBountyFor(address): 0x6d619ef8 - getInvitees(address): 0xe9881a5e - getPendingInvitees(address): 0xe951a3aa - getPendingBounties(address): 0x41155d5e - bountyFor(address): 0xb6567cd5 - collectBounties(): 0xaf6346b0 - setLevel(uint256,uint256,uint256,uint256): 0xb9fb2d18 - setActive(bool): 0xacec338a - setCampaignCode(bytes32): 0xf14b8649 - setMinimums(uint8,uint8): 0x47826c82 - end(): 0xefbe1c1c - setOwner(address): 0x13af4035 - version(): 0x54fd4d50 - codeToUser(bytes32): 0xba6f5680 - users(address): 0xa87430ba - levels(uint256): 0xb2596a67 - owner(): 0x8da5cb5b - goodDollar(): 0x119e5bf3 - active(): 0x02fb0c5e - stats(): 0xd80528ae - levelExpirationEnabled(): 0xa1df6fd3 - minimumClaims(): 0xc121cb92 - minimumDays(): 0xdf0dca04 -events: - InviteeJoined(address,address): 0xd8c638d8979e2ba5dba1f0d66246ee4b1c54b838f0e0a2b601365345eb23b051 - InviterBounty(address,address,uint256,uint256,bool): 0x6081787cd1bd02ab1576c52f03e8710d792d460e7881c3155d77d23893f3768b -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml deleted file mode 100644 index f6f47f46..00000000 --- a/.agents/skills/gooddollar/references/contracts/MentoBroker.abi.yaml +++ /dev/null @@ -1,259 +0,0 @@ -# Mento Broker (IBroker + IBrokerAdmin) — swap router and trading-limit gate for GoodDollar Mento rails -# Interface definitions are vendored in GoodProtocol; implementation lives in mento-core. - -meta: - name: Broker - version: IBroker / IBrokerAdmin - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/MentoInterfaces.sol - - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/swap/Broker.sol - - https://raw.githubusercontent.com/mento-org/mento-core/main/contracts/interfaces/IBroker.sol - implements: [IBroker, IBrokerAdmin] - note: > - swapIn is exact-input; swapOut is exact-output. Quotes route through the - configured exchangeProvider + exchangeId. Reverts use require strings in - Broker.sol plus TradingLimits library checks ("amountOutMin not met", - "amountInMax exceeded", trading-limit errors). - deployments: - mainnet: - production-celo: - MentoBroker: - networkId: 42220 - address: "0x88de45906D4F5a57315c133620cfa484cB297541" - creationBlock: 31415857 - production-xdc: - MentoBroker: - networkId: 50 - address: "0x88de45906D4F5a57315c133620cfa484cB297541" - creationBlock: 100091095 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/mentobroker - - references/guides/swap.md - -initialize: - mutability: nonpayable - access: initializer - inputs: - - _exchangeProviders: address[] - - _reserves: address[] - outputs: [] - -setReserves: - notes: - - "Updates reserve address per already-listed provider without changing provider registration." - mutability: nonpayable - access: owner - inputs: - - _exchangeProviders: address[] - - _reserves: address[] - outputs: [] - emits: [ReserveSet] - errors: - - EXCHANGE_PROVIDER_MISSING - - RESERVE_ZERO - -addExchangeProvider: - mutability: nonpayable - access: owner - inputs: - - exchangeProvider: address - - reserve: address - outputs: - - index: uint256 - emits: [ExchangeProviderAdded, ReserveSet] - errors: - - PROVIDER_ALREADY_LISTED - - PROVIDER_ZERO - - RESERVE_ZERO - -removeExchangeProvider: - mutability: nonpayable - access: owner - inputs: - - exchangeProvider: address - - index: uint256 - outputs: [] - emits: [ExchangeProviderRemoved] - errors: - - INDEX_MISMATCH - -getAmountIn: - notes: - - "Exact-output quote path: returns required tokenIn for target amountOut." - - "Pre-checks collateral reserve balance when tokenOut is collateral." - mutability: view - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountOut: uint256 - outputs: - - amountIn: uint256 - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - -getAmountOut: - notes: - - "Exact-input quote path: returns expected tokenOut for fixed amountIn." - - "Pre-checks collateral reserve balance when tokenOut is collateral." - mutability: view - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountIn: uint256 - outputs: - - amountOut: uint256 - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - -swapIn: - notes: - - "Transfers tokenIn from msg.sender then delivers tokenOut; enforces amountOut >= amountOutMin after exchange + limits." - mutability: nonpayable - access: nonReentrant - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountIn: uint256 - - amountOutMin: uint256 - outputs: - - amountOut: uint256 - emits: [Swap] - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - - AMOUNT_OUT_MIN_NOT_MET - - TRADING_LIMIT_OR_SAFE_TRANSFER - -swapOut: - notes: - - "Computes required tokenIn then pulls up to amountInMax; reverts if amountIn would exceed cap." - mutability: nonpayable - access: nonReentrant - inputs: - - exchangeProvider: address - - exchangeId: bytes32 - - tokenIn: address - - tokenOut: address - - amountOut: uint256 - - amountInMax: uint256 - outputs: - - amountIn: uint256 - emits: [Swap] - errors: - - EXCHANGE_PROVIDER_MISSING - - INSUFFICIENT_RESERVE_BALANCE - - AMOUNT_IN_MAX_EXCEEDED - - TRADING_LIMIT_OR_SAFE_TRANSFER - -burnStableTokens: - notes: - - "Owner-maintained escape hatch burning stables from msg.sender via reserve plumbing." - mutability: nonpayable - access: owner - inputs: - - token: address - - amount: uint256 - outputs: - - ok: bool - errors: - - COLLATERAL_TRANSFER_FAILED - -configureTradingLimit: - notes: - - "Sets per (exchangeId, token) limit config used by swapIn/swapOut guardTradingLimits checks." - mutability: nonpayable - access: owner - inputs: - - exchangeId: bytes32 - - token: address - - config: TradingLimits.Config - outputs: [] - emits: [TradingLimitConfigured] - -getExchangeProviders: - notes: - - "Discovery helper for clients that need the active provider set before quoting/swapping." - mutability: view - inputs: [] - outputs: - - providers: address[] - -exchangeProviders: - mutability: view - inputs: - - index: uint256 - outputs: - - exchangeProvider: address - -isExchangeProvider: - mutability: view - inputs: - - exchangeProvider: address - outputs: - - ok: bool - -tradingLimitsState: - mutability: view - inputs: - - eid: bytes32 - outputs: - - state: TradingLimits.State - -tradingLimitsConfig: - mutability: view - inputs: - - eid: bytes32 - outputs: - - config: TradingLimits.Config - -events: - Swap: - indexed: - - exchangeId: bytes32 - - trader: address - - tokenIn: address - data: - - exchangeProvider: address - - tokenOut: address - - amountIn: uint256 - - amountOut: uint256 - TradingLimitConfigured: - indexed: [] - data: - - exchangeId: bytes32 - - token: address - - config: TradingLimits.Config - ExchangeProviderAdded: - indexed: - - exchangeProvider: address - data: [] - ExchangeProviderRemoved: - indexed: - - exchangeProvider: address - data: [] - ReserveSet: - indexed: - - newAddress: address - - prevAddress: address - data: [] - -errors: - EXCHANGE_PROVIDER_MISSING: "ExchangeProvider does not exist" - EXCHANGE_PROVIDER_EXISTS: "ExchangeProvider already exists in the list" - PROVIDER_ZERO: "ExchangeProvider address can't be 0" - RESERVE_ZERO: "Reserve address can't be 0" - INDEX_MISMATCH: "index doesn't match provider" - INSUFFICIENT_RESERVE_BALANCE: "Insufficient balance in reserve" - AMOUNT_OUT_MIN_NOT_MET: "amountOutMin not met" - AMOUNT_IN_MAX_EXCEEDED: "amountInMax exceeded" - COLLATERAL_TRANSFER_FAILED: "Transfer of the collateral asset failed" - AMOUNT_TOO_LARGE: "amountIn too large / amountOut too large — uint256 to int256 safety checks" - TRADING_LIMIT_OR_SAFE_TRANSFER: "TradingLimits library reverts or ERC20 safety failures during swap" diff --git a/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml deleted file mode 100644 index 249edce4..00000000 --- a/.agents/skills/gooddollar/references/contracts/MentoBroker.selectors.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address[],address[]): 0x73cf25f8 - setReserves(address[],address[]): 0xddeb9dd2 - addExchangeProvider(address,address): 0x770d0a34 - removeExchangeProvider(address,uint256): 0x04710d53 - getAmountIn(address,bytes32,address,address,uint256): 0x04e45640 - getAmountOut(address,bytes32,address,address,uint256): 0xa20f2305 - swapIn(address,bytes32,address,address,uint256,uint256): 0xddbbe850 - swapOut(address,bytes32,address,address,uint256,uint256): 0xd163b135 - burnStableTokens(address,uint256): 0x131cab2a - configureTradingLimit(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0xa868d140 - getExchangeProviders(): 0x2cac2568 - exchangeProviders(uint256): 0xc4454fdc - isExchangeProvider(address): 0xd1d786b1 - tradingLimitsState(bytes32): 0xf01ecd17 - tradingLimitsConfig(bytes32): 0x821a816c -events: - Swap(bytes32,address,address,address,address,uint256,uint256): 0x46e6aeaebfb8f1b6a9be6403b5fc420d9827172046f365e786acb9d5b56c9409 - TradingLimitConfigured(bytes32,address,(uint32,uint32,int48,int48,int48,int48,int48,uint8)): 0x3ed50aa123e1e547aa90fcf88624ef8278d253e252bb1feac75eda93e1905bb6 - ExchangeProviderAdded(address): 0x2ee2cb0721ec60b86190cae5c48e25064b69b35abad32452a4ec99d232033de2 - ExchangeProviderRemoved(address): 0x29e92ab2e30f4f74283034c28c451b6faac986b554f1808101eb6418bdba19d4 - ReserveSet(address,address): 0xb69e1c416d8be92ac92c8e97e77c4626fba5e6ab50161099f659ea3303479e50 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml deleted file mode 100644 index d4bb1f2d..00000000 --- a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.abi.yaml +++ /dev/null @@ -1,542 +0,0 @@ -meta: - name: MessagePassingBridge - version: Axelar + LayerZero message passing - source: - - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/MessagePassingBridge.sol - - https://raw.githubusercontent.com/GoodDollar/GoodBridge/master/packages/bridge-contracts/contracts/messagePassingBridge/IMessagePassingBridge.sol - note: > - Bridge implementation that burns source tokens and mints destination tokens, - transporting requests through Axelar or LayerZero. - Cross-chain transport uses msg.value on the source (LayerZero estimateSendFee path, Axelar gas prepay); destination mint applies bridgeFees bps to the delivered G$ amount. See references/guides/bridge.md "Bridge fee context". - deployments: - mainnet: - production: - MpbBridge: - networkId: 122 - address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" - creationBlock: 25464921 - production-celo: - MpbBridge: - networkId: 42220 - address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" - creationBlock: 21473545 - production-xdc: - MpbBridge: - networkId: 50 - address: "0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5" - creationBlock: 95254417 - related: - - references/guides/bridge.md - -initialize: - notes: - - "Initial bootstrap for DAO wiring and bridge policy state." - - "Sets guardian to msg.sender and uses NameService UBISCHEME as feeRecipient fallback to avatar when missing." - - "Preloads default LZ mappings for Ethereum, Celo, Fuse, and XDC using this contract as trusted remote." - mutability: nonpayable - access: initializer - inputs: - - nameService: address - - limits: (uint256,uint256,uint256,uint256,bool) - - fees: (uint256,uint256,uint256) - outputs: [] - -upgrade: - notes: - - "Reinitializer for migration path; re-applies default LZ mappings through internal add function." - - "Used when upgrading existing proxy deployments to mapping-aware version." - mutability: nonpayable - access: reinitializer - inputs: [] - outputs: [] - -addLzChainSupport: - notes: - - "Administrative chain wiring method for LayerZero transport." - - "Writes both forward (chainId->lzChainId) and reverse (lzChainId->chainId) mappings." - - "Also updates trustedRemoteLookup to enforce source-contract authenticity on receive." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - lzChainId: uint16 - - chainId: uint256 - - remote: address - outputs: [] - -approveRequest: - notes: - - "Manual override for stuck/exceptional inbound requests." - - "When approved, destination _bridgeFrom skips limits enforcement for that request id." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - id: uint256 - outputs: [] - -preventRequest: - notes: - - "Emergency/manual block for a specific request id." - - "Prevents execution by pre-marking executedRequests[id]=true." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - id: uint256 - outputs: [] - -setFeeRecipient: - notes: - - "Sets destination fee mint recipient." - - "If feeRecipient is zero, fee minting is skipped in _bridgeFrom." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - recipient: address - outputs: [] - -setBridgeLimits: - notes: - - "Updates operational throttles: min amount, tx cap, account/day cap, global/day cap, whitelist mode." - - "These limits are checked by canBridge and enforced in _enforceLimits." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - limits: (uint256,uint256,uint256,uint256,bool) - outputs: [] - -setBridgeFees: - notes: - - "Updates bridge fee parameters in basis points with min/max clamps." - - "Reverts when fee exceeds 10000 bps." - - "Effective fee deduction is computed in _takeFee during destination execution." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - fees: (uint256,uint256,uint256) - outputs: [] - -setDisabledBridges: - notes: - - "Toggles allowlist state for source bridge combinations." - - "Key format is keccak256(abi.encode(sourceChainId, BridgeService))." - - "Checked in _bridgeFrom before request execution." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - bridgeKeys: bytes32[] - - disabled: bool[] - outputs: [] - -setFaucet: - notes: - - "Configures optional faucet top-up callback used after successful destination processing." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - _faucet: address - outputs: [] - -setGuardian: - notes: - - "Rotates guardian authority used by owner/avatar/guardian admin gate." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - _guardian: address - outputs: [] - -withdraw: - notes: - - "Avatar-only rescue function for arbitrary token balances held by bridge." - - "Amount 0 means full balance withdrawal." - mutability: nonpayable - access: avatar - inputs: - - token: address - - amount: uint256 - outputs: [] - -pauseBridge: - notes: - - "Sets isClosed state for outbound flow." - - "When closed, _bridgeTo reverts with BRIDGE_LIMITS('closed')." - mutability: nonpayable - access: ownerOrAvatarOrGuardian - inputs: - - isPaused: bool - outputs: [] - -canBridge: - notes: - - "Purely diagnostic read path for preflight checks." - - "Returns (ok, reason) using BridgeHelperLibrary policy evaluation." - - "Includes close flag, whitelist gate, and per-account/global daily limit windows." - mutability: view - inputs: - - from: address - - amount: uint256 - outputs: - - isWithinLimit: bool - - error: string - -bridgeTo: - notes: - - "Generic outbound bridge entrypoint selecting BridgeService.AXELAR or BridgeService.LZ." - - "Burns source-side token amount, normalizes to 18 decimals, and emits BridgeRequest with request id." - - "Requires msg.value for transport fee payment." - mutability: payable - access: anyone - inputs: - - target: address - - targetChainId: uint256 - - amount: uint256 - - bridge: uint8 - outputs: [] - -bridgeToWithLz: - notes: - - "Outbound convenience wrapper fixed to LayerZero transport." - - "If adapterParams is empty, contract injects default type-1 options with 400k gas." - - "Compares required native fee from estimateSendFee against msg.value and reverts on underpayment." - mutability: payable - access: anyone - inputs: - - target: address - - targetChainId: uint256 - - amount: uint256 - - adapterParams: bytes - outputs: [] - -bridgeToWithAxelar: - notes: - - "Outbound convenience wrapper fixed to Axelar transport." - - "Uses gasRefundAddress when provided, otherwise msg.sender as refund recipient." - mutability: payable - access: anyone - inputs: - - target: address - - targetChainId: uint256 - - amount: uint256 - - gasRefundAddress: address - outputs: [] - -toLzChainId: - notes: - - "Lookup helper from local chain id to LayerZero chain id." - mutability: view - inputs: - - chainId: uint256 - outputs: - - lzChainId: uint16 - -fromLzChainId: - notes: - - "Lookup helper from LayerZero chain id to local chain id." - mutability: view - inputs: - - lzChainId: uint16 - outputs: - - chainId: uint256 - -toAxelarChainId: - notes: - - "Pure mapping in current implementation: chainId 1 -> 'Ethereum', 5 -> 'ethereum-2', 42220 and 44787 -> 'celo'; any other id returns empty string and outbound Axelar reverts UNSUPPORTED_CHAIN." - mutability: pure - inputs: - - chainId: uint256 - outputs: - - axlChainId: string - -fromAxelarChainId: - notes: - - "Maps Axelar chain-name strings back into local numeric chain ids." - mutability: pure - inputs: - - axlChainId: string - outputs: - - chainId: uint256 - -estimateSendFee: - notes: - - "Transport fee preflight for LZ path." - - "Builds payload with request id set to 0 for estimation." - - "Returns (0,0) on endpoint estimation failure in handler implementation." - - "_normalizedAmount must match BridgeHelperLibrary.normalizeFromTokenTo18Decimals burned raw amount using native token decimals on the outbound chain." - mutability: view - inputs: - - _dstChainId: uint16 - - _fromAddress: address - - _toAddress: address - - _normalizedAmount: uint256 - - _useZro: bool - - _adapterParams: bytes - outputs: - - nativeFee: uint256 - - zroFee: uint256 - -lzReceive: - notes: - - "LayerZero receive entrypoint." - - "Validates caller is configured endpoint before dispatching nonblocking receive pipeline." - mutability: nonpayable - inputs: - - _srcChainId: uint16 - - _srcAddress: bytes - - _nonce: uint64 - - _payload: bytes - outputs: [] - -lzEndpoint_: - notes: - - "Immutable LayerZero endpoint configured in constructor." - mutability: view - inputs: [] - outputs: - - endpoint: address - -HOME_CHAIN_ID: - notes: - - "Immutable deployment home chain marker." - mutability: view - inputs: [] - outputs: - - chainId: uint32 - -guardian: - notes: - - "Current guardian account authorized for admin operations." - mutability: view - inputs: [] - outputs: - - account: address - -executedRequests: - notes: - - "Replay protection state for inbound request ids." - mutability: view - inputs: - - id: uint256 - outputs: - - done: bool - -disabledSourceBridges: - notes: - - "Per-source bridge toggle state keyed by (sourceChainId, bridgeService) hash." - mutability: view - inputs: - - key: bytes32 - outputs: - - disabled: bool - -isClosed: - notes: - - "Outbound bridge pause state." - mutability: view - inputs: [] - outputs: - - closed: bool - -bridgeFees: - notes: - - "Current fee policy parameters: minFee, maxFee, feeBps." - mutability: view - inputs: [] - outputs: - - minFee: uint256 - - maxFee: uint256 - - fee: uint256 - -bridgeLimits: - notes: - - "Current bridge traffic limits and whitelist policy." - mutability: view - inputs: [] - outputs: - - dailyLimit: uint256 - - txLimit: uint256 - - accountDailyLimit: uint256 - - minAmount: uint256 - - onlyWhitelisted: bool - -bridgeDailyLimit: - notes: - - "Aggregate 24h bridge usage tracker." - mutability: view - inputs: [] - outputs: - - lastTransferReset: uint256 - - bridged24Hours: uint256 - -accountsDailyLimit: - notes: - - "Per-account 24h usage tracker." - mutability: view - inputs: - - account: address - outputs: - - lastTransferReset: uint256 - - bridged24Hours: uint256 - -faucet: - notes: - - "Optional gas top-up contract used on destination completion." - mutability: view - inputs: [] - outputs: - - addr: address - -currentId: - notes: - - "Monotonic nonce component used to derive outbound request ids." - mutability: view - inputs: [] - outputs: - - id: uint256 - -lzChainIdsMapping: - notes: - - "Local chain id -> LayerZero chain id mapping." - mutability: view - inputs: - - chainId: uint256 - outputs: - - lzChainId: uint16 - -feeRecipient: - notes: - - "Address receiving bridge fee mints on destination." - mutability: view - inputs: [] - outputs: - - recipient: address - -approvedRequests: - notes: - - "Manual approvals allowing request execution above standard limits." - mutability: view - inputs: - - id: uint256 - outputs: - - approved: bool - -lzChainToIdsMapping: - notes: - - "LayerZero chain id -> local chain id mapping." - mutability: view - inputs: - - lzChainId: uint16 - outputs: - - chainId: uint256 - -trustedRemoteLookup: - notes: - - "Trusted remote path used to authenticate inbound LZ/Axelar source contract." - mutability: view - inputs: - - lzChainId: uint16 - outputs: - - path: bytes - -gateway: - notes: - - "Axelar gateway address inherited from AxelarExecutable." - mutability: view - inputs: [] - outputs: - - addr: address - -gasService: - notes: - - "Axelar gas service used to prepay cross-chain execution." - mutability: view - inputs: [] - outputs: - - addr: address - -nameService: - notes: - - "DAO NameService pointer from DAOUpgradeableContract." - mutability: view - inputs: [] - outputs: - - addr: address - -avatar: - notes: - - "DAO avatar authority address." - mutability: view - inputs: [] - outputs: - - addr: address - -owner: - notes: - - "Ownable owner from upgradeable ownership flow." - mutability: view - inputs: [] - outputs: - - account: address - -events: - BridgeRequest: - indexed: - - from: address - - to: address - - id: uint256 - data: - - targetChainId: uint256 - - normalizedAmount: uint256 - - timestamp: uint256 - - bridge: uint8 - note: "Outbound intent event; relayers consume this to deliver payload on destination." - ExecutedTransfer: - indexed: - - from: address - - to: address - - id: uint256 - data: - - normalizedAmount: uint256 - - fee: uint256 - - sourceChainId: uint256 - - bridge: uint8 - note: "Destination completion event after replay checks, source-auth checks, limit checks, and mint/fee mint." - FalseSender: - indexed: [] - data: - - sourceChainId: uint256 - - sourceAddress: address - note: "Inbound message ignored because source contract does not match trusted remote path." - -errors: - AlreadyInitialized: "LZ chain support already initialized for this lz chain id." - NOT_GUARDIAN: - inputs: - - sender: address - WRONG_TOKEN: - inputs: - - token: address - INVALID_TARGET_OR_CHAINID: - inputs: - - target: address - - chainId: uint256 - BRIDGE_LIMITS: - inputs: - - reason: string - TRANSFER_FROM: "Source burn/transferFrom style failure." - TRANSFER: "Destination transfer style failure." - ALREADY_EXECUTED: - inputs: - - requestId: uint256 - MISSING_FEE: "Bridge call requires msg.value for transport fee." - UNSUPPORTED_CHAIN: - inputs: - - chainId: uint256 - LZ_FEE: - inputs: - - required: uint256 - - sent: uint256 - INVALID_SENDER: - inputs: - - _srcAddress: bytes - INVALID_ENDPOINT: - inputs: - - lzEndpoint: address diff --git a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml b/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml deleted file mode 100644 index 104b9435..00000000 --- a/.agents/skills/gooddollar/references/contracts/MessagePassingBridge.selectors.yaml +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - initialize(address,(uint256,uint256,uint256,uint256,bool),(uint256,uint256,uint256)): 0xfad16abe - upgrade(): 0xd55ec697 - addLzChainSupport(uint16,uint256,address): 0xc01c8123 - approveRequest(uint256): 0xd7d1bbdb - preventRequest(uint256): 0xc612f9ea - setFeeRecipient(address): 0xe74b981b - setBridgeLimits((uint256,uint256,uint256,uint256,bool)): 0x29b70872 - setBridgeFees((uint256,uint256,uint256)): 0x82dc737d - setDisabledBridges(bytes32[],bool[]): 0xc78ab882 - setFaucet(address): 0xd8b31c77 - setGuardian(address): 0x8a0dac4a - withdraw(address,uint256): 0xf3fef3a3 - pauseBridge(bool): 0x1a394795 - canBridge(address,uint256): 0x3095634a - bridgeTo(address,uint256,uint256,uint8): 0x1fec5c5c - bridgeToWithLz(address,uint256,uint256,bytes): 0xc56bbdd9 - bridgeToWithAxelar(address,uint256,uint256,address): 0x40a00aaf - toLzChainId(uint256): 0x5b23a990 - fromLzChainId(uint16): 0x16ad5512 - toAxelarChainId(uint256): 0x9a9ee081 - fromAxelarChainId(string): 0x56026f37 - estimateSendFee(uint16,address,address,uint256,bool,bytes): 0x05fead15 - lzReceive(uint16,bytes,uint64,bytes): 0x001d3567 - lzEndpoint_(): 0x020051cf - HOME_CHAIN_ID(): 0x8f65be85 - guardian(): 0x452a9320 - executedRequests(uint256): 0x425cfb53 - disabledSourceBridges(bytes32): 0x69c20d36 - isClosed(): 0xc2b6b58c - bridgeFees(): 0x7b0240c0 - bridgeLimits(): 0xc6dd812f - bridgeDailyLimit(): 0xb2f7667f - accountsDailyLimit(address): 0xd4227947 - faucet(): 0xde5f72fd - currentId(): 0xe00dd161 - lzChainIdsMapping(uint256): 0xb5569f18 - feeRecipient(): 0x46904840 - approvedRequests(uint256): 0x0dce292e - lzChainToIdsMapping(uint16): 0x73800fc4 - trustedRemoteLookup(uint16): 0x7533d788 - gateway(): 0x116191b6 - gasService(): 0x6a22d8cc - nameService(): 0x3e6326fc - avatar(): 0x5aef7de6 - owner(): 0x8da5cb5b -events: - BridgeRequest(address,address,uint256,uint256,uint256,uint256,uint8): 0x34b675c8c84b6a9e7979a0d3a54f2b036a19d6444167091a925af2d81e8e66fe - ExecutedTransfer(address,address,uint256,uint256,uint256,uint256,uint8): 0x81e772e0c4366ddbae472926005267ef278dbb257be45dfe97c676ceae348dbe - FalseSender(uint256,address): 0x1eafb58197ea0dc76b9278ccad47f61a239f00b017d89a19081a89321cce213d -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml b/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml deleted file mode 100644 index 01692b7a..00000000 --- a/.agents/skills/gooddollar/references/contracts/NameService.abi.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# NameService — string-key to address registry for GoodProtocol deployments -# Resolves keys such as IDENTITY, UBISCHEME, GOODDOLLAR, GDAO_STAKING. - -meta: - name: NameService - version: UUPS-upgradeable v1 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/utils/NameService.sol - implements: [] - inherits: [Initializable, UUPSUpgradeable] - note: > - Almost every integration starts here. Writes are restricted to the DAO - avatar. Reads are permissionless. Use getAddress(string) with the same - key strings your deployment documented in releases/deployment.json. - deployments: - mainnet: - production: - NameService: - networkId: 122 - address: "0xec6dcE387B1616a0c44fF2E4fA9E90e53Cf14eb0" - creationBlock: 15740314 - production-celo: - NameService: - networkId: 42220 - address: "0x0F5dB7a64A6a64052693676CA898EC7F7A94FF4e" - creationBlock: 17237962 - production-xdc: - NameService: - networkId: 50 - address: "0x1e5154Bf5e31FF56051bbd45958b879Fb7a290FE" - creationBlock: 95143608 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/nameservice - - https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json - -addresses: - # Solidity auto-getter for public mapping addresses(bytes32 => address). - notes: - - "Prefer getAddress(string) off-chain; this expects keccak256(abi.encodePacked(name)) for manual lookups." - mutability: view - inputs: - - nameHash: bytes32 - outputs: - - addr: address - -dao: - mutability: view - inputs: [] - outputs: - - controller: address - -initialize: - notes: - - "Initializer wires Controller and optional pre-hashed name batch." - - "Automatically registers CONTROLLER and AVATAR hashes to dao and dao.avatar()." - mutability: nonpayable - access: initializer - inputs: - - _dao: address - - _nameHashes: bytes32[] - - _addresses: address[] - outputs: [] - -setAddress: - notes: - - "Stores addresses[keccak256(bytes(name))] = addr and emits AddressChanged." - mutability: nonpayable - access: avatar - inputs: - - name: string - - addr: address - outputs: [] - emits: [AddressChanged] - errors: [ONLY_AVATAR] - -setAddresses: - notes: - - "Batch write by raw bytes32 keys; no per-name event emission in this loop." - mutability: nonpayable - access: avatar - inputs: - - hash: bytes32[] - - addrs: address[] - outputs: [] - errors: [ONLY_AVATAR] - -getAddress: - notes: - - "Primary lookup used by claim, staking, identity, and swap integrations." - mutability: view - inputs: - - name: string - outputs: - - addr: address - -events: - AddressChanged: - indexed: [] - data: - - name: string - - addr: address - -errors: - ONLY_AVATAR: "only avatar can call this method — revert string on any avatar-gated path including UUPS _authorizeUpgrade." diff --git a/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml b/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml deleted file mode 100644 index 48e1bcfa..00000000 --- a/.agents/skills/gooddollar/references/contracts/NameService.selectors.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - addresses(bytes32): 0x699f200f - dao(): 0x4162169f - initialize(address,bytes32[],address[]): 0xd41b4e72 - setAddress(string,address): 0x9b2ea4bd - setAddresses(bytes32[],address[]): 0x4ab01f5b - getAddress(string): 0xbf40fac1 -events: - AddressChanged(string,address): 0x135cf55549d8538a41f19f46cc85625da93e68b63484cca8fcb9aaf19e520137 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml deleted file mode 100644 index abfc91cb..00000000 --- a/.agents/skills/gooddollar/references/contracts/SuperToken.abi.yaml +++ /dev/null @@ -1,1033 +0,0 @@ -# SuperToken — the Superfluid Protocol's token standard -# Merges three variants into a single ABI surface: -# • Wrapper Super Token — wraps an existing ERC-20 (upgrade / downgrade) -# • Native Asset Super Token (SETH) — wraps chain-native asset (upgradeByETH / downgradeToETH) -# • Pure Super Token — pre-minted, no underlying token (upgrade/downgrade revert) -# -# Inherits ERC-20, ERC-777, ERC-2612 (permit), EIP-5267, and the Superfluid real-time -# balance and agreement hosting layer from SuperfluidToken. -# -# Proxy / upgradability functions are omitted: -# castrate, getCodeAddress, proxiableUUID, initialize, initializeWithAdmin, updateCode -# Proxy-related events are also omitted: CodeUpdated, Initialized. - -meta: - name: SuperToken - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SuperfluidToken.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/IYieldBackend.sol - inherits: [SuperfluidToken, ISuperToken, UUPSProxiable] - yield_backends: - - name: AaveYieldBackend - note: "Production backend for ERC-20 wrapper tokens using Aave V3" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveYieldBackend.sol - - name: AaveETHYieldBackend - note: "Variant for native-asset (ETH/WETH) wrapper tokens using Aave V3" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/AaveETHYieldBackend.sol - - name: ERC4626YieldBackend - note: "Generic backend for any ERC-4626 vault" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/ERC4626YieldBackend.sol - - name: SparkYieldBackend - note: "Extends ERC4626YieldBackend for Spark Protocol vaults with referral tracking" - source: https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/SparkYieldBackend.sol - variants: - wrapper: "Standard ERC-20 wrapper — upgrade() / downgrade()" - native-asset: "Native asset wrapper (SETH) — upgradeByETH() / downgradeToETH()" - pure: "Pre-minted supply, no underlying — upgrade/downgrade revert" - note: > - Super Tokens are deployed as individual proxies (one per token) by the - SuperTokenFactory. There is no single canonical deployment address. - All proxies share the same logic contract which is upgraded via governance. - -# wrapper super token — a Super Token backed by an ERC-20 underlying token -# native asset super token (SETH) — a Super Token backed by the chain-native asset (ETH, MATIC, etc.) -# pure super token — a Super Token with no underlying; supply is pre-minted at deploy time -# underlying token — the ERC-20 token that a wrapper super token wraps -# real-time balance — balance that changes continuously based on active agreements (CFA/GDA) -# available balance — real-time balance minus locked deposits -# critical — an account whose available balance is negative -# solvent — an account whose real-time balance (before deposit deductions) is non-negative -# operator (ERC-777) — an address authorized to send/burn tokens on behalf of a holder -# buffer / deposit — collateral locked while an outgoing flow is active - -# == ERC-20 Token Info == - -name: - mutability: view - inputs: [] - outputs: - - string - -symbol: - mutability: view - inputs: [] - outputs: - - string - -decimals: - # Always returns 18 regardless of underlying token decimals. - notes: - - "Always returns 18, regardless of the underlying token's decimals (e.g. USDC has 6). The upgrade/downgrade functions handle decimal conversion internally." - mutability: pure - inputs: [] - outputs: - - uint8 - -totalSupply: - mutability: view - inputs: [] - outputs: - - uint256 - -# == ERC-20 Balance & Allowance == - -balanceOf: - # Returns max(0, availableBalance) — clamps negative balances to zero. - # For the full real-time picture, use realtimeBalanceOfNow. - notes: - - "Returns max(0, availableBalance) — negative/critical balances are clamped to zero. Use realtimeBalanceOfNow to detect critical accounts." - mutability: view - inputs: - - account: address - outputs: - - balance: uint256 - -allowance: - mutability: view - inputs: - - account: address - - spender: address - outputs: - - uint256 - -approve: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - amount: uint256 - outputs: - - bool - emits: [Approval] - errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -increaseAllowance: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - addedValue: uint256 - outputs: - - bool - emits: [Approval] - errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -decreaseAllowance: - mutability: nonpayable - access: anyone - inputs: - - spender: address - - subtractedValue: uint256 - outputs: - - bool - emits: [Approval] - errors: [SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -# == ERC-20 Transfers == - -transfer: - # ERC-20 transfer. Does NOT invoke ERC-777 send/receive hooks (by design). - mutability: nonpayable - access: anyone - inputs: - - recipient: address - - amount: uint256 - outputs: - - bool - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -transferFrom: - # ERC-20 transferFrom. Does NOT invoke ERC-777 send/receive hooks. - # Does NOT emit an Approval event on allowance spend (OZ v5 behaviour). - mutability: nonpayable - access: anyone - inputs: - - holder: address - - recipient: address - - amount: uint256 - outputs: - - bool - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -transferAll: - # Transfer the caller's entire balanceOf to recipient. - mutability: nonpayable - access: anyone # transfers msg.sender's entire balance - inputs: - - recipient: address - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -# == ERC-777 == - -granularity: - # Always returns 1. - mutability: pure - inputs: [] - outputs: - - uint256 - -send: - # ERC-777 send — invokes tokensToSend / tokensReceived hooks. - mutability: nonpayable - access: anyone - inputs: - - recipient: address - - amount: uint256 - - userData: bytes - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] - -burn: - # ERC-777 burn — in SuperToken this actually performs a downgrade (unwrap). - notes: - - "Gotcha: Reverts with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure Super Tokens." - mutability: nonpayable - access: anyone - inputs: - - amount: uint256 - - userData: bytes - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] - -authorizeOperator: - mutability: nonpayable - access: anyone - inputs: - - operator: address - emits: [AuthorizedOperator] - -revokeOperator: - mutability: nonpayable - access: anyone - inputs: - - operator: address - emits: [RevokedOperator] - -isOperatorFor: - mutability: view - inputs: - - operator: address - - tokenHolder: address - outputs: - - bool - -defaultOperators: - mutability: view - inputs: [] - outputs: - - "address[]" - -operatorSend: - # ERC-777 operatorSend — invokes tokensToSend / tokensReceived hooks. - mutability: nonpayable - access: operator - inputs: - - sender: address - - recipient: address - - amount: uint256 - - userData: bytes - - operatorData: bytes - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] - -operatorBurn: - # ERC-777 operatorBurn — like burn(), actually performs a downgrade. - mutability: nonpayable - access: operator - inputs: - - account: address - - amount: uint256 - - userData: bytes - - operatorData: bytes - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED] - -# == ERC-20 Permit == - -permit: - mutability: nonpayable - access: anyone - inputs: - - owner: address - - spender: address - - value: uint256 - - deadline: uint256 - - v: uint8 - - r: bytes32 - - s: bytes32 - emits: [Approval] - errors: [SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE, SUPER_TOKEN_PERMIT_INVALID_SIGNER, ECDSAInvalidSignature, ECDSAInvalidSignatureLength, ECDSAInvalidSignatureS, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -nonces: - mutability: view - inputs: - - owner: address - outputs: - - uint256 - -DOMAIN_SEPARATOR: - mutability: view - inputs: [] - outputs: - - bytes32 - -eip712Domain: - # EIP-5267 domain retrieval. - mutability: view - inputs: [] - outputs: - - fields: bytes1 - - name: string - - version: string - - chainId: uint256 - - verifyingContract: address - - salt: bytes32 - - "extensions: uint256[]" - -# == ERC-20 Wrapping == -# These functions interact with the underlying ERC-20 token. -# They revert with SUPER_TOKEN_NO_UNDERLYING_TOKEN on Pure and Native Asset -# Super Tokens (use upgradeByETH / downgradeToETH for SETH). - -upgrade: - # Wrap underlying ERC-20 into Super Tokens for msg.sender. - # Requires prior ERC-20 approval on the underlying token. - notes: - - "Gotcha: amount is always in SuperToken decimals (18), regardless of the underlying token's decimals. The contract handles downscaling internally when pulling from the underlying via transferFrom. However, the ERC-20 approval on the underlying token must use the underlying's native decimals. Example: to wrap 1000 USDC, approve 1000e6 on USDC, then call upgrade(1000e18)." - mutability: nonpayable - access: anyone # wraps for msg.sender - inputs: - - amount: uint256 # in SuperToken decimals (always 18) - emits: [Minted, Transfer, TokenUpgraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -upgradeTo: - # Wrap underlying ERC-20 and mint Super Tokens to a different address. - mutability: nonpayable - access: anyone - inputs: - - to: address - - amount: uint256 - - userData: bytes - emits: [Minted, Transfer, TokenUpgraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -downgrade: - # Unwrap Super Tokens back to underlying ERC-20 for msg.sender. - mutability: nonpayable - access: anyone # unwraps to msg.sender - inputs: - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -downgradeTo: - # Unwrap Super Tokens and send underlying ERC-20 to a different address. - mutability: nonpayable - access: anyone - inputs: - - to: address - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -getUnderlyingToken: - # Returns address(0) for Pure and Native Asset Super Tokens. - mutability: view - inputs: [] - outputs: - - address - -getUnderlyingDecimals: - mutability: view - inputs: [] - outputs: - - uint8 - -toUnderlyingAmount: - # Convert a Super Token amount to its underlying equivalent, accounting for - # decimal differences. adjustedAmount strips precision loss. - mutability: view - inputs: - - amount: uint256 - outputs: - - underlyingAmount: uint256 - - adjustedAmount: uint256 - -# == Native Asset Wrapping (SETH only) == -# These functions exist only on Native Asset Super Token (SETH) proxies. -# They are defined in the SETHProxy contract, not in the SuperToken logic. -# The SETH proxy also accepts plain ETH transfers via receive() which mints -# Super Tokens to the sender. - -upgradeByETH: - # Wrap sent ETH/native asset into Super Tokens for msg.sender. - # Variant: native-asset - mutability: payable - access: anyone # wraps for msg.sender - inputs: [] - emits: [Minted, Transfer, TokenUpgraded] - -upgradeByETHTo: - # Wrap sent ETH/native asset into Super Tokens for a different address. - # Variant: native-asset - mutability: payable - access: anyone - inputs: - - to: address - emits: [Minted, Transfer, TokenUpgraded] - -downgradeToETH: - # Unwrap Super Tokens back to ETH/native asset for msg.sender. - # Variant: native-asset - mutability: nonpayable - access: anyone # unwraps to msg.sender - inputs: - - wad: uint256 - emits: [Burned, Transfer, TokenDowngraded] - -# == Real-time Balance == - -realtimeBalanceOf: - # Full real-time balance across ALL agreements at a given timestamp. - # This aggregates the realtimeBalanceOf from every agreement (CFA, GDA, IDA) - # into a single balance. Use this (or realtimeBalanceOfNow) to check an - # account's actual balance — not the per-agreement versions. - # availableBalance = settled + agreement dynamics − max(0, deposit − owedDeposit) - mutability: view - inputs: - - account: address - - timestamp: uint256 - outputs: - - availableBalance: int256 - - deposit: uint256 - - owedDeposit: uint256 - -realtimeBalanceOfNow: - # Convenience wrapper: realtimeBalanceOf at the current Host timestamp. - # This is the recommended way to check an account's real-time balance. - mutability: view - inputs: - - account: address - outputs: - - availableBalance: int256 - - deposit: uint256 - - owedDeposit: uint256 - - timestamp: uint256 - -isAccountCritical: - # True if availableBalance < 0 at the given timestamp. - mutability: view - inputs: - - account: address - - timestamp: uint256 - outputs: - - isCritical: bool - -isAccountCriticalNow: - mutability: view - inputs: - - account: address - outputs: - - isCritical: bool - -isAccountSolvent: - # True if realtime balance (before deposit deduction) >= 0 at the given timestamp. - mutability: view - inputs: - - account: address - - timestamp: uint256 - outputs: - - isSolvent: bool - -isAccountSolventNow: - mutability: view - inputs: - - account: address - outputs: - - isSolvent: bool - -# == Admin == -# The admin defaults to the Host contract when address(0). -# An explicit admin can be set during initializeWithAdmin or via changeAdmin. - -changeAdmin: - mutability: nonpayable - access: admin - inputs: - - newAdmin: address - emits: [AdminChanged] - errors: [SUPER_TOKEN_ONLY_ADMIN] - -getAdmin: - # Returns address(0) if the Host is the implicit admin. - mutability: view - inputs: [] - outputs: - - address - -# == Yield Backend == -# Hot-pluggable yield generation for wrapper Super Tokens. The yield backend is a -# contract implementing IYieldBackend that is invoked via delegatecall. When enabled, -# the underlying ERC-20 is deposited into a yield protocol (e.g. Aave, Spark, ERC4626 vaults). -# Surplus yield (balance exceeding totalSupply) can be withdrawn by the admin. - -enableYieldBackend: - # Enable a yield backend and deposit all underlying token balance into it. - # Requires no existing yield backend to be set. - notes: - - "Gotcha: Reverts if a yield backend is already enabled — must call disableYieldBackend first to switch backends." - - "The yield backend contract is invoked via delegatecall — it executes in the SuperToken's context." - mutability: nonpayable - access: admin - inputs: - - newYieldBackend: address # must implement IYieldBackend - emits: [YieldBackendEnabled] - -disableYieldBackend: - # Withdraw all assets from the yield backend and disable it. - notes: - - "Calls withdrawMax() then disable() on the backend via delegatecall." - mutability: nonpayable - access: admin - inputs: [] - emits: [YieldBackendDisabled] - -getYieldBackend: - # Returns the current yield backend address, or address(0) if none is set. - mutability: view - inputs: [] - outputs: - - yieldBackend: address - -withdrawSurplusFromYieldBackend: - # Withdraw yield surplus (deposited amount exceeding totalSupply) from the backend. - # The surplus is sent to a receiver defined by the yield backend implementation. - mutability: nonpayable - access: admin - inputs: [] - -# == Self Operations == -# Can only be called by the token contract itself (address(this)). -# Used by custom super token proxies (SETH, Pure) to mint/burn/approve/transfer -# tokens through their own proxy logic. - -selfMint: - mutability: nonpayable - access: self - inputs: - - account: address - - amount: uint256 - - userData: bytes - emits: [Minted, Transfer] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS] - -selfBurn: - mutability: nonpayable - access: self - inputs: - - account: address - - amount: uint256 - - userData: bytes - emits: [Burned, Transfer] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE] - -selfApproveFor: - mutability: nonpayable - access: self - inputs: - - account: address - - spender: address - - amount: uint256 - emits: [Approval] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -selfTransferFrom: - mutability: nonpayable - access: self - inputs: - - holder: address - - spender: address - - recipient: address - - amount: uint256 - emits: [Sent, Transfer] - errors: [SUPER_TOKEN_ONLY_SELF, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -# == Host Batch Operations == -# Called by the Host contract on behalf of a user within batchCall / -# forwardBatchCall. Each mirrors a user-facing function but takes the -# account address explicitly. - -operationApprove: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - amount: uint256 - emits: [Approval] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -operationIncreaseAllowance: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - addedValue: uint256 - emits: [Approval] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -operationDecreaseAllowance: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - subtractedValue: uint256 - emits: [Approval] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS, SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS] - -operationTransferFrom: - mutability: nonpayable - access: host - inputs: - - account: address - - spender: address - - recipient: address - - amount: uint256 - emits: [Sent, Transfer] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE] - -operationSend: - mutability: nonpayable - access: host - inputs: - - spender: address - - recipient: address - - amount: uint256 - - userData: bytes - emits: [Sent, Transfer] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS, SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS, SF_TOKEN_MOVE_INSUFFICIENT_BALANCE, SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT] - -operationUpgrade: - mutability: nonpayable - access: host - inputs: - - account: address - - amount: uint256 - emits: [Minted, Transfer, TokenUpgraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -operationDowngrade: - mutability: nonpayable - access: host - inputs: - - account: address - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -operationUpgradeTo: - mutability: nonpayable - access: host - inputs: - - account: address - - to: address - - amount: uint256 - emits: [Minted, Transfer, TokenUpgraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SUPER_TOKEN_MINT_TO_ZERO_ADDRESS, SafeERC20FailedOperation, SafeCastOverflowedUintToInt] - -operationDowngradeTo: - mutability: nonpayable - access: host - inputs: - - account: address - - to: address - - amount: uint256 - emits: [Burned, Transfer, TokenDowngraded] - errors: [SF_TOKEN_ONLY_HOST, SUPER_TOKEN_NO_UNDERLYING_TOKEN, SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS, SF_TOKEN_BURN_INSUFFICIENT_BALANCE, SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED, SafeERC20FailedOperation] - -# == Agreement Hosting == -# The token acts as storage host for agreement data. These functions are -# called by registered agreement contracts (CFA, GDA, IDA) through the Host. - -createAgreement: - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - "data: bytes32[]" - emits: [AgreementCreated] - errors: [SF_TOKEN_AGREEMENT_ALREADY_EXISTS] - -getAgreementData: - mutability: view - inputs: - - agreementClass: address - - id: bytes32 - - dataLength: uint256 - outputs: - - "data: bytes32[]" - -updateAgreementData: - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - "data: bytes32[]" - emits: [AgreementUpdated] - -terminateAgreement: - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - dataLength: uint256 - emits: [AgreementTerminated] - errors: [SF_TOKEN_AGREEMENT_DOES_NOT_EXIST] - -updateAgreementStateSlot: - mutability: nonpayable - access: agreement - inputs: - - account: address - - slotId: uint256 - - "slotData: bytes32[]" - emits: [AgreementStateUpdated] - -getAgreementStateSlot: - mutability: view - inputs: - - agreementClass: address - - account: address - - slotId: uint256 - - dataLength: uint256 - outputs: - - "slotData: bytes32[]" - -settleBalance: - # Adjust an account's settled balance. Only listed agreements can call this. - mutability: nonpayable - access: agreement - inputs: - - account: address - - delta: int256 - errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] - -makeLiquidationPayoutsV2: - # Execute liquidation payouts. Transfers reward to liquidator or reward account, - # and adjusts the target account's balance. Only listed agreements can call this. - mutability: nonpayable - access: agreement - inputs: - - id: bytes32 - - liquidationTypeData: bytes - - liquidatorAccount: address - - useDefaultRewardAccount: bool - - targetAccount: address - - rewardAmount: uint256 - - targetAccountBalanceDelta: int256 - emits: [Transfer, AgreementLiquidatedV2] - errors: [SF_TOKEN_ONLY_LISTED_AGREEMENT] - -getAccountActiveAgreements: - mutability: view - inputs: - - account: address - outputs: - - "ISuperAgreement[]" - -# == Protocol Info == - -getHost: - mutability: view - inputs: [] - outputs: - - host: address - -POOL_ADMIN_NFT: - # Immutable address of the canonical PoolAdminNFT proxy. - mutability: view - inputs: [] - outputs: - - address - -POOL_MEMBER_NFT: - # Immutable address of the (deprecated) PoolMemberNFT proxy. - mutability: view - inputs: [] - outputs: - - address - -VERSION: - # Returns the version string of the SuperToken logic contract (e.g. "1.0.0"). - mutability: view - inputs: [] - outputs: - - string - -# == Events == - -events: - - # -- ERC-20 -- - - Transfer: - indexed: - - from: address - - to: address - data: - - value: uint256 - - Approval: - indexed: - - owner: address - - spender: address - data: - - value: uint256 - - # -- ERC-777 -- - - Sent: - indexed: - - operator: address - - from: address - - to: address - data: - - amount: uint256 - - data: bytes - - operatorData: bytes - - Minted: - indexed: - - operator: address - - to: address - data: - - amount: uint256 - - data: bytes - - operatorData: bytes - - Burned: - indexed: - - operator: address - - from: address - data: - - amount: uint256 - - data: bytes - - operatorData: bytes - - AuthorizedOperator: - indexed: - - operator: address - - tokenHolder: address - - RevokedOperator: - indexed: - - operator: address - - tokenHolder: address - - # -- EIP-5267 -- - - EIP712DomainChanged: - # Signalled when the EIP-712 domain changes. Not explicitly emitted by - # SuperToken, but declared in the EIP-5267 interface for indexing support. - - # -- Wrapping -- - - TokenUpgraded: - indexed: - - account: address - data: - - amount: uint256 - - TokenDowngraded: - indexed: - - account: address - data: - - amount: uint256 - - # -- Admin -- - - AdminChanged: - indexed: [] - data: - - oldAdmin: address - - newAdmin: address - - # -- Yield Backend -- - - YieldBackendEnabled: - indexed: - - yieldBackend: address - data: - - depositAmount: uint256 - - YieldBackendDisabled: - indexed: - - yieldBackend: address - - PoolAdminNFTCreated: - # Emitted once during logic contract constructor deployment. - indexed: [] - data: - - poolAdminNFT: address - - # -- Agreement Hosting -- - - AgreementCreated: - indexed: - - agreementClass: address - - id: bytes32 - data: - - "data: bytes32[]" - - AgreementUpdated: - indexed: - - agreementClass: address - - id: bytes32 - data: - - "data: bytes32[]" - - AgreementTerminated: - indexed: - - agreementClass: address - - id: bytes32 - - AgreementStateUpdated: - indexed: - - agreementClass: address - - account: address - data: - - slotId: uint256 - - AgreementLiquidatedV2: - indexed: - - agreementClass: address - - id: bytes32 - data: - - liquidatorAccount: address - - targetAccount: address - - rewardAmountReceiver: address - - rewardAmount: uint256 - - targetAccountBalanceDelta: int256 - - liquidationTypeData: bytes - - # -- Deprecated events (from ISuperfluidToken, no longer emitted) -- - - AgreementLiquidated: - deprecated: true - note: "Use AgreementLiquidatedV2" - indexed: - - agreementClass: address - - id: bytes32 - data: - - penaltyAccount: address - - rewardAccount: address - - rewardAmount: uint256 - - AgreementLiquidatedBy: - deprecated: true - note: "Use AgreementLiquidatedV2" - indexed: - - liquidatorAccount: address - - agreementClass: address - - id: bytes32 - data: - - penaltyAccount: address - - bondAccount: address - - rewardAmount: uint256 - - bailoutAmount: uint256 - - Bailout: - deprecated: true - note: "Use AgreementLiquidatedV2" - indexed: - - bailoutAccount: address - - bailoutAmount: uint256 - -# == Errors == - -errors: - - # -- Super Token access -- - - SUPER_TOKEN_ONLY_SELF - - SUPER_TOKEN_ONLY_ADMIN - - SUPER_TOKEN_ONLY_GOV_OWNER - - # -- SuperfluidToken access -- - - SF_TOKEN_ONLY_HOST - - SF_TOKEN_ONLY_LISTED_AGREEMENT - - # -- Wrapping -- - - SUPER_TOKEN_NO_UNDERLYING_TOKEN - - SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED - - # -- ERC-20 / ERC-777 transfer -- - - SUPER_TOKEN_TRANSFER_FROM_ZERO_ADDRESS - - SUPER_TOKEN_TRANSFER_TO_ZERO_ADDRESS - - SF_TOKEN_MOVE_INSUFFICIENT_BALANCE - - # -- Approve -- - - SUPER_TOKEN_APPROVE_FROM_ZERO_ADDRESS - - SUPER_TOKEN_APPROVE_TO_ZERO_ADDRESS - - # -- Mint / Burn -- - - SUPER_TOKEN_MINT_TO_ZERO_ADDRESS - - SUPER_TOKEN_BURN_FROM_ZERO_ADDRESS - - SF_TOKEN_BURN_INSUFFICIENT_BALANCE - - # -- ERC-777 operator -- - - SUPER_TOKEN_CALLER_IS_NOT_OPERATOR_FOR_HOLDER - - SUPER_TOKEN_NOT_ERC777_TOKENS_RECIPIENT - - # -- ERC-2612 permit -- - - SUPER_TOKEN_PERMIT_EXPIRED_SIGNATURE: - inputs: - - deadline: uint256 - - SUPER_TOKEN_PERMIT_INVALID_SIGNER: - inputs: - - signer: address - - owner: address - - # -- NFT proxy -- - - SUPER_TOKEN_NFT_PROXY_ADDRESS_CHANGED - - # -- Agreement hosting -- - - SF_TOKEN_AGREEMENT_ALREADY_EXISTS - - SF_TOKEN_AGREEMENT_DOES_NOT_EXIST - - # -- OpenZeppelin ECDSA (from permit) -- - - ECDSAInvalidSignature - - ECDSAInvalidSignatureLength: - inputs: - - length: uint256 - - ECDSAInvalidSignatureS: - inputs: - - s: bytes32 - - # -- OpenZeppelin SafeCast -- - - SafeCastOverflowedUintToInt: - inputs: - - value: uint256 - - # -- OpenZeppelin SafeERC20 -- - - SafeERC20FailedOperation: - inputs: - - token: address diff --git a/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml b/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml deleted file mode 100644 index f9bb474d..00000000 --- a/.agents/skills/gooddollar/references/contracts/SuperToken.selectors.yaml +++ /dev/null @@ -1,99 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - name(): 0x06fdde03 - symbol(): 0x95d89b41 - decimals(): 0x313ce567 - totalSupply(): 0x18160ddd - balanceOf(address): 0x70a08231 - allowance(address,address): 0xdd62ed3e - approve(address,uint256): 0x095ea7b3 - increaseAllowance(address,uint256): 0x39509351 - decreaseAllowance(address,uint256): 0xa457c2d7 - transfer(address,uint256): 0xa9059cbb - transferFrom(address,address,uint256): 0x23b872dd - transferAll(address): 0xa3a7e7f3 - granularity(): 0x556f0dc7 - send(address,uint256,bytes): 0x9bd9bbc6 - burn(uint256,bytes): 0xfe9d9303 - authorizeOperator(address): 0x959b8c3f - revokeOperator(address): 0xfad8b32a - isOperatorFor(address,address): 0xd95b6371 - defaultOperators(): 0x06e48538 - operatorSend(address,address,uint256,bytes,bytes): 0x62ad1b83 - operatorBurn(address,uint256,bytes,bytes): 0xfc673c4f - permit(address,address,uint256,uint256,uint8,bytes32,bytes32): 0xd505accf - nonces(address): 0x7ecebe00 - DOMAIN_SEPARATOR(): 0x3644e515 - eip712Domain(): 0x84b0196e - upgrade(): 0xd55ec697 - upgradeTo(address,uint256,bytes): 0x5b9d09cc - downgrade(uint256): 0x11bcc81e - downgradeTo(address,uint256): 0x83ba2525 - getUnderlyingToken(): 0xee719bc8 - getUnderlyingDecimals(): 0x92081a47 - toUnderlyingAmount(uint256): 0x282a050b - upgradeByETH(): 0xcf81464b - upgradeByETHTo(address): 0x7687d19b - downgradeToETH(uint256): 0x160e8be3 - realtimeBalanceOf(address,uint256): 0xeb3537cc - realtimeBalanceOfNow(address): 0x2ec8eec7 - isAccountCritical(address,uint256): 0xd9d078d6 - isAccountCriticalNow(address): 0x79359f6f - isAccountSolvent(address,uint256): 0xb84cdd4a - isAccountSolventNow(address): 0xbb0d196e - changeAdmin(address): 0x8f283970 - getAdmin(): 0x6e9960c3 - enableYieldBackend(): 0x5127b621 - disableYieldBackend(): 0x370a190f - getYieldBackend(): 0xe729804b - withdrawSurplusFromYieldBackend(): 0x4a3acda0 - selfMint(address,uint256,bytes): 0xc68d4283 - selfBurn(address,uint256,bytes): 0x9d876741 - selfApproveFor(address,address,uint256): 0x66a12fb6 - selfTransferFrom(address,address,address,uint256): 0x41b706be - operationApprove(address,address,uint256): 0x62aa5287 - operationIncreaseAllowance(address,address,uint256): 0x4b2763b3 - operationDecreaseAllowance(address,address,uint256): 0xc780fd82 - operationTransferFrom(address,address,address,uint256): 0x16d055d6 - operationSend(address,address,uint256,bytes): 0xca0c1e7f - operationUpgrade(address,uint256): 0xca789464 - operationDowngrade(address,uint256): 0x245887fc - operationUpgradeTo(address,address,uint256): 0x1ae88ffc - operationDowngradeTo(address,address,uint256): 0x47ba7ad1 - createAgreement(bytes32,bytes32[]"): 0x816e2dc5 - getAgreementData(address,bytes32,uint256): 0x6c2d9f2f - updateAgreementData(bytes32,bytes32[]"): 0x1bced9c7 - terminateAgreement(bytes32,uint256): 0x27048397 - updateAgreementStateSlot(address,uint256,bytes32[]"): 0x003af740 - getAgreementStateSlot(address,address,uint256,uint256): 0x4b61cc33 - settleBalance(address,int256): 0xcf97256d - makeLiquidationPayoutsV2(bytes32,bytes,address,bool,address,uint256,int256): 0x1863e809 - getAccountActiveAgreements(address): 0x386fa221 - getHost(): 0x20bc4425 - POOL_ADMIN_NFT(): 0xb20db1ac - POOL_MEMBER_NFT(): 0xf5a8b4dd - VERSION(): 0xffa1ad74 -events: - Transfer(address,address,uint256): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - Approval(address,address,uint256): 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 - Sent(address,address,address,uint256,bytes,bytes): 0x06b541ddaa720db2b10a4d0cdac39b8d360425fc073085fac19bc82614677987 - Minted(address,address,uint256,bytes,bytes): 0x2fe5be0146f74c5bce36c0b80911af6c7d86ff27e89d5cfa61fc681327954e5d - Burned(address,address,uint256,bytes,bytes): 0xa78a9be3a7b862d26933ad85fb11d80ef66b8f972d7cbba06621d583943a4098 - AuthorizedOperator(address,address): 0xf4caeb2d6ca8932a215a353d0703c326ec2d81fc68170f320eb2ab49e9df61f9 - RevokedOperator(address,address): 0x50546e66e5f44d728365dc3908c63bc5cfeeab470722c1677e3073a6ac294aa1 - EIP712DomainChanged(): 0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31 - TokenUpgraded(address,uint256): 0x25ca84076773b0455db53621c459ddc84fe40840e4932a62706a032566f399df - TokenDowngraded(address,uint256): 0x3bc27981aebbb57f9247dc00fde9d6cd91e4b230083fec3238fedbcba1f9ab3d - AdminChanged(address,address): 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f - YieldBackendEnabled(address,uint256): 0x8d15dd569157df615aedb4b16273001ae8980ce2aac93e39adc48481dfaefbb5 - YieldBackendDisabled(address): 0x2486b5241145ce1b97a13655ecd4e8e51094ac93259a0589d24524304d6d70d4 - PoolAdminNFTCreated(address): 0xeb87fb34067547f3dc0b85096c3da73c99d4fbb08ff41212b8d7c0b5008b42e6 - AgreementCreated(address,bytes32,bytes32[]"): 0x9a5caaf4460c6dff81ed3dc58f3aef40fe511737a163340e06a4abc1bfe21a73 - AgreementUpdated(address,bytes32,bytes32[]"): 0xc00aaaf56818c1a9d6c54e14c6f93965eb34c9729232fdafa62f90a10600e70a - AgreementTerminated(address,bytes32): 0x71a63dc095de07aa5512ad57a7596a39516317e316981a1cd71000057be1537b - AgreementStateUpdated(address,address,uint256): 0x30f416fa68fca014a0f334464c64b000ba53e99b6d2afdea9d5ca756372d5985 - AgreementLiquidatedV2(address,bytes32,address,address,address,uint256,int256,bytes): 0xb8381a3ce157650e06186e3e8f4dd4dc29236f2688b6eed1893d0a60d7c6386f - AgreementLiquidated(address,bytes32,address,address,uint256): 0x8505c3d8f1f184f032cf0bc4cd80ee61c8b9d94f8907c3281bf0101a2610fe80 - AgreementLiquidatedBy(address,address,bytes32,address,address,uint256,uint256): 0x5f22b60e58b1d6de858bc27c48d5a4653e052da99e083c1d88bb8c58e1abc8ef - Bailout(address,uint256): 0xd6c9a04afc81e8c614310bbee6c9e84f5abe15b82038bf8347014ce0852e6ffd -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml deleted file mode 100644 index 33ac5a94..00000000 --- a/.agents/skills/gooddollar/references/contracts/Superfluid.abi.yaml +++ /dev/null @@ -1,757 +0,0 @@ -# Superfluid Host — the central protocol router -# All agreement calls (CFA, IDA, GDA) are routed through the Host, which manages -# the Superfluid call context, Super App callbacks, app credit, and governance. -# The Host is also the entry point for batch calls and meta-transactions. -# -# NOTE: emits/errors mappings are traced from source code — verify against implementation. -# Proxy/upgradability functions (castrate, updateCode, getCodeAddress, proxiableUUID, initialize) -# are omitted — they belong to the UUPSProxiable layer. - -meta: - name: Superfluid - version: v1 - source: - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol - - https://raw.githubusercontent.com/superfluid-org/protocol-monorepo/refs/heads/dev/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol - implements: [ISuperfluid] - inherits: [UUPSProxiable, BaseRelayRecipient] - deployments: - mainnet: - eth-mainnet: "0x4E583d9390082B65Bef884b629DFA426114CED6d" - polygon-mainnet: "0x3E14dC1b13c488a8d5D310918780c983bD5982E7" - xdai-mainnet: "0x2dFe937cD98Ab92e59cF3139138f18c823a4efE7" - base-mainnet: "0x4C073B3baB6d8826b8C5b229f3cfdC1eC6E47E74" - optimism-mainnet: "0x567c4B141ED61923967cA25Ef4906C8781069a10" - arbitrum-one: "0xCf8Acb4eF033efF16E8080aed4c7D5B9285D2192" - bsc-mainnet: "0xd1e2cFb6441680002Eb7A44223160aB9B67d7E6E" - avalanche-c: "0x60377C7016E4cdB03C87EF474896C11cB560752C" - celo-mainnet: "0xA4Ff07cF81C02CFD356184879D953970cA957585" - scroll-mainnet: "0x0F86a21F6216c061B222c224e315d9FC34520bb7" - degenchain: "0xc1314EdcD7e478C831a7a24169F7dEADB2646eD2" - testnet: - avalanche-fuji: "0x85Fe79b998509B77BF10A8BD4001D58475D29386" - base-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" - eth-sepolia: "0x109412E3C84f0539b43d39dB691B08c90f58dC7c" - optimism-sepolia: "0xd399e2Fb5f4cf3722a11F65b88FAB6B2B8621005" - scroll-sepolia: "0x42b05a6016B9eED232E13fd56a8F0725693DBF8e" - deploymentCreationBlocks: - mainnet: - celo-mainnet: 16393469 - -# == Abbreviations == -# ctx — context (Superfluid call context bytes) -# ACL — Access Control List -# ERC — Ethereum Request for Comments - -# == Glossary == -# Host — central Superfluid contract routing agreement/app calls -# agreement class — a registered agreement contract (CFA, IDA, GDA) -# Super App — a smart contract registered with the Host that receives callbacks -# callback — beforeAgreement* / afterAgreement* hooks called on Super Apps -# context (ctx) — encoded call metadata: msgSender, timestamp, userData, app credit -# app credit — temporary token allowance granted to a Super App during a callback -# jailed — a Super App that violated protocol rules; its callbacks are bypassed -# configWord — bitmask encoding a Super App's app level and callback noop flags. -# App level: APP_LEVEL_FINAL (1 << 1) or APP_LEVEL_SECOND (1 << 0). -# Noop flags disable specific before/after callbacks per agreement type. -# batch operation — a single step in a batchCall array (see BatchOperation in Definitions.sol) -# trusted forwarder — a contract allowed to call forwardBatchCall for meta-transactions - -# == Batch Call == -# Primary entry point for interacting with the protocol. Users compose arrays of -# typed operations (ERC-20 approvals, SuperToken wrapping, agreement calls, etc.). -# -# == Batch Operation Types == -# Used in the operationType field of batchCall/forwardBatchCall operations. -# Data encoding varies by category — see notes on each type. -# -# Category 1–5: ERC-20/ERC-777 token ops -# target: must be a SuperToken (routes through SuperToken.operation* methods, -# NOT arbitrary ERC-20 contracts — use a separate tx for underlying approvals) -# data: abi-encoded parameters only (strip the 4-byte function selector) -# -# Category 101–102: SuperToken wrap/unwrap -# target: the SuperToken -# data: abi-encoded parameters only (strip the 4-byte function selector) -# -# Category 201: agreement call -# target: the agreement contract (e.g. CFA, GDA) -# data: abi.encode(callData, userData) where callData is full -# encodeFunctionData (selector included, with empty ctx placeholder "0x") -# -# Category 202: app action -# target: the Super App -# data: abi.encode(callData) where callData is full encodeFunctionData -# (selector included, with empty ctx placeholder "0x") -# -# Category 301: simple forward -# target: any contract -# data: full encodeFunctionData (selector included), or "0x" for pure value transfers -# -# Category 302: ERC-2771 forward -# target: any contract (must be an ERC-2771 recipient to extract the real sender) -# data: full encodeFunctionData (selector included) -# The ERC2771Forwarder appends the original msg.sender to calldata per ERC-2771. -# Use when the target contract needs to know who initiated the batch. - -batch_operation_types: - ERC20_APPROVE: 1 - ERC20_TRANSFER_FROM: 2 - ERC777_SEND: 3 # deprecated — ERC-777 is being phased out - ERC20_INCREASE_ALLOWANCE: 4 - ERC20_DECREASE_ALLOWANCE: 5 - SUPERTOKEN_UPGRADE: 101 - SUPERTOKEN_DOWNGRADE: 102 - SUPERFLUID_CALL_AGREEMENT: 201 - CALL_APP_ACTION: 202 - SIMPLE_FORWARD_CALL: 301 - ERC2771_FORWARD_CALL: 302 - -batchCall: - # Execute an array of operations in a single transaction as msg.sender. - notes: - - "Gotcha: For SIMPLE_FORWARD_CALL (type 301), target sees msg.sender as the SimpleForwarder contract, NOT the original caller. Use only when the target doesn't need to identify the sender." - - "Gotcha: ERC-20 ops (types 1-5) route through SuperToken.operation* methods, NOT arbitrary ERC-20 contracts. The target must be a SuperToken address. To approve an underlying ERC-20 (e.g. for wrapping), use a separate transaction." - - "Gotcha: If msg.value > 0, the entire amount is forwarded to the first CALL_APP_ACTION, SIMPLE_FORWARD, or ERC2771_FORWARD operation. If none exist, the native tokens are returned to the sender." - mutability: payable - access: anyone - inputs: - - operations: tuple[] - components: - - operationType: uint32 # see batch_operation_types above - - target: address - - data: bytes # encoding varies by operationType — see above - errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] - -forwardBatchCall: - # Same as batchCall but uses EIP-2771 to extract msgSender from calldata. - # Can only be called by contracts registered as trusted forwarders in governance. - # If native tokens are left over, they are refunded to the EIP-2771 sender. - mutability: payable - access: trusted-forwarder - inputs: - - operations: tuple[] - components: - - operationType: uint32 # see batch_operation_types above - - target: address - - data: bytes # encoding varies by operationType — see above - errors: [HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE] - -# == Contextless Call Proxies == -# Entry points for EOAs and non-app contracts to interact with agreements or apps. -# These create a fresh Superfluid context with msg.sender, inject it into the call, -# and route through to the target agreement/app. - -callAgreement: - # Call an agreement function. The Host creates a context with msg.sender and replaces - # the placeholder ctx bytes in callData. This is how EOAs interact with CFA/IDA/GDA. - mutability: nonpayable - access: anyone # context set to msg.sender - inputs: - - agreementClass: address # must be a registered agreement - - callData: bytes # abi-encoded agreement call with placeholder ctx - - userData: bytes # extra data forwarded to Super App callbacks - outputs: - - returnedData: bytes - errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT] - -callAppAction: - # Call an app action function on a Super App. The Host creates a context and routes - # the call. The action must not be an agreement callback selector. - mutability: nonpayable - access: anyone # context set to msg.sender - inputs: - - app: address # must be a registered, non-jailed Super App - - callData: bytes # abi-encoded app action call with placeholder ctx - outputs: - - returnedData: bytes - errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION] - -# == Contextual Call Proxies == -# For Super Apps to chain calls during callbacks. The app must pass the ctx it received. -# Only callable by the app currently executing a callback (ctx.appAddress == msg.sender). - -callAgreementWithContext: - # Chain an agreement call from within a Super App callback. - # The ctx must be valid and msg.sender must be the current callback app. - mutability: nonpayable - access: super-app - inputs: - - agreementClass: address - - callData: bytes - - userData: bytes - - ctx: bytes - outputs: - - newCtx: bytes - - returnedData: bytes - errors: [APP_RULE, HOST_ONLY_LISTED_AGREEMENT, HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS] - -callAppActionWithContext: - # Chain an app action call from within a Super App callback. - mutability: nonpayable - access: super-app - inputs: - - app: address - - callData: bytes - - ctx: bytes - outputs: - - newCtx: bytes - errors: [APP_RULE, HOST_NOT_A_SUPER_APP, HOST_SUPER_APP_IS_JAILED, HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION, HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS] - -# == Context Utilities == - -decodeCtx: - # Decode a raw context bytes blob into the structured Context fields. - mutability: pure - inputs: - - ctx: bytes - outputs: - - context: - type: tuple - components: - - appCallbackLevel: uint8 - - callType: uint8 - - timestamp: uint256 - - msgSender: address - - agreementSelector: bytes4 - - userData: bytes - - appCreditGranted: uint256 - - appCreditWantedDeprecated: uint256 - - appCreditUsed: int256 - - appAddress: address - - appCreditToken: address - -isCtxValid: - # Check if a context blob matches the current transaction's context stamp. - mutability: view - inputs: - - ctx: bytes - outputs: - - bool - -# == App Registry == -# Super App registration. On some deployments, governance permission (via SimpleACL -# or legacy governance config) is required before an app can register. - -registerApp: - # Register msg.sender as a Super App with the given config word. - # On whitelisting-enabled deployments, tx.origin must have the - # ACL_SUPERAPP_REGISTRATION_ROLE or a valid legacy governance key. - mutability: nonpayable - access: self - inputs: - - configWord: uint256 # Super App manifest flags (see SuperAppDefinitions) - emits: [AppRegistered] - errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -registerApp(address,uint256): - # Register an external contract as a Super App. - # Useful for factory patterns where the deployer registers the app. - mutability: nonpayable - access: anyone - inputs: - - app: address # must be a contract - - configWord: uint256 - emits: [AppRegistered] - errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -registerAppWithKey: - # DEPRECATED — use registerApp(uint256) instead. - # Legacy registration using a governance-provided registration key string. - mutability: nonpayable - access: self - inputs: - - configWord: uint256 - - registrationKey: string - emits: [AppRegistered] - errors: [HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -registerAppByFactory: - # DEPRECATED — use registerApp(address, uint256) instead. - # Legacy factory-based registration using governance-authorized factory addresses. - mutability: nonpayable - access: factory - inputs: - - app: address - - configWord: uint256 - emits: [AppRegistered] - errors: [HOST_MUST_BE_CONTRACT, HOST_NO_APP_REGISTRATION_PERMISSION, HOST_INVALID_CONFIG_WORD, HOST_SUPER_APP_ALREADY_REGISTERED] - -# == App Queries == - -isApp: - mutability: view - inputs: - - app: address - outputs: - - bool - -getAppCallbackLevel: - mutability: view - inputs: - - appAddr: address - outputs: - - uint8 - -getAppManifest: - # Returns the full manifest of a Super App. - # isSuperApp is false if the address was never registered. - mutability: view - inputs: - - app: address - outputs: - - isSuperApp: bool - - isJailed: bool - - noopMask: uint256 # bitmask of noop'd agreement callback selectors - -isAppJailed: - mutability: view - inputs: - - app: address - outputs: - - isJail: bool - -# == App Composition == -# Source apps can whitelist target apps for composability (calling downstream). -# Currently limited to MAX_APP_CALLBACK_LEVEL = 1 (one level of nesting). - -allowCompositeApp: - # Whitelist a target Super App for composition. msg.sender must be a Super App - # with a higher callback level than the target. - mutability: nonpayable - access: super-app - inputs: - - targetApp: address - errors: [HOST_SENDER_IS_NOT_SUPER_APP, HOST_RECEIVER_IS_NOT_SUPER_APP, HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL] - -isCompositeAppAllowed: - mutability: view - inputs: - - app: address - - targetApp: address - outputs: - - isAppAllowed: bool - -# == Agreement Framework == -# Internal protocol functions called by registered agreement contracts (CFA, IDA, GDA) -# to manage Super App callbacks and app credit. Only callable by agreements. -# -# Callback lifecycle (called by agreement during flow operations): -# 1. appCallbackPush — set up credit, push callback frame -# 2. callAppBeforeCallback — staticcall app's before hook (read-only) -# 3. (agreement executes core logic) -# 4. callAppAfterCallback — call app's after hook (state changes OK) -# 5. appCallbackPop — finalize credit accounting, pop frame - -callAppBeforeCallback: - # StaticCall a Super App's before-callback. Returns the callback data. - # If the callback reverts during a termination, the app is jailed instead of reverting. - mutability: nonpayable - access: agreement - inputs: - - app: address - - callData: bytes - - isTermination: bool - - ctx: bytes - outputs: - - cbdata: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] - -callAppAfterCallback: - # Call a Super App's after-callback. Returns the (potentially modified) context. - # If the app returns an invalid ctx, it is jailed on termination or reverts otherwise. - mutability: nonpayable - access: agreement - inputs: - - app: address - - callData: bytes - - isTermination: bool - - ctx: bytes - outputs: - - newCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT, HOST_NEED_MORE_GAS, APP_RULE] - -appCallbackPush: - # Push a new callback frame onto the context stack before invoking a Super App. - # Sets up app credit and tracks the callback nesting level. - notes: - - "Gotcha: The appCreditGranted is backed by the flow sender's deposit as \"owed deposit\". If the Super App opens outgoing streams using this credit, the sender's total locked capital increases accordingly (roughly doubles for 1:1 relay, more for fan-out patterns)." - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - app: address - - appCreditGranted: uint256 - - appCreditUsed: int256 - - appCreditToken: address - outputs: - - appCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT, APP_RULE] - -appCallbackPop: - # Pop the callback frame after a Super App callback completes. - # Adjusts appCreditUsed by the delta from the callback. - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - appCreditUsedDelta: int256 - outputs: - - newCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT] - -ctxUseCredit: - # Record additional app credit usage during an agreement operation. - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - appCreditUsedMore: int256 - outputs: - - newCtx: bytes - errors: [HOST_ONLY_LISTED_AGREEMENT] - -jailApp: - # Jail a Super App from within an agreement. Jailed apps have their callbacks bypassed. - mutability: nonpayable - access: agreement - inputs: - - ctx: bytes - - app: address - - reason: uint256 # see SuperAppDefinitions for jail reason codes - outputs: - - newCtx: bytes - emits: [Jail] - errors: [HOST_ONLY_LISTED_AGREEMENT] - -# == Governance == - -getGovernance: - mutability: view - outputs: - - governance: address - -replaceGovernance: - mutability: nonpayable - access: governance - inputs: - - newGov: address - emits: [GovernanceReplaced] - errors: [HOST_ONLY_GOVERNANCE] - -# == Agreement Whitelisting == -# Governance registers and upgrades agreement classes (CFA, IDA, GDA). - -registerAgreementClass: - # Register a new agreement class. Creates a UUPS proxy in upgradable deployments. - mutability: nonpayable - access: governance - inputs: - - agreementClassLogic: address - emits: [AgreementClassRegistered] - errors: [HOST_ONLY_GOVERNANCE, HOST_AGREEMENT_ALREADY_REGISTERED, HOST_MAX_256_AGREEMENTS] - -updateAgreementClass: - # Upgrade an existing agreement class to a new implementation. - mutability: nonpayable - access: governance - inputs: - - agreementClassLogic: address - emits: [AgreementClassUpdated] - errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE, HOST_AGREEMENT_IS_NOT_REGISTERED] - -isAgreementTypeListed: - mutability: view - inputs: - - agreementType: bytes32 # keccak256 of agreement name string - outputs: - - yes: bool - -isAgreementClassListed: - mutability: view - inputs: - - agreementClass: address - outputs: - - yes: bool - -getAgreementClass: - mutability: view - inputs: - - agreementType: bytes32 - outputs: - - agreementClass: address - errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] - -mapAgreementClasses: - # Decode a bitmap into an array of agreement class addresses. - mutability: view - inputs: - - bitmap: uint256 - outputs: - - agreementClasses: address[] - -addToAgreementClassesBitmap: - mutability: view - inputs: - - bitmap: uint256 - - agreementType: bytes32 - outputs: - - newBitmap: uint256 - errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] - -removeFromAgreementClassesBitmap: - mutability: view - inputs: - - bitmap: uint256 - - agreementType: bytes32 - outputs: - - newBitmap: uint256 - errors: [HOST_AGREEMENT_IS_NOT_REGISTERED] - -# == Super Token Factory == - -getSuperTokenFactory: - mutability: view - outputs: - - factory: address - -getSuperTokenFactoryLogic: - # Returns the logic address behind the factory proxy. For non-upgradable - # deployments, returns the factory address itself. - mutability: view - outputs: - - logic: address - -updateSuperTokenFactory: - # Deploy or upgrade the SuperTokenFactory. On first call, creates a UUPS proxy. - mutability: nonpayable - access: governance - inputs: - - newFactory: address - emits: [SuperTokenFactoryUpdated] - errors: [HOST_ONLY_GOVERNANCE, HOST_NON_UPGRADEABLE] - -updateSuperTokenLogic: - # Upgrade a SuperToken proxy to new logic. If newLogicOverride is address(0), - # uses the canonical logic from the SuperTokenFactory. - mutability: nonpayable - access: governance - inputs: - - token: address - - newLogicOverride: address # address(0) means use canonical logic - emits: [SuperTokenLogicUpdated] - errors: [HOST_ONLY_GOVERNANCE] - -changeSuperTokenAdmin: - # Change the admin of a SuperToken. The admin is the only account allowed to - # upgrade the token logic. Default admin is the Host itself. - mutability: nonpayable - access: governance - inputs: - - token: address - - newAdmin: address - errors: [HOST_ONLY_GOVERNANCE] - -# == Pool Beacon == - -updatePoolBeaconLogic: - # Upgrade the SuperfluidPool beacon to new logic. Affects all GDA pool proxies. - mutability: nonpayable - access: governance - inputs: - - newLogic: address - emits: [PoolBeaconLogicUpdated] - errors: [HOST_ONLY_GOVERNANCE] - -# == ERC2771 / Forwarder == - -isTrustedForwarder: - # Check if an address is a governance-approved trusted forwarder for meta-transactions. - mutability: view - inputs: - - forwarder: address - outputs: - - bool - -versionRecipient: - # ERC2771 relay recipient version string. Returns "v1". - mutability: pure - outputs: - - string - -getERC2771Forwarder: - # Returns the ERC2771Forwarder contract used for ERC2771_FORWARD_CALL batch operations. - mutability: view - outputs: - - address - -getSimpleACL: - # Returns the SimpleACL contract used for Super App registration permissioning. - mutability: view - outputs: - - address - -# == Time == - -getNow: - # Returns block.timestamp. Useful for off-chain tooling and testing. - mutability: view - outputs: - - uint256 - -# == Protocol Constants == - -NON_UPGRADABLE_DEPLOYMENT: - # True if the Host was deployed in non-upgradable mode. - mutability: view - outputs: - - bool - -APP_WHITE_LISTING_ENABLED: - # True if Super App registration requires governance permission. - mutability: view - outputs: - - bool - -CALLBACK_GAS_LIMIT: - # Maximum gas forwarded to Super App callbacks. - mutability: view - outputs: - - uint64 - -MAX_APP_CALLBACK_LEVEL: - # Maximum callback nesting depth for composed Super Apps. Currently 1. - mutability: view - outputs: - - uint256 - -MAX_NUM_AGREEMENTS: - # Maximum number of agreement classes that can be registered. 256. - mutability: view - outputs: - - uint32 - -ACL_SUPERAPP_REGISTRATION_ROLE: - # keccak256("ACL_SUPERAPP_REGISTRATION_ROLE") — the SimpleACL role for app registration. - mutability: view - outputs: - - bytes32 - -SIMPLE_FORWARDER: - # Address of the SimpleForwarder used for SIMPLE_FORWARD_CALL batch operations. - mutability: view - outputs: - - address - -# == Events == - -events: - GovernanceReplaced: - data: - - oldGov: address - - newGov: address - - AgreementClassRegistered: - # agreementType is keccak256("org.superfluid-finance.agreements..") - data: - - agreementType: bytes32 - - code: address - - AgreementClassUpdated: - data: - - agreementType: bytes32 - - code: address - - SuperTokenFactoryUpdated: - data: - - newFactory: address - - SuperTokenLogicUpdated: - indexed: - - token: address - data: - - code: address - - PoolBeaconLogicUpdated: - indexed: - - beaconProxy: address - data: - - newBeaconLogic: address - - AppRegistered: - indexed: - - app: address - - Jail: - # Emitted when a Super App is jailed for violating protocol rules. - # See APP_RULE error below for reason code values. - indexed: - - app: address - data: - - reason: uint256 - - # Inherited events (from UUPSProxiable): - # CodeUpdated — emitted on proxy upgrade (uuid, codeAddress) - # Initialized — emitted on proxy initialization (version) - -# == Errors == - -errors: - # Governance - - HOST_ONLY_GOVERNANCE # caller is not the governance contract - - # Agreement whitelisting - - HOST_AGREEMENT_ALREADY_REGISTERED - - HOST_AGREEMENT_IS_NOT_REGISTERED - - HOST_MAX_256_AGREEMENTS # maximum agreement slots exhausted - - HOST_ONLY_LISTED_AGREEMENT # caller is not a registered agreement - - # Upgradability - - HOST_NON_UPGRADEABLE # deployment is non-upgradable - - HOST_CANNOT_DOWNGRADE_TO_NON_UPGRADEABLE - - # App registration - - HOST_MUST_BE_CONTRACT # app address has no code - - HOST_NO_APP_REGISTRATION_PERMISSION - - HOST_INVALID_CONFIG_WORD # invalid Super App manifest flags - - HOST_SUPER_APP_ALREADY_REGISTERED - - HOST_NOT_A_SUPER_APP # target is not a registered Super App - - HOST_SUPER_APP_IS_JAILED - - # App composition - - HOST_SENDER_IS_NOT_SUPER_APP - - HOST_RECEIVER_IS_NOT_SUPER_APP - - HOST_SOURCE_APP_NEEDS_HIGHER_APP_LEVEL - - # Context & call proxies - - HOST_NON_ZERO_LENGTH_PLACEHOLDER_CTX # placeholder ctx must be empty - - HOST_CALL_AGREEMENT_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress - - HOST_CALL_APP_ACTION_WITH_CTX_FROM_WRONG_ADDRESS # msg.sender != ctx.appAddress - - HOST_AGREEMENT_CALLBACK_IS_NOT_ACTION # app action selector matches a callback - - # Batch call - - HOST_UNKNOWN_BATCH_CALL_OPERATION_TYPE - - # Callback execution - - HOST_NEED_MORE_GAS # insufficient gas for Super App callback - - # Super App rule violations (generic, carries a reason code) - # Reason codes (from SuperAppDefinitions.sol): - # 10 — NO_REVERT_ON_TERMINATION_CALLBACK (delete callback reverted) - # 11 — NO_CRITICAL_SENDER_ACCOUNT (callback made sender critical) - # 12 — NO_CRITICAL_RECEIVER_ACCOUNT (callback made receiver critical) - # 20 — CTX_IS_READONLY (state change in before-callback staticcall) - # 21 — CTX_IS_NOT_CLEAN (context not properly returned) - # 22 — CTX_IS_MALFORMATED (context bytes corrupted/fabricated) - # 30 — COMPOSITE_APP_IS_NOT_WHITELISTED (no allowCompositeApp) - # 31 — COMPOSITE_APP_IS_JAILED (downstream app is jailed) - # 40 — MAX_APP_LEVEL_REACHED (callback nesting exceeded limit) - - APP_RULE: - inputs: - - _code: uint256 - - # SafeCast (inherited from OpenZeppelin) - - SafeCastOverflowedUintDowncast: - inputs: - - bits: uint8 - - value: uint256 diff --git a/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml b/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml deleted file mode 100644 index 18edf9b0..00000000 --- a/.agents/skills/gooddollar/references/contracts/Superfluid.selectors.yaml +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - batchCall(tuple[]): 0x31131761 - forwardBatchCall(tuple[]): 0xbc5a2101 - callAgreement(): 0xb7a9d0f0 - callAppAction(): 0x40df3d29 - callAgreementWithContext(address,bytes,bytes,bytes): 0x4329d293 - callAppActionWithContext(address,bytes,bytes): 0xba48b5f8 - decodeCtx(bytes): 0x3f6c923a - isCtxValid(bytes): 0xbf428734 - registerApp(): 0x6d6c85c3 - registerAppWithKey(uint256,string): 0xbd1c448b - registerAppByFactory(address,uint256): 0xf3733052 - isApp(address): 0x3ca3ad4e - getAppCallbackLevel(address): 0x9378fa13 - getAppManifest(address): 0xf9f522f4 - isAppJailed(address): 0x6b4f3335 - allowCompositeApp(address): 0x57121e0c - isCompositeAppAllowed(address,address): 0xbb84cfa1 - callAppBeforeCallback(address,bytes,bool,bytes): 0x74041e02 - callAppAfterCallback(address,bytes,bool,bytes): 0x1e6d0a84 - appCallbackPush(bytes,address,uint256,int256,address): 0x768fabb0 - appCallbackPop(bytes,int256): 0x989b0c3e - ctxUseCredit(bytes,int256): 0x59a29141 - jailApp(bytes,address): 0x5578431e - replaceGovernance(address): 0x7283100c - registerAgreementClass(address): 0x15a024e1 - updateAgreementClass(address): 0x06cecba8 - isAgreementTypeListed(): 0xf8dba358 - isAgreementClassListed(address): 0x8ca48484 - getAgreementClass(bytes32): 0xb6d200de - mapAgreementClasses(uint256): 0xc56a069d - addToAgreementClassesBitmap(uint256,bytes32): 0xbced3ddc - removeFromAgreementClassesBitmap(uint256,bytes32): 0xa5dbbbcd - updateSuperTokenFactory(address): 0x54fbc493 - updateSuperTokenLogic(address): 0x787afde7 - changeSuperTokenAdmin(address,address): 0x0c565075 - updatePoolBeaconLogic(address): 0x2f89bf89 - isTrustedForwarder(address): 0x572b6c05 - SIMPLE_FORWARDER(): 0xf85263b9 -events: - GovernanceReplaced(address,address): 0x13abda02e63c790d0e2818b251282cfe5cbe0a8abd69c54bf5d2260c0907bd2e - AgreementClassRegistered(bytes32,address): 0x878135063a6cfb3bc333e534b1fdc83f4f12221cad6705c31c0567048a8bd3d1 - AgreementClassUpdated(bytes32,address): 0x9279aa773f2b588996032d8de89911555039f28b13a11a7c17074330bc082d9a - SuperTokenFactoryUpdated(address): 0xce13a9895a1719ad4493b2ac1a9bfb36070566161abab408e7ecbe586da8d499 - SuperTokenLogicUpdated(address,address): 0x840acbd291b38534819f47f875839277e502f40e1c7bfea2c5fc2c8017442cd3 - PoolBeaconLogicUpdated(address,address): 0x052cea8931962dd445ef48b0b998d3056bd0705f437087d60fe3c46a3fa09e1f - AppRegistered(address): 0x0d540ad8f39e07d19909687352b9fa017405d93c91a6760981fbae9cf28bfef7 - Jail(address,uint256): 0xbe3aa33bd245135e4e26b223d79d14ea479a47bff09f2b03c53838af1edbb14b - # Inherited events (from UUPSProxiable)(): 0x3a1d0f622cf5be33dc2558b359f025fbeca1d6fb271fb70452a02b0ec2666ce8 -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml deleted file mode 100644 index 75e5e437..00000000 --- a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.abi.yaml +++ /dev/null @@ -1,305 +0,0 @@ -# UBISchemeV2 — dynamic daily UBI pool with per-root claiming and cycle accounting -# Claim uses Identity getWhitelistedRoot(msg.sender); transfers use the root as claim key. - -meta: - name: UBISchemeV2 - version: v2 - source: - - https://raw.githubusercontent.com/GoodDollar/GoodProtocol/master/contracts/ubi/UBISchemeV2.sol - inherits: [DAOUpgradeableContract] - note: > - Pool sizing uses cycleLength, reserveFactor, minActiveUsers, and on-chain G$ - balance. claim() reverts if Identity root is zero. Internal accounting keys - claims by whitelisted root, payouts go to msg.sender. - deployments: - mainnet: - production: - UBIScheme: - networkId: 122 - address: "0xd253A5203817225e9768C05E5996d642fb96bA86" - creationBlock: 15747401 - production-celo: - UBIScheme: - networkId: 42220 - address: "0x43d72Ff17701B2DA814620735C39C620Ce0ea4A1" - creationBlock: 18006679 - production-xdc: - UBIScheme: - networkId: 50 - address: "0x22867567E2D80f2049200E25C6F31CB6Ec2F0faf" - creationBlock: 95249624 - related: - - https://docs.gooddollar.org/for-developers/core-contracts/ubischeme - - references/guides/claim.md - -getClaimerCount: - mutability: view - inputs: - - day: uint256 - outputs: - - count: uint256 - -getClaimAmount: - notes: - - "Returns aggregate amount claimed for the provided day index." - mutability: view - inputs: - - day: uint256 - outputs: - - amount: uint256 - -getDailyStats: - notes: - - "Returns (claimer count, total claimed amount) for the calendar day derived from periodStart." - mutability: view - inputs: [] - outputs: - - claimerCount: uint256 - - claimAmount: uint256 - -setCycleLength: - mutability: nonpayable - access: avatar - inputs: - - _newLength: uint256 - outputs: [] - emits: [CycleLengthSet] - errors: [CYCLE_TOO_SHORT, ONLY_AVATAR] - -setDay: - notes: - - "Anyone may advance currentDay when wall-clock day crosses the next boundary." - mutability: nonpayable - inputs: [] - outputs: [] - emits: [DaySet] - -hasClaimed: - mutability: view - inputs: - - account: address - outputs: - - claimed: bool - -isNotNewUser: - mutability: view - inputs: - - _account: address - outputs: - - isReturning: bool - -estimateNextDailyUBI: - notes: - - "View-only projection when dailyUbi not yet fixed for the current wall day." - mutability: view - inputs: [] - outputs: - - projected: uint256 - -"checkEntitlement()": - notes: - - "Uses msg.sender as member; resolves Identity root before hasClaimed check." - mutability: view - inputs: [] - outputs: - - amount: uint256 - -"checkEntitlement(address)": - notes: - - "Checks claimable amount for the provided member using that account's whitelisted root." - mutability: view - inputs: - - _member: address - outputs: - - amount: uint256 - -claim: - notes: - - "Resolves whitelistedRoot = Identity.getWhitelistedRoot(msg.sender); reverts if zero." - - "_claim keys bookkeeping by root but pays msg.sender." - - "May notify ClaimersDistribution at GDAO_CLAIMERS if configured." - - "On success emits UBIClaimed; distributionFormula may emit UBICalculated, UBICycleCalculated, DaySet, WithdrawFromDao." - mutability: nonpayable - access: anyone - inputs: [] - outputs: - - didClaim: bool - errors: - - NOT_IN_PERIOD_OR_PAUSED - - NOT_WHITELISTED - - CLAIM_TRANSFER_FAILED - - DAO_TRANSFER_FAILED - -setShouldWithdrawFromDAO: - mutability: nonpayable - access: avatar - inputs: - - _shouldWithdraw: bool - outputs: [] - emits: [ShouldWithdrawFromDAOSet] - errors: [ONLY_AVATAR] - -pause: - mutability: nonpayable - access: avatar - inputs: - - _pause: bool - outputs: [] - errors: [ONLY_AVATAR] - -setNewClaimersReserveFactor: - notes: - - "Adjusts reserveFactor used by distribution formula to reserve part of pool for new claimers." - mutability: nonpayable - access: avatar - inputs: - - _reserveFactor: uint32 - outputs: [] - errors: [ONLY_AVATAR] - -withdraw: - notes: - - "Avatar-controlled treasury action transferring G$ from scheme balance to recipient." - mutability: nonpayable - access: avatar - inputs: - - _amount: uint256 - - _recipient: address - outputs: [] - errors: - - ONLY_AVATAR - - WITHDRAW_FAILED - -lastClaimed: - notes: - - "Returns last claim timestamp keyed by account root logic used by hasClaimed checks." - mutability: view - inputs: - - account: address - outputs: - - ts: uint256 - -currentDay: - mutability: view - inputs: [] - outputs: - - day: uint256 - -periodStart: - mutability: view - inputs: [] - outputs: - - ts: uint256 - -dailyUbi: - mutability: view - inputs: [] - outputs: - - amount: uint256 - -lastWithdrawDay: - mutability: view - inputs: [] - outputs: - - day: uint256 - -shouldWithdrawFromDAO: - mutability: view - inputs: [] - outputs: - - flag: bool - -cycleLength: - mutability: view - inputs: [] - outputs: - - days: uint256 - -dailyCyclePool: - mutability: view - inputs: [] - outputs: - - pool: uint256 - -startOfCycle: - mutability: view - inputs: [] - outputs: - - ts: uint256 - -currentCycleLength: - mutability: view - inputs: [] - outputs: - - length: uint256 - -minActiveUsers: - mutability: view - inputs: [] - outputs: - - n: uint256 - -totalClaimsPerUser: - mutability: view - inputs: - - account: address - outputs: - - count: uint256 - -reserveFactor: - mutability: view - inputs: [] - outputs: - - bps: uint32 - -paused: - mutability: view - inputs: [] - outputs: - - isPaused: bool - -events: - WithdrawFromDao: - indexed: [] - data: - - prevBalance: uint256 - - newBalance: uint256 - UBICalculated: - indexed: [] - data: - - day: uint256 - - dailyUbi: uint256 - - blockNumber: uint256 - UBICycleCalculated: - indexed: [] - data: - - day: uint256 - - pool: uint256 - - cycleLength: uint256 - - dailyUBIPool: uint256 - UBIClaimed: - indexed: - - claimer: address - data: - - amount: uint256 - CycleLengthSet: - indexed: [] - data: - - newCycleLength: uint256 - DaySet: - indexed: [] - data: - - newDay: uint256 - ShouldWithdrawFromDAOSet: - indexed: [] - data: - - ShouldWithdrawFromDAO: bool - -errors: - NOT_IN_PERIOD_OR_PAUSED: "not in periodStarted or paused — requireStarted modifier." - CYCLE_TOO_SHORT: "cycle must be at least 1 day long" - DAO_TRANSFER_FAILED: "DAO transfer has failed — _withdrawFromDao balance check." - NOT_WHITELISTED: "UBIScheme: not whitelisted — claim()" - CLAIM_TRANSFER_FAILED: "claim transfer failed — G$ transfer in _transferTokens." - WITHDRAW_FAILED: "withdraw failed — avatar withdraw." - ONLY_AVATAR: "Inherited avatar-only guard on DAOUpgradeable paths." diff --git a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml b/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml deleted file mode 100644 index 666b89d5..00000000 --- a/.agents/skills/gooddollar/references/contracts/UBISchemeV2.selectors.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by scripts/selectors.mjs -functions: - getClaimerCount(uint256): 0xc7a76adf - getClaimAmount(uint256): 0xcef63600 - getDailyStats(): 0x069786ea - setCycleLength(uint256): 0x3d84ceca - setDay(): 0xdddc3616 - hasClaimed(address): 0x73b2e80e - isNotNewUser(address): 0xa21f698a - estimateNextDailyUBI(): 0xc7713870 - checkEntitlement(): 0x98d6621b - checkEntitlement(address): 0x1a787f2e - claim(): 0x4e71d92d - setShouldWithdrawFromDAO(bool): 0xde1de3a0 - pause(bool): 0x02329a29 - setNewClaimersReserveFactor(uint32): 0x414089be - withdraw(uint256,address): 0x00f714ce - lastClaimed(address): 0x013eba92 - currentDay(): 0x5c9302c9 - periodStart(): 0xeda4e6d6 - dailyUbi(): 0x1d8f5ea9 - lastWithdrawDay(): 0xd7c4cbb8 - shouldWithdrawFromDAO(): 0x456ac1c2 - cycleLength(): 0xeac471a0 - dailyCyclePool(): 0x9dc2c033 - startOfCycle(): 0xba075410 - currentCycleLength(): 0x741470ac - minActiveUsers(): 0x37658574 - totalClaimsPerUser(address): 0xcc054dfc - reserveFactor(): 0x4322b714 - paused(): 0x5c975abb -events: - WithdrawFromDao(uint256,uint256): 0x3107ec7eaa50b775d2486c7a394472235804b6fe1c0d4b7bd1d79b09df60f2ba - UBICalculated(uint256,uint256,uint256): 0x836fa39995340265746dfe9587d9fe5c5de35b7bce778afd9b124ce1cfeafdc4 - UBICycleCalculated(uint256,uint256,uint256,uint256): 0x83e0d535b9e84324e0a25922406398d6ff5f96d0c686204ee490e16d7670566f - UBIClaimed(address,uint256): 0x89ed24731df6b066e4c5186901fffdba18cd9a10f07494aff900bdee260d1304 - CycleLengthSet(uint256): 0xa61e6cca2c12e2a0a493683acfe95b034f0f50d793434f4dfe3ba06ea201f344 - DaySet(uint256): 0x67eb03bd555181f9dd23f546e4331ddfb8b4a7d0c8d261ba44e037f30ce894ea - ShouldWithdrawFromDAOSet(bool): 0x6cd9a0fd2e006be39a9918bf56c85cae1d4f4599474483ff18cb93355ebaaf8e -errors: {} diff --git a/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md b/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md deleted file mode 100644 index 41e2baeb..00000000 --- a/.agents/skills/gooddollar/references/contracts/_rich-abi-yaml-format.md +++ /dev/null @@ -1,93 +0,0 @@ -# Reading the Rich ABI YAMLs - -Each YAML is a self-contained contract reference. Here's how to parse them. - -## Root structure - -``` -# Header comment — contract name, description, key notes -meta: # name, version, source, implements, inherits, deployments, - # deploymentCreationBlocks (optional), notes -# == Section == # Grouped functions (these are the core content) -events: # All events the contract emits -errors: # Complete error index -``` - -Three root keys are reserved: `meta`, `events`, `errors`. Every other -root-level key is a **function**. - -## Function entries - -```yaml -createFlow: - # Description of what the function does. - notes: - - "Gotcha: Non-obvious behavior or edge cases listed here as structured data." - mutability: nonpayable # view | pure | nonpayable | payable - access: sender | operator # who can call (omitted for view/pure) - inputs: - - token: address - - receiver: address - - flowRate: int96 # inline comments for non-obvious params - - ctx: bytes - outputs: - - newCtx: bytes - emits: [FlowUpdated, FlowUpdatedExtension] # ordered by emission sequence - errors: [CFA_FLOW_ALREADY_EXISTS, CFA_INVALID_FLOW_RATE] # ordered by check sequence -``` - -Fields appear in this order: description comment, `notes`, `mutability`, -`access`, `inputs`, `outputs`, `emits`, `errors`. All are omitted when not -applicable. - -## Key conventions - -- **`ctx: bytes` parameter** = function is called through the Host - (`callAgreement` / `batchCall`), never directly. -- **`access` labels**: `anyone`, `host`, `self`, `admin`, `governance`, - `sender`, `receiver`, `operator`, `manager`, `pic`, `agreement`, - `trusted-forwarder`, `factory`, `super-app`. Combine with `|`. Conditional: - `anyone(if-critical-or-jailed)`. -- **`emits` and `errors` ordering** carries meaning: matches execution flow, - not alphabetical. First errors in the list are the most likely causes. -- **`notes:` field** on functions (and `meta.notes:` at contract level) - lists non-obvious behavior, common mistakes, or edge cases. Always read - these carefully. -- **`meta.source`** is an array of raw GitHub URLs to the Solidity source files - (implementation, interface, base — filenames are self-documenting). -- **`meta.deployments`** has per-network addresses split into `mainnet` and - `testnet` subgroups. -- **`creationBlock`**: decimal block where that **`address`** first has code; - put it on the same object as **`networkId`** / **`address`**, immediately after **`address`**. -- **`meta.deploymentCreationBlocks`**: same shape as **`meta.deployments`**, for - deployments stored as plain address strings; leaves are decimal block numbers. - -## Events section - -```yaml -events: - FlowUpdated: - indexed: # log topics (filterable) - - token: address - - sender: address - data: # log payload - - flowRate: int96 -``` - -## Errors section - -```yaml -errors: - # -- Category -- - - SIMPLE_ERROR # description - - PARAMETERIZED_ERROR: # errors with diagnostic data - inputs: - - value: uint256 -``` - -## Selector sidecar files - -Every `Foo.abi.yaml` has a companion `Foo.selectors.yaml` in the same -directory. These contain computed hex selectors (function 4-byte, error -4-byte, event 32-byte topic0) with full Solidity signatures for -verification. Generated by `scripts/selectors.mjs`. diff --git a/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md b/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md deleted file mode 100644 index 12b18990..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/faucet-flows.md +++ /dev/null @@ -1,42 +0,0 @@ -# Faucet flows (user-facing explanation) - -This note explains the Faucet in plain language: what it does for users, why a top-up can fail, and what limits exist. - -The on-chain Faucet contract is used to add a small amount of native gas token to a wallet so the user can pay transaction fees. -Reference implementation: [`contracts/fuseFaucet/Faucet.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/fuseFaucet/Faucet.sol). -Addresses per chain: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `Faucet` under `production`, `production-celo`, `production-xdc`). - -## What this means for users - -- If your wallet is eligible, Faucet can send a small gas top-up. -- Eligibility usually depends on identity status and anti-abuse limits. -- This is a support mechanism for transaction fees, not a general transfer or swap service. - -## Main actions (in user language) - -- **Top up wallet** (`topWallet`) - Attempts to send gas to the target wallet after checks pass. - -- **Check eligibility first** (`canTop`) - Fast pre-check to see if top-up is currently allowed. - -- **Estimate top-up amount** (`getToppingAmount`) - Shows the amount Faucet would try to send right now. - -## Why a top-up may fail - -- You are not currently authorized by identity rules. -- Daily limit reached. -- Weekly limit reached. -- Wallet is temporarily banned. -- Wallet is too new for current policy. -- Calculated top-up is below minimum threshold. - -## Important safety note - -- The `onTokenTransfer` path includes a swap-like mechanism and is not meant as a normal user swap route. -- It does not enforce slippage protection in the same way users expect from a dedicated swap UI. - -## For developers and agents - -Use `references/guides/faucet.md` for step-by-step execution flow and deterministic preflight calls. diff --git a/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md b/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md deleted file mode 100644 index 261c2d7f..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/fuse-to-celo-staking-migration.md +++ /dev/null @@ -1,38 +0,0 @@ -# Why Fuse to CELO staking migration uses a staged backend flow - -This explains why the migration is split into allowance detection, unstake, bridge, and destination re-stake instead of a single transaction. In this context, Fuse `GovernanceStakingV2` is the old/source staking contract and Celo **`GooddollarSavingsStream`** is the destination staking contract ([source](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [0x059ee811414230d1Fb157878D2b491240F4D8d3B](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). - -## Why this cannot be one-chain atomic - -Fuse governance staking and CELO destination savings live on different chains, so the system cannot atomically close and reopen stake in one EVM transaction. Cross-chain migration is asynchronous by design and must tolerate timing gaps between source completion and destination finalization. - -## Why user allowance is first - -The migration flow assumes a backend-operated execution wallet. If that wallet needs to pull or operate on user funds in the source path, user approval must exist first. Without allowance, all downstream steps fail, so approval state is the earliest hard gate. - -## Why unstake is separated from bridge - -The source staking position is the canonical balance record on Fuse. The migration must first materialize transferable G$ by closing or reducing stake, then bridge only the confirmed unlocked amount. Bridging before final unstake confirmation introduces mismatch risk. - -## Why bridge finalization must be explicit - -Bridge transfers are eventually consistent across chains. The destination stake step must only run after the bridged G$ is confirmed on CELO. This avoids phantom staking attempts and preserves deterministic accounting. - -## Why destination uses `stakeFor` - -Ubeswap `GooddollarSavingsStream` on Celo supports `stakeFor(amount, recipient)`, which allows the backend execution wallet to stake on behalf of the user after bridge finalization. This fits migration operations where custody is temporary during the transfer window. - -## Main operational risks - -- partial migration from source unstake or bridge limit constraints -- stuck-in-transit bridge messages delaying destination staking -- stale assumptions about contract addresses across networks -- reward expectation mismatch when moving from Fuse governance staking mechanics to CELO savings mechanics - -## Contract/source map - -- Fuse staking family (GoodProtocol): [`GovernanceStaking.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/governance/GovernanceStaking.sol) -- Fuse deployment mapping: [`deployment.json`](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`production.GovernanceStakingV2`) -- Celo savings (Ubeswap): [`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol) -- Ubeswap contracts repository: [Ubeswap/gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) -- Bridge normalization for LZ quotes: [`BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) (off-chain LayerZero fee estimation must match this; `canBridge` and `bridgeToWithLz` use the raw burn amount on the source chain) diff --git a/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md b/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md deleted file mode 100644 index 754a3a05..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/gooddao-daostack-surface.md +++ /dev/null @@ -1,37 +0,0 @@ -# GoodDAO and DAOStack surface - -GoodProtocol’s on-chain **governance shell** is largely **DAOStack-shaped**: an **Avatar** holds protocol assets and reputation context; a **Controller** registers **schemes** and routes privileged calls. GoodDocs summarizes DAO-facing roles; **Avatar**, **Controller**, and other DAO contract addresses live only in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). Implementation follows [DAOStack Arc](https://github.com/daostack/arc) patterns. - -## Core interfaces (GoodProtocol) - -[`DAOStackInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/DAOStackInterfaces.sol) defines the pieces most integrations touch: - -### Avatar - -- **`nativeToken()`** — often the G$ token address for the deployment. -- **`nativeReputation()`** — **`GReputation`** token for voting weight (see `GReputation` in `deployment.json`). -- **`owner()`** — owner of the Avatar, typically the **`Controller`** contract. - -### Controller - -- **`avatar()`** — address of the Avatar contract. -- **`registerScheme` / `unregisterScheme` / `unregisterSelf` / `isSchemeRegistered` / `getSchemePermissions`** — scheme lifecycle and permission bitmask per scheme+avatar. -- **`genericCall(contract, data, avatar, value)`** — executes arbitrary calls **as the avatar** (used heavily by DAO-backed contracts to move tokens or call NameService). -- **`mintTokens`**, **`externalTokenTransfer`**, **`sendEther`** — treasury-style operations through the controller/avatar. - -## How GoodProtocol contracts use it - -- **`DAOUpgradeableContract`** / **`DAOContract`** descendants resolve **`dao`** (Controller) and **`avatar`** from **NameService** keys such as **`CONTROLLER`** and **`AVATAR`** (also written during **NameService.initialize**). -- **Avatar-gated writes** (for example **NameService.setAddress**, **UBISchemeV2** admin functions, **IdentityV3** after `initDAO`) require **`msg.sender == dao.avatar()`** (or equivalent role), not EOAs. -- Schemes that upgrade themselves (for example staking **`upgrade()`**) use **`dao.genericCall`** and **`unregisterSelf`** against the avatar. - -## What agents should do - -1. Treat **DAO calls** as **governance-only** unless the user explicitly controls the avatar or a registered scheme. -2. For **read-only** work, use **NameService** and per-contract **view** functions; use **Controller.isSchemeRegistered** when validating that a target contract is an approved scheme (for example staking migrations). -3. Never fabricate **Avatar** or **Controller** addresses — use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or NameService. - -## References - -- [Core contracts — DAO contracts](https://docs.gooddollar.org/for-developers/core-contracts) (narrative only; addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) -- DAOStack Arc controller and avatar concepts in the upstream repo linked from GoodDocs. diff --git a/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md b/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md deleted file mode 100644 index 96d321a3..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/how-ubi-is-minted.md +++ /dev/null @@ -1,68 +0,0 @@ -# How UBI is minted - -This document aligns agent explanations with [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and GoodDocs component pages; **contract addresses** come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). - -## Monetary creation (protocol level) - -- New G$ is created in connection with reserve mechanics: purchases into the reserve and reserve-side parameters (including reserve ratio) influence how much G$ can be issued while maintaining backing (see sustainability and issuance sections in GoodDocs). -- Selling G$ back to the reserve burns supply in that model. -- G$ that the protocol creates is allocated across UBI, savings incentives, treasury, and ecosystem uses per the distribution section of GoodDocs. - -## Where G$ is actually created - -Creation happens at the G$ token `mint(...)` call site, not inside `UBIScheme.claim()`. - -Current implementation uses the Mento-core expansion path: - -- `GoodDollarExpansionController` mints and routes to distribution helper. - -**DistributionHelper recipients** decide how much eventually lands in the UBI pool. - -## Claim vs reserve minting (important distinction) - -- **Reserve path:** Buying G$ through the reserve-backed AMM (or related Mento rails) is where mint and burn tied to the reserve model most directly apply at the token level. -- **Daily UBI `claim()`:** On `UBISchemeV2`, a successful claim typically **transfers G$ from the scheme contract’s balance** to the user (`token.transfer` in `_transferTokens`). The scheme may be **refilled** from the DAO avatar via internal `_withdrawFromDao` when configured, not necessarily minting in the same transaction as `claim()`. So describe user-facing UBI as **receipt from the UBI scheme balance**; reserve **minting** is the macro story, **transfer** is the usual claim-time mechanism. - -## Mento-core expansion flow (detailed) - -This is the detailed path for modern reserve-ratio-aware expansion. - -1. A caller triggers `mintUBIFromExpansion(exchangeId)` on `GoodDollarExpansionController`. -2. Expansion is time-gated by config (`expansionFrequency`, `lastExpansion`), so it does not run every block. -3. Controller computes a reserve-ratio scalar (effectively compounding `(1 - expansionRate)` for elapsed periods). -4. Controller calls `GoodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar)`. -5. Exchange provider updates exchange state (including reserve-ratio math) and returns `amountToMint`. -6. Controller mints G$ to `distributionHelper`. -7. Controller triggers distribution (`onDistribution`) so recipients (including UBIScheme when configured) receive allocation. - -Also in `GoodDollarExpansionController`: - -- `mintUBIFromInterest(exchangeId, reserveInterest)` -- `mintUBIFromReserveBalance(exchangeId)` - -These are additional funding paths that mint to distribution helper as part of reserve-driven policy. - -## Reserve ratio, expansion, and risk - -- Practical reserve-ratio intuition: collateral backing strength per unit of G$ supply. -- Lower reserve ratio means weaker backing and higher risk when adding new supply. -- Expansion uses reserve-ratio-aware math instead of blind fixed minting, but aggressive params can still increase sell pressure. -- Key policy levers are expansion rate and expansion frequency (plus caller cadence/automation quality). - -## What claimers experience - -- Verified users receive daily UBI from a pool split among those who claim in each period (GoodDocs). -- The user-facing transaction is a UBIScheme-style `claim` on chains where it is deployed; ABI and version follow your deployment. - -## On-chain components (typical) - -- Identity system for verification and whitelist roots. -- UBIScheme (or successor) for entitlement and claim execution. -- G$ token: value reaches users via **transfer** from scheme balance and/or broader minting economics from the reserve side depending on which action you analyze. -- DistributionHelper: bridge layer between mint source and recipient buckets (UBI, others). - -## Agent guidance - -- Use GoodDocs for macro issuance and allocation; use UBIScheme + token transfer behavior for **claim** explanations. -- Use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for contract addresses instead of guessing. -- When users ask "why no UBI funding today", check whether mint functions were executed, then verify DistributionHelper recipient config and UBIScheme balance. diff --git a/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md b/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md deleted file mode 100644 index 267fa744..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/inviter-invitee-reward-model.md +++ /dev/null @@ -1,58 +0,0 @@ -# Inviter and invitee reward model - -This explains why invite rewards sometimes work and sometimes fail, in user-facing terms. - -Invite rewards are handled by `InvitesV2`, but eligibility depends heavily on the identity system (`Identity`, often `IdentityV4`). In practice, most confusing cases are caused by whitelist or reverification state, not by the invite contract itself. - -## How the reward model works - -There are two separate moments: - -- `join`: the invitee registers with an invite code. -- bounty payout: the contract later checks if this invitee is eligible and pays inviter and invitee when rules pass. - -So joining does not guarantee immediate payout. Payout depends on current eligibility at claim time. - -## Why whitelist status is the main gate - -For bounty eligibility, `InvitesV2` checks whitelist state through Identity. - -The important behavior is: - -- A user can still have status `1` in identity storage but fail `isWhitelisted(...)` if reverification is due. -- When reverification is due, bounty checks fail until an admin refreshes authentication(Face Verification). -- Connected-wallet setups can still fail if the specific address used in invite flow does not pass the whitelist check expected by the contract path. - -## Why reverification blocks rewards - -Reverification cadence is defined in `IdentityV4` with day-based options (`reverifyDaysOptions`) and per-user progression (`authCount`). - -When too many days pass since the last authentication for that user’s current step: - -- `shouldReverify(...)` becomes true -- `isWhitelisted(...)` becomes false for bounty gating -- `canCollectBountyFor(...)` fails until authentication is refreshed - -This is why teams may see users who were once valid but are currently not eligible for invite rewards. - -## Common reasons a bounty is not paid - -- Invitee or inviter is not currently whitelisted. -- Reverification is due for invitee or inviter. -- `minimumClaims` or `minimumDays` thresholds are not met yet. -- Bounty was already paid or was zero at join time. -- Contract is inactive, or identity-chain checks do not match the active chain. -- Invite code or join state is invalid (duplicate code, self-invite, already joined). - -## What to measure for analytics - -- Historical pass: account was authenticated at least once (`lastAuthenticated > 0`). -- Current eligibility: account is currently whitelist-valid (`isWhitelisted`/non-zero root, depending on query design). - -Do not treat these as the same metric. Historical pass explains past onboarding success; current eligibility explains current payout success. - -## Contract sources - -- Invite contract: [`InvitesV2.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/invite/InvitesV2.sol) -- Identity contract: [`IdentityV4.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/identity/IdentityV4.sol) -- Deployment addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) diff --git a/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md b/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md deleted file mode 100644 index e46db2a8..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/mento-reserve-economics.md +++ /dev/null @@ -1,27 +0,0 @@ -# Mento and reserve economics - -This note ties **macro G$ economics** (GoodDocs) to **Mento trading surfaces** (broker, reserve, expansion) that agents integrate on-chain. - -## Protocol-level story (GoodDocs) - -- G$ is **reserve-backed**; issuance and price discovery follow an **augmented bonding curve** (Bancor-style dynamics) described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). -- **Buying** from the reserve side increases reserve assets and can support **new G$ supply** within reserve rules; **selling back** burns G$ and returns collateral, with parameters governed by **GoodDAO**. -- Distribution of newly created G$ spans UBI, savings incentives, treasury, and ecosystem allocations (same doc), but execution timing depends on the mint path and distribution trigger used by the deployment flow. - -## User-facing buy and sell (historical vs current) - -- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) describes reserve interaction, fees (including exit contribution on some paths), and older explorer flows (GoodMarketMaker / exchangeHelper on Ethereum testnets). Treat that page as **product narrative**; **live contract addresses** must come only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). - -## Mento stack (agent integration) - -On networks where GoodDollar uses Mento (see `MentoBroker` and related keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) for your environment): - -- **`IBroker`** (implementation in [mento-core `Broker.sol`](https://github.com/mento-org/mento-core)) is the usual **swap entrypoint**: `getAmountIn` / `getAmountOut`, `swapIn` (exact in), `swapOut` (exact out), plus **trading limits** state per exchange id and token. -- **Reserve** holds collateral; **exchange provider** contracts price trades against the reserve; **GoodDollarExpansionController** and related interfaces in GoodProtocol’s [`MentoInterfaces.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/MentoInterfaces.sol) describe expansion and avatar wiring for governance-facing changes. -- Agents should not hardcode **exchangeId** or provider addresses: read them from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or discover via `getExchangeProviders()` after confirming the broker address for the chain from that file. - -## Agent guidance - -1. Explain **macro** supply and reserve behavior with GoodDocs language. -2. Execute **swaps** with broker quotes, slippage bounds, and allowances per `references/guides/swap.md`. -3. On revert, distinguish **slippage / limit** failures (broker) from **reserve liquidity** messages (see `MentoBroker.abi.yaml` error map and mento-core source). diff --git a/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md b/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md deleted file mode 100644 index b59e5c92..00000000 --- a/.agents/skills/gooddollar/references/deep-researches/on-off-ramp-service.md +++ /dev/null @@ -1,69 +0,0 @@ -# On- and off-ramp service via stable token swap - -Use this note for the service pattern where ramp providers do not list G$ directly. The practical path is: ramp in/out with a listed stable token (for example cUSD), then swap between stable and G$ on-chain. - -## Why this is required - -- Most on-/off-ramp providers list mainstream stable tokens, not G$. -- Service needs a bridge asset for fiat rails. -- Stable token becomes the integration point with ramp providers, while G$ remains the in-app asset. - -## On-chain source of truth - -- Solidity: [`contracts/utils/BuyGDClone.sol`](https://github.com/GoodDollar/GoodProtocol/blob/master/contracts/utils/BuyGDClone.sol) -- Components: - - `BuyGDCloneFactory` - - `BuyGDCloneV2` - - `DonateGDClone` -- Deployments: [releases/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) - -## Service architecture - -The factory deploys EIP-1167 minimal clones and wires swap infrastructure (router, oracle, quoter, optional Mento broker configuration, G$ token, stable token). - -Swap execution is **dual-path**: - -- **Uniswap-style route** (router/quoter path) -- **Mento-based route** (broker/exchange-provider path) - -For each swap request, the service compares quoted outputs and selects the route with the **larger `amountOut`** (best execution for the same input amount), then enforces `minAmount` guard on the selected path. - -Each user gets a deterministic clone address from owner-based salt: - -- `predict(owner)` for buy clone -- `predictDonation(owner, donor)` for donation clone - -This is why clone-per-user design is used: - -- predictable per-user addresses for audit and routing -- isolated execution context -- cheaper deployment than full contract instances - -## Execution surface (conceptual) - -The operational surface is intentionally small and deterministic: - -- `create(owner)` -- `createAndSwap(owner, minAmount)` -- `predict(owner)` -- `createDonation`, `createDonationAndSwap`, `predictDonation` - -This keeps on-/off-ramp architecture auditable and predictable across users. - -## Risks - -- Wrong factory or wrong chain causes permanent fund loss risk. -- Stale router/oracle/mento config can fail swap or produce bad execution. -- Missing `minAmount` protection increases slippage risk. -- Quote source mismatch or stale quotes across Uniswap/Mento can pick a suboptimal route if not refreshed just before execution. - -## Boundary note - -This file explains **why** this architecture exists for ramp services and why per-user clones matter. -For step-by-step service execution flow, use `references/guides/on-off-ramp.md`. - -## Cross-reference - -- User narrative: [Buy and Sell G$](https://docs.gooddollar.org/user-guides) -- Token integration details: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) -- Broker ABI: `references/contracts/MentoBroker.abi.yaml` diff --git a/.agents/skills/gooddollar/references/guides/bridge.md b/.agents/skills/gooddollar/references/guides/bridge.md deleted file mode 100644 index 71fd6e8c..00000000 --- a/.agents/skills/gooddollar/references/guides/bridge.md +++ /dev/null @@ -1,187 +0,0 @@ -# Bridge guide - -Use for moving G$ across supported networks with deployment-specific bridge contracts. - -Primary local ABI reference for MessagePassingBridge flow: - -- `references/contracts/MessagePassingBridge.abi.yaml` - -## GoodDocs alignment - -- User flow and high-level behavior: [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars). -- Resolve supported bridge contract addresses per chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example `MpbBridge` under `production`, `production-celo`, `production-xdc`). Use [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) for user-facing flow and troubleshooting, not for addresses. - -## Goal - -Bridge with deterministic pre-checks: bridge support, allowance, amount, cross-chain transport fee, and delivered G$ after destination **amount** limits and protocol fee. - -## Required inputs - -- source and destination chain metadata -- bridge contract address for source chain -- source G$ token address -- amount in source token decimals -- signer and rpc url - -## Execution flow - -1. Resolve source bridge and token addresses for the network pair. -2. Run bridge eligibility checks for sender and amount via `canBridge(from, amount)` on the **source** bridge (same contract you call for `bridgeToWithLz` / `bridgeToWithAxelar`). Outbound burn does not invoke `canBridge` inside `_bridgeTo`; destination mint still enforces **amount** limits (see **Bridge amount limit context**). -3. Read allowance and approve bridge spender when required. -4. Resolve transport mode (`LZ` or `AXELAR`) and estimate required native fee. -5. Send bridge transaction with nonzero `msg.value` and explicit transport method. -6. Return tx hash and normalized bridge parameters. - -## Bridge fee context - -Two different costs show up on `MessagePassingBridge`; do not conflate them. - -**1. Cross-chain transport fee (native gas token on the source chain)** -Paid as **`msg.value`** on the outbound call. The contract reverts with **`MISSING_FEE`** if `msg.value` is zero. On the **LayerZero** path the contract compares your `msg.value` to **`estimateSendFee`** and reverts **`LZ_FEE(required, sent)`** if it is too low. Use the same **normalized** amount for `estimateSendFee` as the contract uses internally (see the next section). On the **Axelar** path you still attach native value for Gas Service / execution; there is no single `estimateSendFee` analogue in the snippet—follow Envio/Axelar docs or simulate the exact call for production amounts. - -**2. Protocol fee on minted G$ (destination chain, basis points)** -When the message is executed on the **destination** chain, the bridge applies **`bridgeFees`** (min / max / fee bps via `setBridgeFees`) and mints the recipient **minus** that fee; the fee portion is minted to **`feeRecipient`** when it is non-zero (see `bridgeFees()`, `feeRecipient`, and `_takeFee` / `ExecutedTransfer` in `references/contracts/MessagePassingBridge.abi.yaml`). This is **not** the LayerZero relayer fee; it is a separate cut on the **token amount** delivered on arrival. - -**3. Optional OFT / LayerZero token-adapter path** -If the flow uses the GoodDollar OFT-style adapter instead of `MessagePassingBridge`, fee quoting follows **`quoteSend`** / **`MessagingFee`** on that contract; see `references/contracts/GoodDollarOFTAdapter.abi.yaml`. - -## Bridge amount limit context - -**Bridge limit** means **bridge amount limit**: policy on **how much G$** (token volume) may move—**`minAmount`**, per-transfer cap, per-account daily cap, and aggregate daily cap—plus **`onlyWhitelisted`**. It does **not** mean the cross-chain **native** transport fee (`msg.value`, **`LZ_FEE`**), and it does **not** mean the **destination mint fee** in **`bridgeFees`** (bps); those are covered under **Bridge fee context**. - -Amount caps and counters are **per bridge deployment**: read the **source** contract for outbound **amount** policy and usage meters tied to the burn, and the **destination** contract for **`_enforceLimits`** at inbound mint completion; do not assume identical **`bridgeLimits`** across chains. - -**1. Amount caps and usage meters** -**`bridgeLimits()`** exposes **`dailyLimit`**, **`txLimit`**, **`accountDailyLimit`**, **`minAmount`**, and **`onlyWhitelisted`**. Compare those caps to **`bridgeDailyLimit()`** (aggregate **`bridged24Hours`** and **`lastTransferReset`**) and **`accountsDailyLimit(account)`** (same fields per sender). Updates use **`setBridgeLimits`** (access per the ABI). Field-level notes and accessors live in `references/contracts/MessagePassingBridge.abi.yaml`. - -**2. Source preflight vs destination enforcement** -**`canBridge(from, amount)`** on the **source** is a view-only diagnostic for **that amount**: same policy family as amount limit checks, evaluated on the **raw** burn size (not the LayerZero fee normalization). Outbound **`_bridgeTo`** does **not** call **`canBridge`**; call it from the client if you want **`(false, reason)`** before signing instead of learning only from a revert after burn setup. When the message is executed on the **destination**, **`_enforceLimits`** is the hard gate for **amount** throttles and whitelist behavior at mint time. - -## Outbound pause, approved requests, and inbound source bridges - -These controls are separate from numeric **amount** caps; they still block or relax bridging and can surface as **`BRIDGE_LIMITS`** or inbound skips. - -**`pauseBridge`** sets **`isClosed`**; when closed, outbound flow reverts with **`BRIDGE_LIMITS('closed')`**. **`approvedRequests(requestId)`** on the destination lets **`_bridgeFrom`** skip standard **amount** limit enforcement for that completion when set. **`setDisabledBridges`** toggles **`disabledSourceBridges`** entries keyed by **`keccak256(abi.encode(sourceChainId, BridgeService))`**, controlling whether an inbound relay from that source is accepted before the rest of destination handling. See `references/contracts/MessagePassingBridge.abi.yaml`. - -## Axelar vs LayerZero on GoodDollar deployments - -LayerZero mappings are initialized for Ethereum, Celo, Fuse, and XDC in `initialize` / `upgrade` on `MessagePassingBridge`. The **Axelar** path is only usable where `toAxelarChainId(targetChainId)` returns a non-empty string; the on-chain pure function currently maps **1**, **5**, **42220**, and **44787** only. For **Fuse (122)** or **XDC (50)** targets, use **LZ** (`bridgeToWithLz`) unless governance ships a broader Axelar mapping. - -## LayerZero fee and `estimateSendFee` - -`bridgeToWithLz` burns the G$ **raw** amount in the source token’s `decimals()`. Inside the contract, LayerZero payload and `estimateSendFee` use a value normalized to **18 decimals** the same way as [GoodBridge `BridgeHelperLibrary.normalizeFromTokenTo18Decimals`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol): if `decimals < 18`, multiply by `10^(18 - decimals)`; if `decimals > 18`, divide by `10^(decimals - 18)`; otherwise use the raw amount. - -`canBridge(from, amount)` is evaluated on the **raw** burn amount, not the normalized value. - -Read `decimals()` from the source G$ contract when building off-chain fee quotes so you stay aligned if a deployment differs. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -function normalizedForLzFee(raw, tokenDecimals) { - if (tokenDecimals < 18) return raw * 10n ** BigInt(18 - tokenDecimals); - if (tokenDecimals > 18) return raw / 10n ** BigInt(tokenDecimals - 18); - return raw; -} - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const token = new ethers.Contract( - process.env.GOODDOLLAR_ADDRESS, - [ - "function decimals() view returns (uint8)", - "function allowance(address,address) view returns (uint256)", - "function approve(address,uint256) returns (bool)", - ], - signer, -); - -const bridge = new ethers.Contract( - process.env.BRIDGE_ADDRESS, - [ - "function canBridge(address,uint256) view returns (bool,string)", - "function toLzChainId(uint256) view returns (uint16)", - "function estimateSendFee(uint16,address,address,uint256,bool,bytes) view returns (uint256,uint256)", - "function bridgeToWithLz(address,uint256,uint256,bytes) payable", - "function bridgeToWithAxelar(address,uint256,uint256,address) payable", - ], - signer, -); - -const owner = await signer.getAddress(); -const targetChainId = Number(process.env.TARGET_CHAIN_ID); -const recipient = process.env.RECIPIENT; -const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); -const transport = (process.env.BRIDGE_TRANSPORT || "LZ").toUpperCase(); -const tokenDecimals = await token.decimals(); -const normalizedForLzEstimate = normalizedForLzFee(amount, Number(tokenDecimals)); - -const [canBridge, reason] = await bridge.canBridge(owner, amount); -if (!canBridge) throw new Error(`Bridge blocked: ${reason}`); - -const allowance = await token.allowance(owner, process.env.BRIDGE_ADDRESS); -if (allowance < amount) { - const approveTx = await token.approve(process.env.BRIDGE_ADDRESS, amount); - await approveTx.wait(); -} - -let tx; - -if (transport === "LZ") { - const dstEid = await bridge.toLzChainId(targetChainId); - if (dstEid === 0) throw new Error("Unsupported target chain for LayerZero"); - - const adapterParams = process.env.LZ_ADAPTER_PARAMS || "0x"; - const [nativeFee] = await bridge.estimateSendFee( - dstEid, - owner, - recipient, - normalizedForLzEstimate, - false, - adapterParams, - ); - if (nativeFee <= 0n) throw new Error("Estimated LayerZero fee is zero"); - - tx = await bridge.bridgeToWithLz(recipient, targetChainId, amount, adapterParams, { - value: nativeFee, - }); -} else if (transport === "AXELAR") { - const nativeFee = ethers.parseEther(process.env.AXELAR_FEE_ETH || "0.01"); - tx = await bridge.bridgeToWithAxelar(recipient, targetChainId, amount, owner, { - value: nativeFee, - }); -} else { - throw new Error("Unsupported BRIDGE_TRANSPORT. Use LZ or AXELAR"); -} - -const receipt = await tx.wait(); -console.log( - JSON.stringify( - { - txHash: receipt.hash, - sourceBridge: process.env.BRIDGE_ADDRESS, - targetChainId, - transport, - recipient, - rawAmount: amount.toString(), - tokenDecimals: Number(tokenDecimals), - normalizedAmountForLz: normalizedForLzEstimate.toString(), - }, - null, - 2, - ), -); -``` - -## Failure handling - -- unsupported destination: return targetChainId, bridge address, and transport mode -- fee too low (`LZ_FEE` or underpriced Axelar fee): re-estimate and retry with user confirmation -- approval or balance issue: return required delta -- credited G$ on destination is reduced by **`bridgeFees`** (bps / min / max); that is independent of the source **`msg.value`** transport fee -- **`canBridge`** false on source: return the **`reason`** string from the view call -- **`BRIDGE_LIMITS(reason)`** custom error (see `references/contracts/MessagePassingBridge.abi.yaml` **errors**): **`reason`** labels the failing check (numeric **amount** limit, whitelist, **`closed`**, or other policy string from the implementation) -- source preflight passed but destination still reverts: re-read **`bridgeLimits`** and daily counters for **amount** caps and **`onlyWhitelisted`**; check **`isClosed`**, **`approvedRequests`**, and **`disabledSourceBridges`** per **Outbound pause, approved requests, and inbound source bridges**; message delivery can cross a reset boundary or policy change diff --git a/.agents/skills/gooddollar/references/guides/check-identity.md b/.agents/skills/gooddollar/references/guides/check-identity.md deleted file mode 100644 index adc01659..00000000 --- a/.agents/skills/gooddollar/references/guides/check-identity.md +++ /dev/null @@ -1,87 +0,0 @@ -# Check identity guide - -Use when the user asks whether an address is eligible for UBI or how identity links wallets. - -## GoodDocs alignment - -- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity): associated addresses resolve to a verified root in the Identity contract; `connectAccount` links wallets. -- One claim per day applies across all connected addresses for the same verified identity (see the hint on that page). - -## Goal - -Determine whitelist or authentication status with deterministic on-chain reads. - -## Metric semantics - -- **Passed whitelisting (historical):** use `lastAuthenticated(account) > 0`. -- **Still whitelisted (current):** use `getWhitelistedRoot(account) != 0x0` or `isWhitelisted(account) == true`. -- `getWhitelistedRoot(account) != 0x0` is a current-state signal, not an "ever passed" signal. - -## Required inputs - -- `nameServiceAddress` or explicit Identity address -- `account` to check -- `rpcUrl` and chain configuration - -## Execution flow - -1. Resolve `IDENTITY` from NameService when used on the deployment. -2. Read `getWhitelistedRoot(account)` or equivalent for the deployed Identity version. -3. Treat non-zero root as tied to a whitelisted identity tree when that is the protocol rule for the deployment. -4. Read `lastAuthenticated(account)` for historical pass status and `getWhitelistedRoot(account)` or `isWhitelisted(account)` for current status. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); - -const nameService = new ethers.Contract( - process.env.NAMESERVICE_ADDRESS, - ["function getAddress(string) view returns (address)"], - provider, -); - -const identityAddress = await nameService.getAddress("IDENTITY"); -const identity = new ethers.Contract( - identityAddress, - [ - "function getWhitelistedRoot(address) view returns (address)", - "function isWhitelisted(address) view returns (bool)", - "function lastAuthenticated(address) view returns (uint256)", - ], - provider, -); - -const account = process.env.ACCOUNT; -const root = await identity.getWhitelistedRoot(account); -const isWhitelisted = await identity.isWhitelisted(account); -const lastAuthenticated = await identity.lastAuthenticated(account); - -console.log( - JSON.stringify( - { - account, - identityAddress, - whitelistedRoot: root, - isWhitelisted, - lastAuthenticated: lastAuthenticated.toString(), - }, - null, - 2, - ), -); -``` - -## Return shape - -- `isWhitelisted` or equivalent boolean summary -- `whitelistedRoot` or equivalent -- `lastAuthenticated` for historical-pass classification -- optional metadata fields when available - -## Failure handling - -- NameService cannot resolve `IDENTITY`: stop and fix inputs using [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`Identity` / `NameService` for the target environment)—not GoodDocs tables. -- Read failures: return the failing call and next step. diff --git a/.agents/skills/gooddollar/references/guides/claim.md b/.agents/skills/gooddollar/references/guides/claim.md deleted file mode 100644 index bd51abd0..00000000 --- a/.agents/skills/gooddollar/references/guides/claim.md +++ /dev/null @@ -1,83 +0,0 @@ -# Claim guide - -Use when the user wants to claim daily UBI. Protocol context: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [UBIScheme (GoodDocs behavior)](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme)—contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). - -## Goal - -Execute a safe `claim()` with identity pre-checks and clear outputs. - -## GoodDocs alignment - -- UBI is distributed daily to verified users; the active pool is split among claimers in each period (see [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works)). -- UBIScheme deployments vary by chain (Fuse, Celo, XDC); resolve live contract addresses only from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`UBIScheme` under `production`, `production-celo`, or `production-xdc`). Use [Core contracts / UBIScheme](https://docs.gooddollar.org/for-developers/core-contracts/ubischeme) for documented behavior, not for addresses. - -## Required inputs - -- `nameServiceAddress` or explicit UBIScheme and Identity addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) -- `rpcUrl` and chain configuration -- signer context - -## Execution flow - -1. Resolve `IDENTITY` and `UBISCHEME` from NameService when NameService is the source of truth for the deployment. -2. Confirm whitelist status for the claiming account. -3. Optionally read entitlement or claimable state before sending `claim()`. -4. Call `claim()` on the resolved UBIScheme (contract generation may differ by deployment; align ABI with your target). -5. Return tx hash and claimed amount when derivable from events or balance delta. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const nameService = new ethers.Contract( - process.env.NAMESERVICE_ADDRESS, - ["function getAddress(string) view returns (address)"], - provider, -); - -const identityAddress = await nameService.getAddress("IDENTITY"); -const ubiAddress = await nameService.getAddress("UBISCHEME"); - -const identity = new ethers.Contract( - identityAddress, - [ - "function isWhitelisted(address) view returns (bool)", - "function getWhitelistedRoot(address) view returns (address)", - ], - provider, -); - -const account = await signer.getAddress(); -const isWhitelisted = await identity.isWhitelisted(account); -if (!isWhitelisted) throw new Error("Account is not whitelisted"); - -const root = await identity.getWhitelistedRoot(account); -if (root === ethers.ZeroAddress) throw new Error("No whitelisted root"); - -const ubi = new ethers.Contract( - ubiAddress, - ["function claim()", "event UBICalculated(address,uint256,uint256,uint256)"], - signer, -); - -const tx = await ubi.claim(); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, account, root }, null, 2)); -``` - -## Pre-check failures - -- Not whitelisted: stop and point the user to identity verification flows in GoodDocs. -- Missing contract address: stop; use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. -- Zero entitlement: communicate that nothing is claimable in the current period without guessing amounts. - -## Output contract - -- network -- resolved contract addresses -- tx hash -- claim outcome details when available diff --git a/.agents/skills/gooddollar/references/guides/faucet.md b/.agents/skills/gooddollar/references/guides/faucet.md deleted file mode 100644 index d224e85e..00000000 --- a/.agents/skills/gooddollar/references/guides/faucet.md +++ /dev/null @@ -1,76 +0,0 @@ -# Faucet top-up guide - -Use when the user needs native gas top-up via GoodProtocol Faucet. - -## Goal - -Run deterministic pre-checks and call `topWallet` only when eligibility and limits pass. - -## Required inputs - -- `rpcUrl`, chain configuration, signer -- Faucet address for the chain (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) `Faucet` under the matching environment, or `NameService.getAddress` when the deployment documents the key) -- target user address - -## Execution flow - -1. Resolve Faucet contract address for the active chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json). -2. Run `canTop(user)` as preflight. -3. Read `getToppingAmount(user)` and communicate expected top-up. -4. If eligible, call `topWallet(user)`. -5. Return tx hash and resulting top-up context. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const faucet = new ethers.Contract( - process.env.FAUCET_ADDRESS, - [ - "function canTop(address) view returns (bool)", - "function getToppingAmount(address) view returns (uint256)", - "function topWallet(address payable)", - ], - signer, -); - -const user = process.env.USER; -const canTop = await faucet.canTop(user); -if (!canTop) throw new Error("Faucet canTop returned false"); - -const amount = await faucet.getToppingAmount(user); -const tx = await faucet.topWallet(user); -const receipt = await tx.wait(); - -console.log( - JSON.stringify( - { - txHash: receipt.hash, - user, - toppingAmount: amount.toString(), - }, - null, - 2, - ), -); -``` - -## Common rejection reasons - -- `not authorized` -- daily or weekly cap reached -- banned address -- low effective `toTop` vs minimum threshold -- faucet inactive or wrong chain/address - -## Output contract - -- network -- faucet address -- `canTop` preflight result -- top-up amount estimate -- tx hash (when executed) diff --git a/.agents/skills/gooddollar/references/guides/gooddocs.md b/.agents/skills/gooddollar/references/guides/gooddocs.md deleted file mode 100644 index 5e5b4b39..00000000 --- a/.agents/skills/gooddollar/references/guides/gooddocs.md +++ /dev/null @@ -1,37 +0,0 @@ -# GoodDocs hub - -Canonical protocol documentation lives at [GoodDocs](https://docs.gooddollar.org/). - -This is a **routing guide** (quick link map), not a deep protocol analysis note. - -## Start here - -- [Welcome](https://docs.gooddollar.org/) -- [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) - -## User guides - -- [Buy and Sell G$](https://docs.gooddollar.org/user-guides) (reserve-backed buy/sell; includes historical Ethereum/Kovan explorer workflows in the doc) -- [Bridge GoodDollars](https://docs.gooddollar.org/user-guides/bridge-gooddollars) (MessagePassingBridge, fees, limits, troubleshooting) -- [Connect another wallet address to identity](https://docs.gooddollar.org/user-guides/connect-another-wallet-address-to-identity) - -## Developers - -- [Core contracts](https://docs.gooddollar.org/for-developers/core-contracts) (module overview; **do not** read contract addresses from GoodDocs—use [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only) -- [Developer guides index](https://docs.gooddollar.org/for-developers/developer-guides) -- [Integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (ERC-677, ERC-777, decimals by chain, `transferAndCall`, fees) -- [Use G$ streaming](https://docs.gooddollar.org/for-developers/developer-guides/use-gusd-streaming) (Superfluid on Celo, CFAv1Forwarder) - -## Chain IDs (bridge doc) - -| Network | Chain ID | -| -------- | -------- | -| Ethereum | 1 | -| Fuse | 122 | -| Celo | 42220 | -| XDC | 50 | - -## This repo - -- Action playbooks: `references/guides/*.md`. -- Rich ABIs: `references/contracts/*.abi.yaml`. diff --git a/.agents/skills/gooddollar/references/guides/goodsdks.md b/.agents/skills/gooddollar/references/guides/goodsdks.md deleted file mode 100644 index 8092e407..00000000 --- a/.agents/skills/gooddollar/references/guides/goodsdks.md +++ /dev/null @@ -1,110 +0,0 @@ -# GoodSDKs integration guide - -Use this guide when the task is SDK-first (app integration), not raw contract-first. - -## Scope - -GoodSDKs is the app integration layer for GoodDollar: - -- `@goodsdks/citizen-sdk` for identity and claim flows. -- `@goodsdks/react-hooks` for Wagmi React hooks. -- `@goodsdks/good-reserve` for reserve buy or sell flows. -- `@goodsdks/engagement-sdk` for engagement rewards flows. -- `@goodsdks/ui-components` and `@goodsdks/savings-widget` for web components. - -## Routing map - -- Check whitelist, identity root, FV link -> `@goodsdks/citizen-sdk` (`IdentitySDK`) -- Claim UBI with entitlement checks and fallback chains -> `@goodsdks/citizen-sdk` (`ClaimSDK`) -- React app with Wagmi and minimal glue code -> `@goodsdks/react-hooks` -- Buy or sell via reserve rails (Celo or XDC support rules) -> `@goodsdks/good-reserve` -- Reward app registration, claims, reward history -> `@goodsdks/engagement-sdk` -- Embeddable UI in non-React or mixed stacks -> `@goodsdks/ui-components` or `@goodsdks/savings-widget` - -## Deterministic setup - -Monorepo prerequisites: - -```bash -cd ~/Projects/GoodSDKs -corepack enable -yarn install --immutable -yarn build -``` - -Target one workspace: - -```bash -yarn workspace @goodsdks/citizen-sdk build -yarn workspace @goodsdks/react-hooks build -``` - -## Deterministic usage snippets - -Identity SDK: - -```ts -import { createPublicClient, createWalletClient, custom, http } from "viem"; -import { IdentitySDK } from "@goodsdks/citizen-sdk"; - -const publicClient = createPublicClient({ transport: http("https://forno.celo.org") }); -const walletClient = createWalletClient({ transport: custom(window.ethereum) }); - -const identitySDK = await IdentitySDK.init({ - publicClient, - walletClient, - env: "production", -}); - -const { isWhitelisted, root } = await identitySDK.getWhitelistedRoot("0xYourAccount"); -console.log({ isWhitelisted, root }); -``` - -Claim SDK: - -```ts -import { ClaimSDK, IdentitySDK } from "@goodsdks/citizen-sdk"; - -const identitySDK = await IdentitySDK.init({ publicClient, walletClient, env: "production" }); -const claimSDK = await ClaimSDK.init({ - publicClient, - walletClient, - identitySDK, - env: "production", -}); - -const entitlement = await claimSDK.checkEntitlement(); -if (entitlement.amount > 0n) { - const receipt = await claimSDK.claim(); - console.log(receipt.transactionHash); -} -``` - -React hooks: - -```tsx -import { useIdentitySDK, useClaimSDK, useGoodReserve } from "@goodsdks/react-hooks"; - -const identity = useIdentitySDK("production"); -const claim = useClaimSDK("production"); -const reserve = useGoodReserve("production"); -``` - -Reserve SDK: - -```ts -import { GoodReserveSDK } from "@goodsdks/good-reserve"; - -const sdk = new GoodReserveSDK(publicClient, walletClient, "production"); -const quote = await sdk.getBuyQuote(CUSD_ADDRESS, amountIn); -const tx = await sdk.buy(CUSD_ADDRESS, amountIn, (quote * 95n) / 100n); -console.log(tx.hash); -``` - -## Agent rules - -1. Prefer SDK methods first for app tasks. -2. Use contract-level guides only when SDK does not expose required behavior. -3. Do not invent SDK method names; align with package READMEs and exported types. -4. For chain support errors, report chain and env explicitly (do not silently fallback). -5. For UI tasks, prefer hooks or components over bespoke wallet and viem plumbing. diff --git a/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md b/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md deleted file mode 100644 index e7b6f286..00000000 --- a/.agents/skills/gooddollar/references/guides/hypersync-hyperrpc.md +++ /dev/null @@ -1,110 +0,0 @@ -# Envio HyperSync and HyperRPC - -Use this guide when the task is high-volume historical blockchain data fetch (events, blocks, txs), especially analytics and indexing workflows. - -## Official docs - -- HyperSync overview: [docs.envio.dev/docs/HyperSync/overview](https://docs.envio.dev/docs/HyperSync/overview) -- HyperRPC overview: [docs.envio.dev/docs/HyperRPC/overview-hyperrpc](https://docs.envio.dev/docs/HyperRPC/overview-hyperrpc) -- HyperRPC supported networks: [docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks](https://docs.envio.dev/docs/HyperRPC/hyperrpc-supported-networks) - -## What to use - -- **HyperSync**: preferred for new data pipelines and heavy historical scans. -- **HyperRPC**: read-only JSON-RPC drop-in for existing RPC code paths. - -## HyperRPC vs HyperSync (avoid mixing them up) - -- **HyperRPC** is a **hosted JSON-RPC URL** (same methods as `eth_getLogs`, `eth_blockNumber`, and so on). Any HTTP client or existing RPC stack can call it; put the API token in the URL path as Envio documents. -- **HyperSync** is a **separate high-throughput query API** used through **Envio client libraries** (for example `@envio-dev/hypersync-client` in Node). It is **not** “just another RPC endpoint” with the same ergonomics as a one-line `fetch` to `eth_getLogs` at large scale. - -## Decision rule - -1. For GoodDollar protocol history that exists on subgraphs, query the subgraph first and validate fields in `references/subgraphs/*-guide.md`. -2. If subgraph schema or freshness cannot satisfy the request, use **HyperSync** for large scans and pipelines, or **HyperRPC** when you must stay inside standard JSON-RPC. -3. For write operations (sending tx), use normal RPC providers; HyperRPC is read-only. - -## GoodDollar-relevant network coverage - -- Celo and XDC are supported on HyperRPC. -- Fuse is not currently listed; treat this as non-blocking and use existing providers for Fuse. - -## Access and auth - -- HyperRPC/HyperSync usage is account-based. -- HyperRPC requires an API key for reliable production use. -- Requests without API token are rate-limited and should be treated as non-production fallback only. -- Add API key in endpoint URL as documented by Envio. -- HyperRPC token pattern example from docs: `https://.rpc.hypersync.xyz/` - -## Agents: Envio API token when HyperSync is the best option - -After you decide **HyperSync** is the right tool for the user query (for example large historical scans or pipeline-scale log pulls where subgraphs are insufficient), check for a usable Envio credential in the execution environment (`ENVIO_API_TOKEN` for `@envio-dev/hypersync-client`, or the token Envio documents for your chosen URL pattern). - -If **no** Envio API token is available and you cannot complete the HyperSync path without it, **stop and explicitly ask the user** to provide an Envio API token (name the env var you need, typically `ENVIO_API_TOKEN`). Do not silently rely on anonymous or heavily rate-limited access as a substitute when HyperSync was already identified as the best approach. - -## Practical use in this repo - -- Keep subgraphs as first option for indexed protocol entities. -- Use HyperSync/HyperRPC when subgraph coverage is missing, stale, or insufficient for bulk historical pulls. -- When an agent chooses **HyperSync** as the best path and no Envio API token is available, follow **Agents: Envio API token when HyperSync is the best option** in this file and ask the user for `ENVIO_API_TOKEN` before proceeding. -- Keep contract addresses from [GoodProtocol/deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only; use GoodDocs for product context, not for resolving contract addresses. -- For implementation details (client setup, query structure, supported methods), follow the Envio docs links above directly. - -## From block for historical fetches - -For **`eth_getLogs`**, HyperRPC, and HyperSync range queries, the lower bound is **`fromBlock`** (or the client’s equivalent). Prefer the deployment’s **`creationBlock`** from the matching row in `references/contracts/*.abi.yaml` (or **`meta.deploymentCreationBlocks`** where deployments are plain address strings) so scans do not start at genesis when you only need post-deploy history. Field placement for **`creationBlock`** is defined in `references/contracts/_rich-abi-yaml-format.md`. If you cannot determine the creation block, **`fromBlock` 0** is valid. - -## Prebuilt scripts (developers and local agents) - -These scripts avoid rediscovering HyperRPC wiring on every task. They require **Node.js 18 or newer** (global `fetch`). Paths like `scripts/...` are relative to the **GoodSkills repository root** (the directory that contains both `skills/` and `scripts/`), not relative to `skills/gooddollar/` alone. - -### Last N Identity `WhitelistedAdded` logs via HyperRPC - -- Script: `scripts/fetch-whitelist-events-hyperrpc.mjs` -- Default `EVENT_TOPIC0` matches `WhitelistedAdded(address)` on `IdentityV4`; override `EVENT_TOPIC0` for other events. -- Production Celo defaults: `CONTRACT_ADDRESS` defaults to `Identity` from `production-celo` in [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`0xC361A6E67822a0EDc17D899227dd9FC50BD62F42`). If `HYPERRPC_URL` is unset, the script builds `https://celo.rpc.hypersync.xyz/` from `HYPERRPC_API_TOKEN` or `ENVIO_API_TOKEN`. -- Optional env: `HYPERRPC_URL` (overrides token-based default), `CONTRACT_ADDRESS`, `LIMIT` (default `500`), `STEP` (default `2000`), `FROM_BLOCK` (decimal; omit to read **`creationBlock`** for `CONTRACT_ADDRESS` from `ABI_PATH` or the default `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`), `ABI_PATH`, `TO_BLOCK` (default `latest`). **`fromBlock`** behavior is described in **From block for historical fetches** above. - -```bash -cd /path/to/GoodSkills -export HYPERRPC_API_TOKEN='' -node scripts/fetch-whitelist-events-hyperrpc.mjs -``` - -Web-only assistants without a shell cannot run the file; they should return the same env keys and command text so the user runs it locally. - -## HyperSync client minimal path (install required) - -HyperSync uses the official client. Install and query pattern (Celo example URLs from [Envio Celo docs](https://docs.envio.dev/docs/HyperIndex/celo)): - -```bash -npm install @envio-dev/hypersync-client -export ENVIO_API_TOKEN='' -``` - -Save as a `.mjs` file (or use `"type": "module"` in a local `package.json`) and run with `node`: - -```javascript -import { HypersyncClient, presetQueryLogsOfEvent } from "@envio-dev/hypersync-client"; - -const client = new HypersyncClient({ - url: "https://celo.hypersync.xyz", - apiToken: process.env.ENVIO_API_TOKEN, -}); - -const identity = "0x..."; -const whitelistedAddedTopic0 = - "0xee1504a83b6d4a361f4c1dc78ab59bfa30d6a3b6612c403e86bb01ef2984295f"; - -const fromBlock = 17237952; -const toBlock = await client.getHeight(); - -const query = presetQueryLogsOfEvent(identity, whitelistedAddedTopic0, fromBlock, toBlock); -const res = await client.get(query); -console.log(res.data.logs.length); -``` - -The example **`fromBlock`** matches **`creationBlock`** for production Celo Identity in `skills/gooddollar/references/contracts/IdentityV4.abi.yaml`; see **From block for historical fetches** above. - -Full API and streaming patterns: [HyperSync clients](https://docs.envio.dev/docs/HyperSync/hypersync-clients) and the package README for `@envio-dev/hypersync-client`. diff --git a/.agents/skills/gooddollar/references/guides/invite-bounties.md b/.agents/skills/gooddollar/references/guides/invite-bounties.md deleted file mode 100644 index d60aafdd..00000000 --- a/.agents/skills/gooddollar/references/guides/invite-bounties.md +++ /dev/null @@ -1,115 +0,0 @@ -# Invite bounties guide - -Use when the task is to verify or execute inviter-invitee bounty payout flow and explain why payout is blocked. - -## Goal - -Check eligibility deterministically, execute payout only when eligible, and return exact failure reason when not eligible. - -## Required inputs - -- target chain -- `InvitesV2` address -- `Identity` address -- optional `UBISchemeV2` address when claims threshold is active -- invitee address -- inviter address -- rpc url and signer - -## Execution flow - -1. Resolve contract addresses from `deployment.json`. -2. Read invitee state from `users(invitee)` and global thresholds (`minimumClaims`, `minimumDays`, `active`). -3. Check current eligibility with `canCollectBountyFor(invitee)`. -4. If not eligible, read identity whitelist for invitee and inviter and return concrete blocker. -5. If eligible, execute `bountyFor(invitee)` or `collectBounties()` and return tx hash plus payout values from events. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const invites = new ethers.Contract( - process.env.INVITES_ADDRESS, - [ - "function canCollectBountyFor(address) view returns (bool)", - "function bountyFor(address)", - "function users(address) view returns (bytes32,address,uint40,uint24,bool,uint256)", - "function minimumClaims() view returns (uint256)", - "function minimumDays() view returns (uint256)", - "function active() view returns (bool)", - "event InviterBounty(address indexed inviter,address indexed invitee,uint256 bountyPaid,uint256 inviterLevel,bool earnedLevel)" - ], - signer, -); - -const identity = new ethers.Contract( - process.env.IDENTITY_ADDRESS, - [ - "function isWhitelisted(address) view returns (bool)" - ], - provider, -); - -const invitee = process.env.INVITEE_ADDRESS; -const inviter = process.env.INVITER_ADDRESS; - -const [isActive, eligible, inviteeWhitelisted, inviterWhitelisted, minClaims, minDays] = await Promise.all([ - invites.active(), - invites.canCollectBountyFor(invitee), - identity.isWhitelisted(invitee), - identity.isWhitelisted(inviter), - invites.minimumClaims(), - invites.minimumDays(), -]); - -if (!isActive) throw new Error("Invites contract is inactive"); -if (!eligible) { - throw new Error( - `Not eligible. inviteeWhitelisted=${inviteeWhitelisted} inviterWhitelisted=${inviterWhitelisted} minimumClaims=${minClaims} minimumDays=${minDays}`, - ); -} - -const tx = await invites.bountyFor(invitee); -const receipt = await tx.wait(); -const bountyEvent = receipt.logs - .map((log) => { - try { - return invites.interface.parseLog(log); - } catch { - return null; - } - }) - .find((e) => e && e.name === "InviterBounty"); - -console.log( - JSON.stringify( - { - txHash: receipt.hash, - invitee, - inviter, - bountyPaid: bountyEvent?.args?.bountyPaid?.toString() ?? null, - }, - null, - 2, - ), -); -``` - -## Failure handling - -- invitee or inviter is not currently whitelisted -- reverification is due and whitelist check fails until re-authentication -- minimum claims or minimum days is not met -- bounty already paid or bounty-at-join is zero -- contract inactive or wrong deployment addresses - -## Output contract - -- network and addresses used -- eligibility status and blockers -- tx hash when sent -- payout values when available from logs diff --git a/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md b/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md deleted file mode 100644 index 4ad9fd26..00000000 --- a/.agents/skills/gooddollar/references/guides/migrate-fuse-staking-to-celo-savings.md +++ /dev/null @@ -1,116 +0,0 @@ -# Fuse to CELO staking migration guide - -Use when the user wants to migrate an existing Fuse governance stake into a CELO destination savings flow. In this flow, Fuse `GovernanceStakingV2` is the old staking contract (source) and **`GooddollarSavingsStream`** on Celo is the destination savings contract ([`GooddollarSavingsStream.sol`](https://github.com/Ubeswap/gooddollar-contracts/blob/main/contracts/GooddollarSavingsStream.sol), [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B)). - -## Goal - -Close a user stake on Fuse, bridge the resulting G$ to CELO, and stake on CELO for that user in a controlled backend flow. - -## Required inputs - -- user address on Fuse and corresponding destination address on CELO -- Fuse `GovernanceStakingV2` address (`production.GovernanceStakingV2` in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json)) -- Fuse G$ token address and bridge contract address (from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json): `GoodDollar`, `MpbBridge` under `production`) -- CELO G$ token address (from `production-celo` in the same file) and destination savings contract address -- backend signer or service wallet with required execution permissions -- chain RPC URLs for Fuse and CELO - -## Address resolution quick table - -| Purpose | Network | Source key/path | Value | -|---|---|---|---| -| Governance staking (source close) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStakingV2` | `0xB7C3e738224625289C573c54d402E9Be46205546` | -| Governance staking (previous) | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GovernanceStaking` | `0xFAF457Fb4A978Be059506F6CD41f9B30fCa753b0` | -| Fuse G$ token | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.GoodDollar` | `0x495d133B938596C9984d462F007B676bDc57eCEC` | -| Fuse bridge | Fuse (`production`, `networkId: 122`) | `deployment.json` -> `production.MpbBridge` | `0xa3247276DbCC76Dd7705273f766eB3E8a5ecF4a5` | -| Destination savings | CELO (`networkId: 42220`) | [CeloScan](https://celoscan.io/address/0x059ee811414230d1Fb157878D2b491240F4D8d3B) / `GooddollarSavingsStream` | `0x059ee811414230d1Fb157878D2b491240F4D8d3B` (`process.env.CELO_SAVINGS`) | - -Canonical sources: - -- [GoodProtocol deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) -- [Fuse explorer contract (GovernanceStakingV2 address)](https://explorer.fuse.io/address/0xB7C3e738224625289C573c54d402E9Be46205546?tab=contract) -- [Ubeswap gooddollar-contracts](https://github.com/Ubeswap/gooddollar-contracts) -- [GoodBridge bridge helper normalization](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol) - -## Token decimals and `MessagePassingBridge` LZ fees - -Production deployments checked on-chain: Fuse `GoodDollar` uses **2** decimals and Celo `GoodDollar` uses **18**. Fuse `GovernanceStakingV2` (sG$) uses **2** decimals. Resolve `decimals()` from each live token in your runner so you stay correct if deployments change. - -On the Fuse `MpbBridge` (`MessagePassingBridge`), `canBridge(from, amount)` and `bridgeToWithLz(..., amount, ...)` use the **raw G$ burn amount** in source token decimals. Off-chain **`estimateSendFee`’s `_normalizedAmount` argument** must match what the contract builds internally: **`normalizeFromTokenTo18Decimals(amount, IERC20(nativeToken()).decimals())`** ([`BridgeHelperLibrary`](https://github.com/GoodDollar/GoodBridge/blob/master/packages/bridge-contracts/contracts/messagePassingBridge/BridgeHelperLibrary.sol)). Format displayed amounts per chain (`formatUnits`/UI) using each chain’s G$ decimals. - -## Execution flow - -1. Confirm allowance for whichever asset your flow spends first (for example sG$ allowance to the backend on Fuse `GovernanceStakingV2`, or Fuse G$ allowance if you pull G$ directly), before any transfers or bridges. -2. Verify current stake state on Fuse before closing: - - stake token balance - - withdrawable stake amount - - pending rewards if any -3. Execute Fuse unstake or close flow on governance staking (`withdrawStake` or equivalent full-close path). -4. Compute net G$ available for migration after unstake completion and any reward claim behavior. -5. Bridge G$ from Fuse to CELO using the configured bridge path and track the transfer id or tx hash pair. -6. Wait for destination finalization on CELO and verify credited G$ balance at the backend execution wallet. -7. Approve destination savings contract to spend migrated G$ amount. -8. Stake for the user on CELO with `stakeFor(amount, recipient)` on `GooddollarSavingsStream` (G$ native Super Token; approve the savings contract, not only ERC20 GoodDollar from `deployment.json` if your wallet holds the Super Token). -9. Return a migration result with both chain tx hashes and final CELO staked amount. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const fuse = new ethers.JsonRpcProvider(process.env.FUSE_RPC_URL); -const celo = new ethers.JsonRpcProvider(process.env.CELO_RPC_URL); -const signerFuse = new ethers.Wallet(process.env.BACKEND_PK, fuse); -const signerCelo = new ethers.Wallet(process.env.BACKEND_PK, celo); - -const user = process.env.USER_ADDRESS; -const migrateAmount = BigInt(process.env.MIGRATE_AMOUNT); - -const celoGd = new ethers.Contract( - process.env.CELO_GD_TOKEN, - [ - "function approve(address spender,uint256 amount) returns (bool)", - "function balanceOf(address) view returns (uint256)", - ], - signerCelo, -); - -const savings = new ethers.Contract( - process.env.CELO_SAVINGS, - ["function stakeFor(uint256 amount,address recipient)"], - signerCelo, -); - -const approveTx = await celoGd.approve(process.env.CELO_SAVINGS, migrateAmount); -await approveTx.wait(); - -const stakeTx = await savings.stakeFor(migrateAmount, user); -const receipt = await stakeTx.wait(); - -console.log( - JSON.stringify( - { - user, - celoStakeTx: receipt.hash, - migratedAmount: migrateAmount.toString(), - }, - null, - 2, - ), -); -``` - -## Pre-check failures - -- User allowance missing on Fuse: stop and request allowance tx from user. -- Stake close fails on Fuse: stop and return exact revert reason before bridge. -- Bridge transfer not finalized on CELO: do not call `stakeFor` until destination balance is confirmed. -- CELO savings approval missing or too low: re-approve exact amount before staking. - -## Output contract - -- user address -- Fuse unstake tx hash -- bridge tx hash or transfer identifier -- CELO stake tx hash -- final staked amount on CELO diff --git a/.agents/skills/gooddollar/references/guides/on-off-ramp.md b/.agents/skills/gooddollar/references/guides/on-off-ramp.md deleted file mode 100644 index d2127051..00000000 --- a/.agents/skills/gooddollar/references/guides/on-off-ramp.md +++ /dev/null @@ -1,77 +0,0 @@ -# On- and off-ramp service guide - -Use when implementing service flow where fiat ramps support a listed stable token (for example cUSD), and the app needs stable <-> G$ swap on-chain. - -## Goal - -Operate deterministic clone-based swap routing with explicit chain/factory verification and slippage guard. - -## Required inputs - -- target chain and factory address -- owner address used for clone derivation -- stable token and G$ token addresses -- direction: on-ramp or off-ramp -- `minAmount` guard -- signer and rpc url - -## Execution flow - -1. Resolve factory for target chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (`BuyGDFactory` / `BuyGDFactoryV2` under `production-celo`, or the key your deployment uses). -2. Compute expected clone via `predict(owner)`. -3. If clone not yet deployed for flow, call `create(owner)` or `createAndSwap(owner, minAmount)`. -4. Execute stable -> G$ (on-ramp) or G$ -> stable (off-ramp) through clone path. -5. Return chain id, factory, predicted clone, effective clone, tx hashes. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const factory = new ethers.Contract( - process.env.BUY_GD_CLONE_FACTORY, - [ - "function predict(address) view returns (address)", - "function create(address) returns (address)", - "function createAndSwap(address,uint256) returns (address)", - ], - signer, -); - -const owner = process.env.OWNER; -const minAmount = ethers.parseUnits(process.env.MIN_AMOUNT, Number(process.env.DECIMALS_OUT)); -const predicted = await factory.predict(owner); - -const tx = await factory.createAndSwap(owner, minAmount); -const receipt = await tx.wait(); - -console.log( - JSON.stringify( - { - txHash: receipt.hash, - owner, - predictedClone: predicted, - chainId: (await provider.getNetwork()).chainId.toString(), - }, - null, - 2, - ), -); -``` - -## Failure handling - -- predicted clone mismatch with trusted expectation -- wrong chain or wrong factory address -- swap output below `minAmount` -- stale router/oracle or exchange configuration - -## Output contract - -- network and chain id -- factory address -- predicted and actual clone addresses -- tx hashes diff --git a/.agents/skills/gooddollar/references/guides/save.md b/.agents/skills/gooddollar/references/guides/save.md deleted file mode 100644 index d0e716cb..00000000 --- a/.agents/skills/gooddollar/references/guides/save.md +++ /dev/null @@ -1,93 +0,0 @@ -# Save and stake guide - -Use when the user wants to stake G$, withdraw rewards, or exit stake. Staking economics sit alongside other protocol allocations described in [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works). - -## GoodDocs alignment - -- Token integration and fee awareness: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token) (`_processFees`, decimals per chain). -- Contract addresses: [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only (for example staking and G$ token keys under `production` / `production-celo`). GoodDocs covers behavior and decimals patterns, not canonical deployment addresses. - -## Goal - -Run staking actions with balance and allowance safety checks. - -## Required inputs - -- `nameServiceAddress` or explicit staking and token addresses -- `amount` or `shares` depending on the action -- `rpcUrl`, chain configuration, signer - -## Execution flow - -1. Resolve staking and G$ token addresses from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) or, when the deployment documents the key, from `NameService.getAddress` on chain. -2. Read token balance and allowance. -3. Approve the staking contract when `stake` uses `transferFrom`. -4. Execute `stake`, `withdrawRewards`, or `withdrawStake` as requested. -5. Return tx hash and key resulting balances or events. - -## Deterministic snippets - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const token = new ethers.Contract( - process.env.GOODDOLLAR_ADDRESS, - [ - "function balanceOf(address) view returns (uint256)", - "function allowance(address,address) view returns (uint256)", - "function approve(address,uint256) returns (bool)", - ], - signer, -); - -const staking = new ethers.Contract( - process.env.STAKING_ADDRESS, - [ - "function stake(uint256)", - "function withdrawRewards()", - "function withdrawStake(uint256)", - ], - signer, -); -``` - -Stake: - -```js -const amount = ethers.parseUnits(process.env.AMOUNT, Number(process.env.DECIMALS)); -const owner = await signer.getAddress(); -const allowance = await token.allowance(owner, process.env.STAKING_ADDRESS); -if (allowance < amount) { - const approveTx = await token.approve(process.env.STAKING_ADDRESS, amount); - await approveTx.wait(); -} -const tx = await staking.stake(amount); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: "stake" }, null, 2)); -``` - -Withdraw rewards: - -```js -const tx = await staking.withdrawRewards(); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawRewards" }, null, 2)); -``` - -Withdraw stake: - -```js -const shares = ethers.parseUnits(process.env.SHARES, Number(process.env.DECIMALS)); -const tx = await staking.withdrawStake(shares); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: "withdrawStake" }, null, 2)); -``` - -## Failure handling - -- Insufficient balance: report shortfall. -- Approval issues: report token, spender, and required allowance. -- Reverts: return attempted function and parameters without guessing custom errors. diff --git a/.agents/skills/gooddollar/references/guides/stream.md b/.agents/skills/gooddollar/references/guides/stream.md deleted file mode 100644 index 6cbf7a45..00000000 --- a/.agents/skills/gooddollar/references/guides/stream.md +++ /dev/null @@ -1,135 +0,0 @@ -# Stream guide - -Primary references for stream execution are local ABI assets in this repo: - -- `references/contracts/CFAv1Forwarder.abi.yaml` -- `references/contracts/ConstantFlowAgreementV1.abi.yaml` -- `references/contracts/Superfluid.abi.yaml` -- `references/contracts/SuperToken.abi.yaml` - -## Goal - -Create, update, or delete Superfluid constant flows using deterministic contract calls and local ABI references. - -## Protocol facts used by this guide - -- Forwarder path uses `CFAv1Forwarder.createFlow`, `updateFlow`, `deleteFlow`. -- Host path uses `Superfluid.callAgreement` with CFA calldata for `createFlow`, `updateFlow`, `deleteFlow`. -- Stream token is a SuperToken; flow rates are `int96` in token-wei per second. -- `getBufferAmountByFlowrate(token, flowRate)` is the canonical pre-check for required buffer. - -## Two implementation styles in this repo - -1. **Forwarder (matches GoodDocs):** call CFAv1Forwarder with token, sender, receiver, flowRate, userData. -2. **Host callAgreement:** encode CFA `createFlow` / `updateFlow` / `deleteFlow` and call `Superfluid.callAgreement`. - -## Minimal method map - -- Forwarder: - - `createFlow(address token, address receiver, int96 flowrate, bytes userData)` - - `updateFlow(address token, address receiver, int96 flowrate, bytes userData)` - - `deleteFlow(address token, address sender, address receiver, bytes userData)` - - `getBufferAmountByFlowrate(address token, int96 flowrate)` -- Host: - - `callAgreement(address agreementClass, bytes callData, bytes userData)` -- CFA: - - `createFlow(address token, address receiver, int96 flowRate, bytes ctx)` - - `updateFlow(address token, address receiver, int96 flowRate, bytes ctx)` - - `deleteFlow(address token, address sender, address receiver, bytes ctx)` - -## Required inputs - -- G$ Super Token address for the environment -- CFA forwarder address, or Superfluid host address plus CFA agreement address -- `action`: create, update, delete -- `receiver`, `flowRate` where applicable -- `rpcUrl`, chain configuration, signer - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const forwarder = new ethers.Contract( - process.env.CFA_FORWARDER, - [ - "function createFlow(address,address,int96,bytes)", - "function updateFlow(address,address,int96,bytes)", - "function deleteFlow(address,address,address,bytes)", - ], - signer, -); - -const token = process.env.SUPER_TOKEN; -const sender = await signer.getAddress(); -const receiver = process.env.RECEIVER; -const flowRate = BigInt(process.env.FLOW_RATE); - -if (process.env.ACTION === "create") { - const tx = await forwarder.createFlow(token, receiver, flowRate, "0x"); - const receipt = await tx.wait(); - console.log(JSON.stringify({ txHash: receipt.hash, action: "create" }, null, 2)); -} - -if (process.env.ACTION === "update") { - const tx = await forwarder.updateFlow(token, receiver, flowRate, "0x"); - const receipt = await tx.wait(); - console.log(JSON.stringify({ txHash: receipt.hash, action: "update" }, null, 2)); -} - -if (process.env.ACTION === "delete") { - const tx = await forwarder.deleteFlow(token, sender, receiver, "0x"); - const receipt = await tx.wait(); - console.log(JSON.stringify({ txHash: receipt.hash, action: "delete" }, null, 2)); -} -``` - -Host callAgreement example: - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const cfa = new ethers.Interface([ - "function createFlow(address,address,int96,bytes)", - "function updateFlow(address,address,int96,bytes)", - "function deleteFlow(address,address,address,bytes)", -]); - -const host = new ethers.Contract( - process.env.SUPERFLUID_HOST, - ["function callAgreement(address,bytes,bytes) returns (bytes)"], - signer, -); - -const token = process.env.SUPER_TOKEN; -const sender = await signer.getAddress(); -const receiver = process.env.RECEIVER; -const flowRate = BigInt(process.env.FLOW_RATE); - -let callData = "0x"; -if (process.env.ACTION === "create") { - callData = cfa.encodeFunctionData("createFlow", [token, receiver, flowRate, "0x"]); -} -if (process.env.ACTION === "update") { - callData = cfa.encodeFunctionData("updateFlow", [token, receiver, flowRate, "0x"]); -} -if (process.env.ACTION === "delete") { - callData = cfa.encodeFunctionData("deleteFlow", [token, sender, receiver, "0x"]); -} - -const tx = await host.callAgreement(process.env.CFA_ADDRESS, callData, "0x"); -const receipt = await tx.wait(); -console.log(JSON.stringify({ txHash: receipt.hash, action: process.env.ACTION }, null, 2)); -``` - -## Failure handling - -- Wrong network or missing addresses: stop and return missing host or forwarder or token addresses. -- Insufficient buffer: use `getBufferAmountByFlowrate` and reduce flow rate or top up balance. -- Revert on create or update: verify token is a SuperToken and flowRate is positive. diff --git a/.agents/skills/gooddollar/references/guides/swap.md b/.agents/skills/gooddollar/references/guides/swap.md deleted file mode 100644 index 3db8c2dc..00000000 --- a/.agents/skills/gooddollar/references/guides/swap.md +++ /dev/null @@ -1,83 +0,0 @@ -# Swap guide - -Use for buying or selling G$ through Mento-connected contracts on networks where they appear in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `MentoBroker`, `MentoReserve`, `MentoExchangeProvider`, `MentoExpansionController` keys under `production-celo` or `production-xdc`). GoodDocs describes Mento product behavior, not deployment addresses. - -## GoodDocs alignment - -- Reserve and buy or sell mechanics at the protocol level: [How GoodDollar works](https://docs.gooddollar.org/how-gooddollar-works) and [Buy and Sell G$ user guide](https://docs.gooddollar.org/user-guides) (includes reserve AMM narrative; older explorer step-by-step for Ethereum testnets remains in that page for reference). -- Integration patterns and decimals: [How to integrate the G$ token](https://docs.gooddollar.org/for-developers/developer-guides/how-to-integrate-the-gusd-token). - -## Goal - -Execute bounded swaps using broker quotes and correct allowances. - -## Required inputs - -- `direction` as buy or sell -- broker and exchange identifiers for the deployment -- amounts in correct token decimals for the chain -- `rpcUrl`, chain configuration, signer - -## Execution flow - -1. Confirm Mento Broker (and related) addresses for the chain from [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) only. -2. Fetch quote (`getAmountOut` or `getAmountIn` depending on direction and ABI). -3. Apply slippage bounds. -4. Approve the spent token for the broker when required. -5. Call `swapIn` or `swapOut` per your integration. -6. Return tx hash and effective amounts. - -## Deterministic snippet - -```js -import { ethers } from "ethers"; - -const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); -const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); - -const broker = new ethers.Contract( - process.env.BROKER_ADDRESS, - [ - "function getAmountOut(address,address,uint256) view returns (uint256)", - "function swapIn(address,address,uint256,uint256) returns (uint256)", - ], - signer, -); - -const tokenIn = new ethers.Contract( - process.env.TOKEN_IN, - [ - "function allowance(address,address) view returns (uint256)", - "function approve(address,uint256) returns (bool)", - ], - signer, -); - -const amountIn = ethers.parseUnits(process.env.AMOUNT_IN, Number(process.env.DECIMALS_IN)); -const quotedOut = await broker.getAmountOut(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn); -const slippageBps = BigInt(process.env.SLIPPAGE_BPS); -const minOut = quotedOut * (10000n - slippageBps) / 10000n; - -const owner = await signer.getAddress(); -const allowance = await tokenIn.allowance(owner, process.env.BROKER_ADDRESS); -if (allowance < amountIn) { - const approveTx = await tokenIn.approve(process.env.BROKER_ADDRESS, amountIn); - await approveTx.wait(); -} - -const tx = await broker.swapIn(process.env.TOKEN_IN, process.env.TOKEN_OUT, amountIn, minOut); -const receipt = await tx.wait(); -console.log( - JSON.stringify( - { txHash: receipt.hash, amountIn: amountIn.toString(), minOut: minOut.toString() }, - null, - 2, - ), -); -``` - -## Failure handling - -- No deployment on chain: direct the user to an environment that defines the needed keys in [deployment.json](https://github.com/GoodDollar/GoodProtocol/blob/master/releases/deployment.json) (for example `production-celo` with `MentoBroker`). -- Stale quote or tight slippage: refresh quote or relax bounds with user consent. -- Allowance or balance shortfall: report exact delta. diff --git a/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md b/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md deleted file mode 100644 index 36af6398..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/_query-patterns.md +++ /dev/null @@ -1,35 +0,0 @@ -# Subgraph query discipline (GoodDollar) - -## Subgraph vs RPC - -- **Subgraph:** historical events, lists, aggregates, time ranges, analytics. Data lags chain head. -- **RPC / SDK:** current balances, live `claim` eligibility, exact view calls. Prefer for user-facing “what is true right now”. - -## Generic GraphQL mechanics - -Graph-node generates `entity`, `entities`, `Entity_filter`, `Entity_orderBy`, pagination, and `_meta` from each deployment’s `schema.graphql`. For scalar rules, filters, `_meta`, and common pitfalls, see [The Graph — Querying a subgraph](https://thegraph.com/docs/en/querying/graphql-api/). - -## GoodDollar-specific - -- **Addresses in `where` clauses:** normalize to **lowercase** hex strings; subgraphs store addresses lowercased. -- **BigInt fields:** query as **string** literals in GraphQL JSON (e.g. `"1000000000000000000"`). -- **Schema truth:** entity names differ per deployment. Run introspection or read the deployment’s `schema.graphql` under the relevant package in [GoodDollar/GoodSubGraphs](https://github.com/GoodDollar/GoodSubGraphs) before assuming field names. - -## Meta block - -Use `_meta { block { number } }` to detect how far behind indexing is when debugging stale data. - -```graphql -{ - _meta { - block { - number - } - hasIndexingErrors - } -} -``` - -## When subgraphs are not enough - -If the subgraph cannot answer the question (missing entities or fields, or stale indexing per `_meta`), switch to the decision rules in `references/guides/hypersync-hyperrpc.md`. When **HyperSync** is the best fit and no Envio API token is available in the environment, **ask the user directly** for `ENVIO_API_TOKEN` (or the token your HyperSync client expects) before running large scans; do not silently use anonymous quota as a stand-in. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md b/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md deleted file mode 100644 index a266ceab..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/goodcollective-guide.md +++ /dev/null @@ -1,54 +0,0 @@ -# GoodCollective — Subgraph Usage Guide - -Companion to `goodcollective.graphql`. - -## Endpoint - -- Explorer: [GoodCollective](https://thegraph.com/explorer/subgraphs/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y?view=Query&chain=arbitrum-one) -- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/3LbJh9DXhJVvuVDdm5i6StNboJmL9oMNNkBaKyzc4Y8Y` - ---- - -## Terminology: “claim” here is not daily UBI - -In this subgraph, **Claim** and **ClaimEvent** refer to **GoodCollective reward or pool claim flows**, not the protocol’s **daily UBI claim** from `UBIScheme` / `UBISchemeV2`. - -When a user says **“claim”** in normal GoodDollar product language, they almost always mean **claim daily UBI**. For that, use the GoodDollar Celo subgraph (`walletStats` / claim-related aggregates) and on-chain `claim` per `references/guides/claim.md` — do not answer “last N UBI claims” from GoodCollective **Claim** alone. - ---- - -## Entity Overview - -### Core collective graph - -**Collective** — pool identity, limits/settings links, totals, and claim/payment counters. -**Donor** / **Steward** — participant-level donation/support state. -**DonorCollective** / **StewardCollective** — join entities tying participants to a collective. - -### Claim and support flow - -**Claim** and **ClaimEvent** — GoodCollective **reward** claim lifecycle and per-claim reward events (not daily UBI from UBIScheme). -**SupportEvent** — support/donation change events across donor/collective links. - -### Metadata and policy entities - -**IpfsCollective** — IPFS metadata projection. -**PoolSettings**, **UBILimits**, **SafetyLimits** — pool policy and operational bounds. -**ProvableNFT** — NFT linkage used in claim/reward flows. - ---- - -## Typical Questions This Subgraph Answers - -- Which donors/stewards are attached to a collective? -- How much was donated/rewarded per collective and per participant? -- Which claim events occurred and what reward quantities were emitted? -- What limits and settings govern a specific collective pool? - ---- - -## Query Discipline - -- Use authenticated gateway access for programmatic queries. -- Lowercase address-like identifiers where applicable. -- Validate `_meta` before operational dashboards or reporting exports. diff --git a/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql b/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql deleted file mode 100644 index e503db9c..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/goodcollective.graphql +++ /dev/null @@ -1,200 +0,0 @@ -scalar BigDecimal - -scalar BigInt - -scalar Boolean - -scalar Bytes - -type Claim { - id: String! - collective: Collective! - txHash: String! - networkFee: BigInt! - totalRewards: BigInt! - events: [ClaimEvent!]! - timestamp: Int! -} - -type ClaimEvent { - id: String! - eventType: Int! - timestamp: Int! - quantity: BigInt! - rewardPerContributor: BigInt! - contributors: [Steward!]! - nft: ProvableNFT - claim: Claim! -} - -type Collective { - id: String! - pooltype: PoolType! - ipfs: IpfsCollective - settings: PoolSettings! - ubiLimits: UBILimits - limits: SafetyLimits - donors: [DonorCollective!] - stewards: [StewardCollective!] - projectId: String! - isVerified: Boolean! - poolFactory: String! - timestamp: Int! - paymentsMade: Int! - totalDonations: BigInt! - totalRewards: BigInt! - claims: [Claim!]! -} - -type Donor { - id: String! - timestamp: Int! - totalDonated: BigInt! - collectives: [DonorCollective!]! -} - -""" -Represents the relationship between a Donor and a Collective - -""" - -type DonorCollective { - id: String! - donor: Donor! - collective: Collective! - contribution: BigInt! - flowRate: BigInt! - timestamp: Int! - events: [SupportEvent!]! -} - -scalar Float - -scalar ID - -""" -4 bytes signed integer - -""" - -scalar Int - -""" -8 bytes signed integer - -""" - -scalar Int8 - -type IpfsCollective { - id: String! - name: String! - description: String! - rewardDescription: String - goodidDescription: String - email: String - website: String - twitter: String - instagram: String - threads: String - infoLabel: String - headerImage: String! - logo: String! - images: [String!] -} - -type PoolSettings { - id: String! - nftType: BigInt! - manager: Bytes! - membersValidator: Bytes! - uniquenessValidator: Bytes! - rewardToken: Bytes! -} - -enum PoolType { - DirectPayments - UBI -} - -type ProvableNFT { - id: String! - owner: String! - hash: String! - stewards: [Steward!]! - collective: Collective -} - -type SafetyLimits { - id: String! - maxTotalPerMonth: BigInt! - maxMemberPerMonth: BigInt! - maxMemberPerDay: BigInt! -} - -type Steward { - """ - { user address} - - """ - id: String! - """ - Number of actions performed - - """ - actions: Int! - totalEarned: BigInt! - totalUBIEarned: BigInt! - """ - NFT's minted to steward - - """ - nfts: [ProvableNFT!]! - """ - Collectives the steward is apart of - - """ - collectives: [StewardCollective!]! -} - -""" -Represents the relationship between a Steward and a Collective - -""" - -type StewardCollective { - id: String! - steward: Steward! - collective: Collective! - actions: Int! - totalEarned: BigInt! -} - -scalar String - -type SupportEvent { - id: String! - networkFee: BigInt! - donor: Donor! - collective: Collective! - donorCollective: DonorCollective! - contribution: BigInt! - previousContribution: BigInt! - isFlowUpdate: Boolean! - flowRate: BigInt! - previousFlowRate: BigInt! - timestamp: Int! -} - -scalar Timestamp - -type UBILimits { - id: String! - cycleLengthDays: BigInt! - claimPeriodDays: BigInt! - minActiveUsers: BigInt! - claimForEnabled: Boolean! - maxClaimAmount: BigInt! - maxClaimers: BigInt! - onlyMembers: Boolean! -} diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md deleted file mode 100644 index 347951f4..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo-guide.md +++ /dev/null @@ -1,104 +0,0 @@ -# GoodDollar Celo — Subgraph Usage Guide - -Companion to `gooddollar-celo.graphql`. - -## Endpoint - -- Explorer: [GoodDollarCelo](https://thegraph.com/explorer/subgraphs/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw?view=Query&chain=arbitrum-one) -- Gateway form: `https://gateway.thegraph.com/api/subgraphs/id/F7314rxGdcpKPC1nN5KCoFW84EGRoUyzseY2sAT9PEkw` - ---- - -## Entity Overview - -### UBI and usage statistics - -**DailyUBI** — day-level UBI pool/quota activity and cycle fields. -**WalletStat** — wallet behavior aggregates: tx counts/values, claim stats, active/whitelist indicators. -**TransactionStat** — day-level transaction totals and circulation view. -**GlobalStatistics** — global claim and distribution rollups. - -### Additional UBI history entities - -**UBICollected** — collected UBI/community-pool values by block event. -**UBIHistory** — timeline totals for daily UBI/community-pool. - ---- - -## Field Availability Reference (use before drafting queries) - -Use this section to validate what the subgraph already provides before switching data sources. - -### DailyUBI - -- `id` — day index key (`unix / 86400`) -- `pool` — UBI cycle pool for that day -- `quota` — daily UBI amount per eligible claimer -- `activeUsers` — active users count in scheme context -- `totalUBIDistributed` — amount actually claimed/distributed that day -- `totalClaims` — claim tx count for that day -- `newClaimers` — newly whitelisted users for that day -- `timestamp` — last update timestamp for the record -- `ubiSchemeAddress` — UBIScheme address used for the record -- `balance` — G$ balance held by UBIScheme -- `cycleLength` — current cycle length -- `dayInCycle` — current day position inside cycle - -### WalletStat - -- `id` — wallet address -- `dateAppeared` — first indexed wallet activity timestamp -- `balance` — running token balance from transfers -- `inTransactionsCount`, `inTransactionsValue` — incoming tx count and value -- `outTransactionsCount`, `outTransactionsValue` — outgoing tx count and value -- `totalTransactionsCount`, `totalTransactionsValue` — total tx count and value -- `inTransactionsCountClean`, `inTransactionsValueClean` — incoming metrics excluding contract-address flows -- `outTransactionsCountClean`, `outTransactionsValueClean` — outgoing metrics excluding contract-address flows -- `totalTransactionsCountClean`, `totalTransactionsValueClean` — total clean traffic metrics -- `lastClaimed` — timestamp of latest UBI claim -- `totalClaimedCount`, `totalClaimedValue` — total claims and cumulative claimed value -- `claimStreak`, `longestClaimStreak` — current and best historical streaks -- `isWhitelisted` — current whitelist status -- `isActiveUser` — current active-user status -- `dateJoined` — first-whitelist timestamp -- `lastTransactionFrom`, `lastTransactionTo` — latest outgoing and incoming tx timestamps - -### TransactionStat - -- `id` — bucket key (day key or `"aggregated"`) -- `dayStartBlockNumber` — first block in bucket -- `transactionsCount`, `transactionsValue` — all transfer tx count and value -- `transactionsCountClean`, `transactionsValueClean` — transfer metrics excluding contract-address flows -- `totalInCirculation` — inferred circulating supply from mint or burn behavior - -### GlobalStatistics - -- `id` — fixed key (`"statistics"`) -- `TransactionStat` — link to aggregated transaction stats -- `totalUBIDistributed` — lifetime distributed UBI -- `uniqueClaimers` — tracked unique claimers via whitelist add or remove -- `totalClaims` — lifetime UBI claim events - -### Explorer naming - -- Singular names (`dailyUBI`, `walletStat`) fetch by `id` -- Plural names (`dailyUBIs`, `walletStats`) query lists with filters and pagination - ---- - -## Typical Questions This Subgraph Answers - -- How much UBI was distributed on a day/cycle? -- Which wallets are active or recently claiming? -- What are aggregate tx/circulation trends? -- How did collected UBI/community-pool values evolve over time? - ---- - -## Query Discipline - -- Use lowercase address strings in filters. -- Use string-safe handling for large integer values. -- Use `_meta` to validate freshness before cross-day analytics. -- Use authenticated gateway access for programmatic queries. -- Before claiming a field or entity is missing, verify availability from this guide and schema first. diff --git a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql deleted file mode 100644 index e544a0dc..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/gooddollar-celo.graphql +++ /dev/null @@ -1,113 +0,0 @@ -scalar BigDecimal - -scalar BigInt - -scalar Boolean - -scalar Bytes - -type DailyUBI { - id: ID! - pool: BigInt! - quota: BigInt! - activeUsers: BigInt! - totalUBIDistributed: BigInt! - totalClaims: BigInt! - newClaimers: BigInt! - timestamp: BigInt! - ubiSchemeAddress: Bytes - balance: BigInt! - cycleLength: BigInt! - dayInCycle: BigInt! -} - -scalar Float - -type GlobalStatistics { - id: ID! - TransactionStat: TransactionStat - totalUBIDistributed: BigInt! - uniqueClaimers: BigInt! - totalClaims: BigInt! -} - -scalar ID - -""" -4 bytes signed integer -""" - -scalar Int - -""" -8 bytes signed integer -""" - -scalar Int8 - -""" -Defines the order direction, either ascending or descending -""" - -scalar String - -""" -A string representation of microseconds UNIX timestamp (16 digits) -""" - -scalar Timestamp - -type TransactionStat { - id: ID! - dayStartBlockNumber: BigInt! - transactionsCount: BigInt! - transactionsCountClean: BigInt! - transactionsValue: BigInt! - transactionsValueClean: BigInt! - totalInCirculation: BigInt! -} - -type UBICollected { - id: ID! - contract: Bytes - block: BigInt! - blockTimestamp: BigInt! - ubi: BigDecimal! - communityPool: BigDecimal! -} - -type UBIHistory { - id: ID! - block: BigInt! - blockTimestamp: BigInt! - totalDailyUBI: BigDecimal! - totalDailyCommunityPool: BigDecimal! -} - -type WalletStat { - id: ID! - dateAppeared: BigInt! - balance: BigInt! - inTransactionsCount: BigInt! - inTransactionsCountClean: BigInt! - inTransactionsValue: BigInt! - inTransactionsValueClean: BigInt! - outTransactionsCount: BigInt! - outTransactionsCountClean: BigInt! - outTransactionsValue: BigInt! - outTransactionsValueClean: BigInt! - totalTransactionsCount: BigInt! - totalTransactionsCountClean: BigInt! - totalTransactionsValue: BigInt! - totalTransactionsValueClean: BigInt! - lastClaimed: BigInt! - totalClaimedCount: BigInt! - totalClaimedValue: BigInt! - claimStreak: BigInt! - longestClaimStreak: BigInt! - isWhitelisted: Boolean! - isActiveUser: Boolean! - dateJoined: BigInt! - lastTransactionFrom: BigInt! - lastTransactionTo: BigInt! -} diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md b/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md deleted file mode 100644 index 9f55b734..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/reserve-celo-guide.md +++ /dev/null @@ -1,41 +0,0 @@ -# Reserve Celo — Subgraph Usage Guide - -Companion to `reserve-celo.graphql`. This guide is for reserve pricing and broker swap indexing on Celo. - -## Endpoint - -- Goldsky: `https://api.goldsky.com/api/public/project_cmizuamdtfouu01x4csuk5dk1/subgraphs/reserve_celo/1.0/gn` - ---- - -## Entity Overview - -### Core entity - -**ReservePrice** — one indexed reserve pricing point produced from broker swap flow plus exchange-provider price read. -Key fields: `exchangeId`, `exchangeProvider`, `price`, `timestamp`, `day`, `tokenIn`, `tokenOut`, `amountIn`, `amountOut`, `user`, `blockNumber`, `transactionHash`. - ---- - -## Typical Questions This Subgraph Answers - -- What are the most recent reserve prices? -- What was the reserve price on a specific day window? -- Which token pair and user triggered a pricing point? -- Which tx hash/block produced a given price point? - ---- - -## Query Discipline - -- Lowercase all address values in filters. -- Use string values for large integer variables. -- Use `_meta` before analytics queries when stale indexing is suspected. - ---- - -## Practical Start - -1. Check `_meta` block height and `hasIndexingErrors`. -2. Pull latest `ReservePrice` records sorted by `timestamp desc`. -3. Add `day`-based narrowing for historical windows. diff --git a/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql b/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql deleted file mode 100644 index 56cac37b..00000000 --- a/.agents/skills/gooddollar/references/subgraphs/reserve-celo.graphql +++ /dev/null @@ -1,90 +0,0 @@ -scalar BigDecimal - -scalar BigInt - -scalar Boolean - -scalar Bytes - -scalar Float - -scalar ID - -""" -4 bytes signed integer -""" - -scalar Int - -""" -8 bytes signed integer -""" - -scalar Int8 - -""" -Defines the order direction, either ascending or descending -""" - -type ReservePrice { - """ - Exchange ID combined with timestamp - """ - id: ID! - """ - The exchange ID from the Swap event - """ - exchangeId: Bytes! - """ - The exchange provider address - """ - exchangeProvider: Bytes! - """ - The current price from IBancorExchangeProvider.currentPrice() - """ - price: BigInt! - """ - Transaction timestamp - """ - timestamp: BigInt! - """ - Day ID (timestamp / 86400) - """ - day: BigInt! - """ - Token in address from the swap - """ - tokenIn: Bytes! - """ - Token out address from the swap - """ - tokenOut: Bytes! - """ - Amount in from the swap - """ - amountIn: BigInt! - """ - Amount out from the swap - """ - amountOut: BigInt! - """ - User who initiated the swap - """ - user: Bytes! - """ - Block number - """ - blockNumber: BigInt! - """ - Transaction hash - """ - transactionHash: Bytes! -} - -scalar String - -""" -A string representation of microseconds UNIX timestamp (16 digits) -""" - -scalar Timestamp diff --git a/.gitignore b/.gitignore index 94096bab..591a70c2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ yalc.lock /dist/ .vercel .continueignore +.agents/ e2e.log docs/specs/** -codex-resume \ No newline at end of file +codex-resume +skills-lock.json \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json deleted file mode 100644 index 9b92c725..00000000 --- a/skills-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "skills": { - "gooddollar": { - "source": "GoodDollar/GoodSkills", - "sourceType": "github", - "skillPath": "skills/gooddollar/SKILL.md", - "computedHash": "d5f8cc5079f386e7170925e61214fb3334176aa6d0fc6bdc50400e535692b377" - } - } -} From ff95f8cc19d091768c30f03b2e3d593795b2e4fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:20:58 +0000 Subject: [PATCH 5/7] docs: add bridge history sync comments --- .../sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts | 5 +++++ .../src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts index 8d590103..e038d8f7 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts @@ -33,8 +33,10 @@ export type MPBBridgeHistoryCache = { chains?: Partial>; }; +// The cache only keeps a rolling 30-day history so first paint stays useful without growing forever. export const HISTORY_WINDOW_DAYS = 30; export const HISTORY_WINDOW_SECONDS = HISTORY_WINDOW_DAYS * 24 * 60 * 60; +// Public RPCs were failing on large getLogs windows, so every sync is split into small ranges. export const HISTORY_BLOCK_CHUNK_SIZE = 500; const getEventCacheKey = (event: CachedBridgeEvent) => @@ -52,6 +54,7 @@ export const createBlockChunks = (fromBlock: number, toBlock: number, chunkSize const chunks: Array<{ fromBlock: number; toBlock: number }> = []; + // Build inclusive ranges so callers can safely fetch [fromBlock, toBlock] without gaps or overlaps. for (let cursor = fromBlock; cursor <= toBlock; cursor += chunkSize) { chunks.push({ fromBlock: cursor, @@ -77,6 +80,7 @@ export const mergeBridgeHistoryCache = ( const mergedRequests = new Map(); const mergedTransfers = new Map(); + // Merge new rows into the cached window and let the event key dedupe repeated fetches across refreshes. pruneExpiredEvents((current.BridgeRequest || []).concat(nextEvents.BridgeRequest || []), minTimestamp).forEach( event => mergedRequests.set(getEventCacheKey(event), event) ); @@ -89,6 +93,7 @@ export const mergeBridgeHistoryCache = ( BridgeRequest: sortBridgeEvents(Array.from(mergedRequests.values())), ExecutedTransfer: sortBridgeEvents(Array.from(mergedTransfers.values())), chains: { + // Per-chain sync state is merged independently so one failing RPC does not wipe successful cursors. ...(current.chains || {}), ...nextChains } diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts index 8f35dc23..dc32bb1b 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts @@ -22,6 +22,7 @@ const CHAIN_IDS = [SupportedChains.FUSE, SupportedChains.CELO, SupportedChains.M const MAX_PARALLEL_CHUNKS = 3; const hydrateCachedEvent = (event: CachedBridgeEvent) => { + // Persist plain JSON in storage, then rebuild the BigNumber-shaped fields the rest of the hook expects. const targetChainId = ethers.BigNumber.from(event.targetChainId); const amount = ethers.BigNumber.from(event.amount); const timestamp = ethers.BigNumber.from(event.timestamp); @@ -68,6 +69,7 @@ const getErrorMessage = (error: unknown) => { const runWithConcurrency = async (tasks: Array<() => Promise>, concurrency: number): Promise => { const results: T[] = []; + // Batch chunk fetches instead of firing every getLogs request at once against the same public RPC. for (let index = 0; index < tasks.length; index += concurrency) { const nextResults = await Promise.all(tasks.slice(index, index + concurrency).map(task => task())); results.push(...nextResults); @@ -200,6 +202,7 @@ const syncChainHistory = async ( const chainState = currentCache.chains?.[chainId]; const targetTimestamp = Math.floor(Date.now() / 1000) - HISTORY_WINDOW_SECONDS; const fromBlock = + // Warm cache: resume from the last synced block. Cold cache: backfill only the rolling history window. chainState?.lastSyncedBlock !== undefined ? chainState.lastSyncedBlock + 1 : await findHistoryStartBlock(provider, latestBlock, targetTimestamp); @@ -262,6 +265,7 @@ export const useMPBBridgeHistory = () => { ":" ); + // Scope cache entries to the wallet and the deployed bridge addresses so network/config changes do not mix data. return `GD_MPBBridgeHistory_v${HISTORY_CACHE_VERSION}_${account.toLowerCase()}_${contractAddresses}`; }, [account, contracts]); @@ -320,10 +324,12 @@ export const useMPBBridgeHistory = () => { let cancelled = false; + // Keep cached rows on screen and expose a separate refreshing state while each chain sync runs. setSyncing(true); const syncHistory = async () => { const currentCache = historyCacheRef.current; + // Sync every chain independently so a single failing RPC cannot block the others from updating cache. const settledChains = await Promise.allSettled( chainContracts.map(({ chainId, contract }) => syncChainHistory(chainId, contract, currentCache, account)) ); @@ -340,12 +346,14 @@ export const useMPBBridgeHistory = () => { const { chainId } = chainContracts[index]; if (result.status === "fulfilled") { + // Successful chains contribute rows and advance only their own cursor/error state. nextChains[chainId] = result.value.chainState; nextBridgeRequests.push(...result.value.bridgeRequests); nextExecutedTransfers.push(...result.value.executedTransfers); return; } + // Failed chains keep their last good cursor and surface a chain-specific error for the UI. nextChains[chainId] = { ...(currentCache.chains?.[chainId] || {}), error: { @@ -366,6 +374,7 @@ export const useMPBBridgeHistory = () => { setHistoryCache(nextCache); historyCacheRef.current = nextCache; + // Persist the merged cache after every refresh so the next mount can render immediately from storage. void AsyncStorage.setItem(cacheKey, nextCache).catch(error => console.warn("Failed to store MPB bridge history cache", error) ); @@ -426,6 +435,7 @@ export const useMPBBridgeHistory = () => { const requestId = event.data?.id?.toString(); const sourceChainId = event.data.sourceChainId.toNumber(); + // Match a request against completion events from the other chains to preserve the old merged UX. const completedEventsMap = CHAIN_IDS.filter(chainId => chainId !== sourceChainId).reduce((result, chainId) => { return { ...result, ...completedByTargetChain[chainId] }; }, {} as Record); From 6096f6d137a58f2b2cc28e45d3cd5a841f30c7cc Mon Sep 17 00:00:00 2001 From: Victor Edeh Date: Tue, 14 Jul 2026 12:50:20 +0100 Subject: [PATCH 6/7] fix: stabilize MPB bridge transaction history reads (#271) * fix: stabilize MPB bridge history reads * fix: finalize MPB bridge history recovery * fix: handle transient MPB bridge block read errors * fix: polish MPB bridge transaction details * fix: address MPB bridge review feedback --- .gitignore | 4 +- .../bridge/mpbridge/MPBBridgeController.tsx | 8 +- .../bridge/mpbridge/TransactionHistory.tsx | 3 +- .../feature/useMPBBridgeFeatureController.ts | 7 +- .../feature/useMPBBridgeViewController.ts | 9 +- .../src/apps/bridge/mpbridge/hooks.ts | 13 +- .../src/apps/bridge/mpbridge/index.ts | 10 +- .../src/apps/bridge/mpbridge/types.ts | 5 + .../mpbridge/utils/transactionHelpers.test.ts | 30 ++ .../mpbridge/utils/transactionHelpers.ts | 4 +- .../sdk-v2/src/hooks/useMulticallAtChain.tsx | 5 +- .../sdk-v2/src/sdk/mpbridge/hooks/index.ts | 2 + .../mpbridge/hooks/useMPBBridge.helpers.ts | 16 + .../sdk/mpbridge/hooks/useMPBBridge.test.ts | 21 + .../src/sdk/mpbridge/hooks/useMPBBridge.ts | 158 +++++- .../hooks/useMPBBridgeHistory.helpers.test.ts | 32 ++ .../hooks/useMPBBridgeHistory.helpers.ts | 35 ++ .../sdk/mpbridge/hooks/useMPBBridgeHistory.ts | 488 ++++++++++++++---- 18 files changed, 701 insertions(+), 149 deletions(-) create mode 100644 packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.test.ts create mode 100644 packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.helpers.ts create mode 100644 packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.test.ts diff --git a/.gitignore b/.gitignore index 591a70c2..94096bab 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,6 @@ yalc.lock /dist/ .vercel .continueignore -.agents/ e2e.log docs/specs/** -codex-resume -skills-lock.json \ No newline at end of file +codex-resume \ No newline at end of file diff --git a/packages/good-design/src/apps/bridge/mpbridge/MPBBridgeController.tsx b/packages/good-design/src/apps/bridge/mpbridge/MPBBridgeController.tsx index 819a7ab6..d10edcc9 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/MPBBridgeController.tsx +++ b/packages/good-design/src/apps/bridge/mpbridge/MPBBridgeController.tsx @@ -3,23 +3,27 @@ import { VStack } from "native-base"; import { MPBBridge } from "./MPBBridge"; import { useMPBBridgeFeatureController } from "./feature/useMPBBridgeFeatureController"; +import type { MPBBridgeReadOnlyUrls } from "./types"; interface IMPBBridgeControllerProps { withHistory?: boolean; onBridgeStart?: () => void; onBridgeSuccess?: () => void; onBridgeFailed?: (e: Error) => void; + bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls; } export const MPBBridgeController: React.FC = ({ onBridgeStart, onBridgeSuccess, - onBridgeFailed + onBridgeFailed, + bridgeReadOnlyUrls }) => { const bridgeProps = useMPBBridgeFeatureController({ onBridgeStart, onBridgeSuccess, - onBridgeFailed + onBridgeFailed, + bridgeReadOnlyUrls }); return ( diff --git a/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx b/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx index f78a45b8..5a5ccc34 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx +++ b/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx @@ -21,6 +21,7 @@ export const TransactionHistory: React.FC = ({ onTxDetailsPress }) => { const errorEntries = Object.entries(historyErrorsByChain || {}); + const hasTransactionHistory = realTransactionHistory.length > 0; return ( @@ -69,7 +70,7 @@ export const TransactionHistory: React.FC = ({ Loading transaction history... - ) : realTransactionHistory.length > 0 ? ( + ) : hasTransactionHistory ? ( diff --git a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeFeatureController.ts b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeFeatureController.ts index 9d1ca53d..7d7c4638 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeFeatureController.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeFeatureController.ts @@ -3,13 +3,14 @@ import { useEthers } from "@usedapp/core"; import { ethers } from "ethers"; import { useMPBBridgeFlow, useG$Decimals, SupportedChains, VALIDATION_REASONS } from "@gooddollar/web3sdk-v2"; -import { BridgeProvider, MPBBridgeProps } from "../types"; +import { BridgeProvider, MPBBridgeProps, MPBBridgeReadOnlyUrls } from "../types"; import { getDefaultTargetChain } from "../utils/chainHelpers"; interface UseMPBBridgeFeatureControllerParams { onBridgeStart?: () => void; onBridgeSuccess?: () => void; onBridgeFailed?: (e: Error) => void; + bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls; } const ZERO_FEE = { nativeFee: ethers.BigNumber.from(0), zroFee: ethers.BigNumber.from(0) }; @@ -17,7 +18,8 @@ const ZERO_FEE = { nativeFee: ethers.BigNumber.from(0), zroFee: ethers.BigNumber export const useMPBBridgeFeatureController = ({ onBridgeStart, onBridgeSuccess, - onBridgeFailed + onBridgeFailed, + bridgeReadOnlyUrls }: UseMPBBridgeFeatureControllerParams): MPBBridgeProps => { const { chainId, account } = useEthers(); const [bridgeProvider, setBridgeProvider] = useState("layerzero"); @@ -176,6 +178,7 @@ export const useMPBBridgeFeatureController = ({ onBridgeStart: onBridgeStartHandler, onBridgeFailed, onBridgeSuccess, + bridgeReadOnlyUrls, bridgeProvider, onBridgeProviderChange: setBridgeProvider }; diff --git a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts index ae62f5c1..5ab7535a 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts @@ -17,6 +17,12 @@ import { useMPBBridgeUiState } from "./useMPBBridgeUiState"; const DEBOUNCE_MS = 300; const TRANSACTION_HISTORY_DEBOUNCE_MS = 2000; +const BRIDGE_HISTORY_CHAIN_IDS: SupportedChains[] = [ + SupportedChains.CELO, + SupportedChains.FUSE, + SupportedChains.MAINNET, + SupportedChains.XDC +]; const FLOW_PENDING_STATES = new Set([ "awaiting_network_switch", @@ -134,6 +140,7 @@ export const useMPBBridgeViewController = ({ onBridgeStart, onBridgeFailed, onBridgeSuccess, + bridgeReadOnlyUrls, bridgeProvider: propBridgeProvider, onBridgeProviderChange }: MPBBridgeProps): MPBBridgeViewModel => { @@ -166,7 +173,7 @@ export const useMPBBridgeViewController = ({ } = useMPBBridgeUiState(); const { realTransactionHistory, historyLoading, historyRefreshing, historyErrorsByChain, refreshHistory } = - useDebouncedTransactionHistory(TRANSACTION_HISTORY_DEBOUNCE_MS); + useDebouncedTransactionHistory(TRANSACTION_HISTORY_DEBOUNCE_MS, bridgeReadOnlyUrls, BRIDGE_HISTORY_CHAIN_IDS); const { getBalanceForChain } = useChainBalances(); const gdValue = getBalanceForChain(sourceChain); diff --git a/packages/good-design/src/apps/bridge/mpbridge/hooks.ts b/packages/good-design/src/apps/bridge/mpbridge/hooks.ts index eee5e6cb..d273f6dd 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/hooks.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/hooks.ts @@ -3,7 +3,7 @@ import { CurrencyValue } from "@usedapp/core"; import { useG$Amounts, useProductionG$Balance, G$Amount, useGetEnvChainId } from "@gooddollar/web3sdk-v2"; import { BigNumber } from "ethers"; import { fetchBridgeFees, useMPBBridgeHistory } from "@gooddollar/web3sdk-v2"; -import type { IMPBFees, IMPBLimits } from "./types"; +import type { IMPBFees, IMPBLimits, MPBBridgeHistoryChainIds, MPBBridgeReadOnlyUrls } from "./types"; import { convertTransaction } from "./utils"; const CACHE_KEY = "mpb-bridge-fees-cache"; @@ -175,14 +175,21 @@ export const useChainBalances = () => { return { getBalanceForChain }; }; -export const useDebouncedTransactionHistory = (delay = 1000) => { +export const useDebouncedTransactionHistory = ( + delay = 1000, + bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls, + bridgeHistoryChainIds?: MPBBridgeHistoryChainIds +) => { const { historySorted: realTransactionHistory, initialLoading, refreshing, errorsByChain, refreshHistory - } = useMPBBridgeHistory() ?? {}; + } = useMPBBridgeHistory({ + readOnlyUrls: bridgeReadOnlyUrls, + chainIds: bridgeHistoryChainIds + }) ?? {}; const [debouncedHistory, setDebouncedHistory] = useState(realTransactionHistory); const timeoutRef = useRef(); diff --git a/packages/good-design/src/apps/bridge/mpbridge/index.ts b/packages/good-design/src/apps/bridge/mpbridge/index.ts index 637ff65b..5d603672 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/index.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/index.ts @@ -2,7 +2,15 @@ export { MPBBridge } from "./MPBBridge"; export { MPBBridgeController } from "./MPBBridgeController"; export { useMPBBridgeFeatureController } from "./feature/useMPBBridgeFeatureController"; export { BridgeTransactionCard, BridgeTransactionList } from "./MPBBridgeTransactionCard"; -export type { MPBBridgeProps, IMPBLimits, IMPBFees, BridgeProvider, BridgeTransaction } from "./types"; +export type { + MPBBridgeProps, + MPBBridgeHistoryChainIds, + MPBBridgeReadOnlyUrls, + IMPBLimits, + IMPBFees, + BridgeProvider, + BridgeTransaction +} from "./types"; export { ChainSelector } from "./ChainSelector"; export { BridgeProviderSelector } from "./BridgeProviderSelector"; diff --git a/packages/good-design/src/apps/bridge/mpbridge/types.ts b/packages/good-design/src/apps/bridge/mpbridge/types.ts index b5192ebe..830be1f4 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/types.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/types.ts @@ -1,7 +1,11 @@ import { BigNumber } from "ethers"; +import type { SupportedChains } from "@gooddollar/web3sdk-v2"; export type BridgeProvider = "axelar" | "layerzero"; +export type MPBBridgeReadOnlyUrls = Partial>; +export type MPBBridgeHistoryChainIds = SupportedChains[]; + export type BridgeTransaction = { id: string; transactionHash: string; @@ -62,6 +66,7 @@ export interface MPBBridgeProps { onBridgeStart?: (sourceChain: string, targetChain: string) => Promise; onBridgeFailed?: (error: Error) => void; onBridgeSuccess?: () => void; + bridgeReadOnlyUrls?: MPBBridgeReadOnlyUrls; bridgeProvider?: BridgeProvider; onBridgeProviderChange?: (provider: BridgeProvider) => void; } diff --git a/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.test.ts b/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.test.ts new file mode 100644 index 00000000..a115dc0c --- /dev/null +++ b/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.test.ts @@ -0,0 +1,30 @@ +import { TransactionStatus } from "@usedapp/core"; + +import { createTransactionDetails } from "./transactionHelpers"; + +jest.mock("@gooddollar/web3sdk-v2", () => ({ + getSourceChainId: jest.fn(() => 42220) +})); + +describe("createTransactionDetails", () => { + it("sets a date for the submitted transaction details", () => { + const date = new Date("2026-07-07T10:00:00.000Z"); + + const transaction = createTransactionDetails({ + amountWei: "10000000000000000000", + sourceChain: "celo", + targetChain: "xdc", + bridgeProvider: "layerzero", + bridgeStatus: { + status: "Success", + transaction: { hash: "0xbridge" } + } as Partial, + bridgeToTxHash: undefined, + date + }); + + expect(transaction.date).toBe(date); + expect(transaction.transactionHash).toBe("0xbridge"); + expect(transaction.amount).toBe("10.00"); + }); +}); diff --git a/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.ts b/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.ts index 427cdd3a..a2e848b6 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/utils/transactionHelpers.ts @@ -11,10 +11,11 @@ interface CreateTransactionDetailsParams { bridgeProvider: string; bridgeStatus: Partial | undefined; bridgeToTxHash: string | undefined; + date?: Date; } export const createTransactionDetails = (params: CreateTransactionDetailsParams): BridgeTransaction => { - const { amountWei, sourceChain, targetChain, bridgeProvider, bridgeStatus, bridgeToTxHash } = params; + const { amountWei, sourceChain, targetChain, bridgeProvider, bridgeStatus, bridgeToTxHash, date } = params; const amountBN = ethers.BigNumber.from(amountWei || "0"); const amountFormatted = utils.formatEther(amountBN); @@ -37,6 +38,7 @@ export const createTransactionDetails = (params: CreateTransactionDetailsParams) amount: parseFloat(amountFormatted).toFixed(2), bridgeProvider: bridgeProvider as "axelar" | "layerzero", status, + date: date ?? new Date(), chainId: sourceChainId }; }; diff --git a/packages/sdk-v2/src/hooks/useMulticallAtChain.tsx b/packages/sdk-v2/src/hooks/useMulticallAtChain.tsx index b3015c40..9cf7a073 100644 --- a/packages/sdk-v2/src/hooks/useMulticallAtChain.tsx +++ b/packages/sdk-v2/src/hooks/useMulticallAtChain.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react"; import { Result } from "@ethersproject/abi"; import { BigNumber } from "@ethersproject/bignumber"; -import { BaseProvider, JsonRpcProvider, Provider } from "@ethersproject/providers"; +import { BaseProvider, JsonRpcProvider, Provider, StaticJsonRpcProvider } from "@ethersproject/providers"; import { Contract } from "ethers"; import { noop } from "lodash"; @@ -86,7 +86,8 @@ export const useReadOnlyProvider = (chainId: number) => { return (factory as any)() as JsonRpcProvider; } - const provider = new JsonRpcProvider(factory as any); + // The chain is already known here, so avoid an extra network-detection RPC on rate-limited endpoints. + const provider = new StaticJsonRpcProvider(factory as any, chainId); provider.pollingInterval = pollingInterval; return provider; diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/index.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/index.ts index 2a7f1783..c8797fec 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/index.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/index.ts @@ -2,7 +2,9 @@ export * from "./useBridgeMonitoring"; export * from "./useBridgeValidators"; export * from "./useGetMPBBridgeData"; export * from "./useLayerZeroFee"; +export * from "./useMPBBridge.helpers"; export * from "./useMPBBridge"; +export * from "./useMPBBridgeHistory.helpers"; export * from "./useMPBBridgeHistory"; export * from "./useMPBG$TokenContract"; export * from "./useProductionG$Balance"; diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.helpers.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.helpers.ts new file mode 100644 index 00000000..2cc4e990 --- /dev/null +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.helpers.ts @@ -0,0 +1,16 @@ +export const getTransactionErrorMessage = (error: any): string => { + return ( + error?.error?.data?.message || + error?.error?.message || + error?.reason || + error?.data?.message || + error?.message || + "Transaction failed" + ); +}; + +export const isTransientBlockReadError = (error: any): boolean => { + const message = getTransactionErrorMessage(error).toLowerCase(); + + return message.includes("unknown block") || message.includes("no block"); +}; diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.test.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.test.ts new file mode 100644 index 00000000..a3b6ed32 --- /dev/null +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.test.ts @@ -0,0 +1,21 @@ +import { getTransactionErrorMessage, isTransientBlockReadError } from "./useMPBBridge.helpers"; + +describe("useMPBBridge transaction error helpers", () => { + it("detects unknown block read errors from common wallet/provider shapes", () => { + expect(isTransientBlockReadError(new Error("Unknown block"))).toBe(true); + expect(isTransientBlockReadError({ reason: "no block found" })).toBe(true); + expect(isTransientBlockReadError({ error: { data: { message: "UNKNOWN BLOCK" } } })).toBe(true); + }); + + it("does not classify regular transaction failures as transient block reads", () => { + expect(isTransientBlockReadError(new Error("user rejected transaction"))).toBe(false); + expect(isTransientBlockReadError({ error: { message: "execution reverted" } })).toBe(false); + }); + + it("extracts a readable transaction error message", () => { + expect(getTransactionErrorMessage({ error: { data: { message: "execution reverted" } } })).toBe( + "execution reverted" + ); + expect(getTransactionErrorMessage({ reason: "user rejected transaction" })).toBe("user rejected transaction"); + }); +}); diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.ts index 6360d70e..6a18eecf 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridge.ts @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { useContractFunction, useEthers } from "@usedapp/core"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { TransactionStatus, useContractFunction, useEthers } from "@usedapp/core"; import { ethers } from "ethers"; import { useSwitchNetwork } from "../../../contexts"; import { useG$Decimals } from "../../base/react"; @@ -19,6 +19,54 @@ import { useMPBG$TokenContract } from "./useMPBG$TokenContract"; import { useLayerZeroFee } from "./useLayerZeroFee"; import { useBridgeMonitoring } from "./useBridgeMonitoring"; import { useBridgeValidators } from "./useBridgeValidators"; +import { getTransactionErrorMessage, isTransientBlockReadError } from "./useMPBBridge.helpers"; + +const BRIDGE_TO_TRANSACTION_NAME = "MPBBridgeTo"; + +const createIdleTransactionStatus = (transactionName: string): TransactionStatus => ({ + status: "None", + transactionName +}); + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const waitForReceiptAfterSubmission = async ( + transaction: ethers.providers.TransactionResponse, + provider: ethers.providers.Provider +) => { + let lastError: unknown; + + for (let attempt = 0; attempt < 6; attempt++) { + try { + return await transaction.wait(); + } catch (error) { + if (!isTransientBlockReadError(error)) { + throw error; + } + + lastError = error; + await sleep(1000 * (attempt + 1)); + + try { + const receipt = await provider.getTransactionReceipt(transaction.hash); + + if (receipt) { + return receipt; + } + } catch (receiptError) { + if (!isTransientBlockReadError(receiptError)) { + throw receiptError; + } + + lastError = receiptError; + } + } + } + + console.warn("[useMPBBridge] Receipt polling hit transient block read errors after submission", lastError); + + return undefined; +}; export const useMPBBridge = (bridgeProvider: BridgeProvider = "axelar"): UseMPBBridgeReturn => { const bridgeLock = useRef(false); @@ -40,9 +88,88 @@ export const useMPBBridge = (bridgeProvider: BridgeProvider = "axelar"): UseMPBB transactionName: "MPBBridgeApprove" }); - const bridgeTo = useContractFunction(bridgeContractOrNull, "bridgeTo", { - transactionName: "MPBBridgeTo" - }); + const [bridgeToState, setBridgeToState] = useState(() => + createIdleTransactionStatus(BRIDGE_TO_TRANSACTION_NAME) + ); + + const resetBridgeToState = useCallback(() => { + setBridgeToState(createIdleTransactionStatus(BRIDGE_TO_TRANSACTION_NAME)); + }, []); + + const sendBridgeTo = useCallback( + async (...args: any[]) => { + if (!bridgeContractOrNull || !library || !account || !chainId) { + const errorMessage = "Bridge contract is not ready"; + setBridgeToState({ + status: "Exception", + errorMessage, + chainId, + transactionName: BRIDGE_TO_TRANSACTION_NAME + }); + return undefined; + } + + let transaction: ethers.providers.TransactionResponse | undefined; + + setBridgeToState({ + status: "PendingSignature", + chainId, + transactionName: BRIDGE_TO_TRANSACTION_NAME + }); + + try { + const signer = (library as ethers.providers.Web3Provider).getSigner(account); + const bridgeContractWithSigner = bridgeContractOrNull.connect(signer); + const submittedTransaction = (await bridgeContractWithSigner.bridgeTo( + ...args + )) as ethers.providers.TransactionResponse; + transaction = submittedTransaction; + + setBridgeToState({ + status: "Mining", + transaction: submittedTransaction, + chainId, + transactionName: BRIDGE_TO_TRANSACTION_NAME + }); + + const receipt = await waitForReceiptAfterSubmission(submittedTransaction, library); + const didTransactionFail = receipt?.status === 0; + + setBridgeToState({ + status: didTransactionFail ? "Fail" : "Success", + transaction: submittedTransaction, + receipt, + errorMessage: didTransactionFail ? "Bridge transaction failed" : undefined, + chainId, + transactionName: BRIDGE_TO_TRANSACTION_NAME + }); + + return receipt; + } catch (error: any) { + const errorMessage = getTransactionErrorMessage(error); + + setBridgeToState({ + status: transaction ? "Fail" : "Exception", + transaction, + errorMessage, + chainId, + transactionName: BRIDGE_TO_TRANSACTION_NAME + }); + + return undefined; + } + }, + [account, bridgeContractOrNull, chainId, library] + ); + + const bridgeTo = useMemo( + () => ({ + state: bridgeToState, + send: sendBridgeTo, + resetState: resetBridgeToState + }), + [bridgeToState, sendBridgeTo, resetBridgeToState] + ); const { computeLayerZeroFee } = useLayerZeroFee(bridgeContractOrNull, bridgeProvider, account); @@ -243,7 +370,7 @@ export const useMPBBridge = (bridgeProvider: BridgeProvider = "axelar"): UseMPBB [bridgeProvider, bridgeTo, computeLayerZeroFee] ); - // Helper function to execute bridge transaction (approve if needed, then bridge) + // Helper function to validate a bridge request, then ask for a fresh approval before bridging. const executeBridgeTransaction = useCallback( async (bridgeRequest: BridgeRequest, fees: any) => { const { source, target } = { @@ -265,26 +392,9 @@ export const useMPBBridge = (bridgeProvider: BridgeProvider = "axelar"): UseMPBB return; } - // Check allowance — skip approval when already sufficient - if (gdContract && account) { - try { - const allowance = await gdContract.allowance(account, bridgeContract.address); - const amountBN = ethers.BigNumber.from(bridgeRequest.amount); - - if (allowance.gte(amountBN)) { - console.log("[useMPBBridge] Allowance sufficient, executing bridgeTo directly"); - // executeBridgeTransfer handles its own locking - await executeBridgeTransfer(bridgeRequest); - return; - } - } catch (error) { - // Failed to check allowance, proceed with approval flow - } - } - void approve.send(bridgeContract.address, bridgeRequest.amount); }, - [bridgeProvider, bridgeContract, account, approve, validateBridgeTransaction, gdContract, executeBridgeTransfer] + [bridgeProvider, bridgeContract, approve, validateBridgeTransaction] ); useEffect(() => { diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts index 5f3e620c..6aba51e4 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts @@ -1,7 +1,10 @@ /* eslint-env jest */ import { + createAccountEventTopics, createBlockChunks, + dedupeLogs, + getAddressTopic, getErrorsByChain, mergeBridgeHistoryCache, MPBBridgeHistoryCache @@ -16,6 +19,35 @@ describe("useMPBBridgeHistory helpers", () => { ]); }); + it("creates indexed account topics for bridge history log filters", () => { + const account = "0xc1bA0ACD3030321851889309497663998D87D8d6"; + const accountTopic = "0x000000000000000000000000c1ba0acd3030321851889309497663998d87d8d6"; + + expect(getAddressTopic(account)).toBe(accountTopic); + expect(createAccountEventTopics("0xtopic", account)).toEqual([ + ["0xtopic", accountTopic], + ["0xtopic", null, accountTopic] + ]); + }); + + it("falls back to the event topic when the account address is unavailable", () => { + expect(createAccountEventTopics("0xtopic")).toEqual([["0xtopic"]]); + expect(createAccountEventTopics("0xtopic", "invalid")).toEqual([["0xtopic"]]); + }); + + it("dedupes logs that match both indexed account filters", () => { + expect( + dedupeLogs([ + { transactionHash: "0x1", logIndex: 2, value: "from-match" }, + { transactionHash: "0x1", logIndex: 2, value: "to-match" }, + { transactionHash: "0x2", logIndex: 1, value: "other" } + ]) + ).toEqual([ + { transactionHash: "0x1", logIndex: 2, value: "to-match" }, + { transactionHash: "0x2", logIndex: 1, value: "other" } + ]); + }); + it("merges history rows, prunes old cache entries, and keeps chain sync state", () => { const nowMs = new Date("2026-06-29T00:00:00.000Z").getTime(); const recentTimestamp = Math.floor(nowMs / 1000) - 60; diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts index e038d8f7..46916aef 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts @@ -65,6 +65,41 @@ export const createBlockChunks = (fromBlock: number, toBlock: number, chunkSize return chunks; }; +export const getAddressTopic = (address?: string) => { + if (!address) { + return undefined; + } + + const normalizedAddress = address.toLowerCase(); + + if (!/^0x[0-9a-f]{40}$/.test(normalizedAddress)) { + return undefined; + } + + return `0x${normalizedAddress.slice(2).padStart(64, "0")}`; +}; + +export const createAccountEventTopics = (eventTopic: string, account?: string) => { + const accountTopic = getAddressTopic(account); + + if (!accountTopic) { + return [[eventTopic]]; + } + + return [ + [eventTopic, accountTopic], + [eventTopic, null, accountTopic] + ]; +}; + +export const dedupeLogs = (logs: T[]) => { + const logsByKey = new Map(); + + logs.forEach(log => logsByKey.set(`${log.transactionHash}:${log.logIndex ?? 0}`, log)); + + return Array.from(logsByKey.values()); +}; + export const pruneExpiredEvents = (events: CachedBridgeEvent[], minTimestamp: number) => events.filter(event => Number(event.timestamp || 0) >= minTimestamp); diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts index dc32bb1b..6ebcebcc 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts @@ -2,9 +2,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useEthers } from "@usedapp/core"; import { ethers } from "ethers"; import { first, groupBy, sortBy } from "lodash"; +import Contracts from "@gooddollar/goodprotocol/releases/deployment.json"; +import { CONTRACT_TO_ABI } from "../../base/sdk"; import { AsyncStorage } from "../../storage"; import { SupportedChains, formatAmount } from "../../constants"; -import { useGetContract } from "../../base/react"; +import { useGetEnvChainId } from "../../base/react"; +import { useReadOnlyProvider } from "../../../hooks/useMulticallAtChain"; import { BridgeEventName, CachedBridgeEvent, @@ -12,14 +15,63 @@ import { MPBBridgeHistoryCache, HISTORY_BLOCK_CHUNK_SIZE, HISTORY_WINDOW_SECONDS, + createAccountEventTopics, createBlockChunks, + dedupeLogs, getErrorsByChain, mergeBridgeHistoryCache } from "./useMPBBridgeHistory.helpers"; -const HISTORY_CACHE_VERSION = 2; +const HISTORY_CACHE_VERSION = 6; const CHAIN_IDS = [SupportedChains.FUSE, SupportedChains.CELO, SupportedChains.MAINNET, SupportedChains.XDC]; -const MAX_PARALLEL_CHUNKS = 3; +const HISTORY_REQUEST_DELAY_MS = 500; +// Celo is a fast chain, so one day of history is far more than a few thousand blocks. +const HISTORY_FAST_SYNC_BLOCKS = HISTORY_BLOCK_CHUNK_SIZE * 240; + +export type MPBBridgeHistoryReadOnlyUrls = Partial>; + +export type UseMPBBridgeHistoryOptions = { + readOnlyUrls?: MPBBridgeHistoryReadOnlyUrls; + chainIds?: SupportedChains[]; +}; + +type ChainHistorySyncRange = { + fromBlock: number; + toBlock: number; + commitCursor: boolean; +}; + +type ChainHistoryEventSyncResult = { + chainId: SupportedChains; + eventName: BridgeEventName; + events: CachedBridgeEvent[]; + hasErrors: boolean; + chainState: ChainSyncState; +}; + +const useMPBBridgeHistoryContract = (chainId: SupportedChains, readOnlyUrls?: MPBBridgeHistoryReadOnlyUrls) => { + const { defaultEnv } = useGetEnvChainId(chainId); + const fallbackProvider = useReadOnlyProvider(chainId); + const overrideUrl = readOnlyUrls?.[chainId]; + + const provider = useMemo(() => { + if (overrideUrl) { + return new ethers.providers.StaticJsonRpcProvider(overrideUrl, chainId); + } + + return fallbackProvider; + }, [chainId, fallbackProvider, overrideUrl]); + + return useMemo(() => { + const deployment = Contracts[defaultEnv as keyof typeof Contracts] as { MpbBridge?: string } | undefined; + + if (!provider || !deployment?.MpbBridge) { + return; + } + + return new ethers.Contract(deployment.MpbBridge, CONTRACT_TO_ABI.MpbBridge.abi, provider); + }, [defaultEnv, provider]); +}; const hydrateCachedEvent = (event: CachedBridgeEvent) => { // Persist plain JSON in storage, then rebuild the BigNumber-shaped fields the rest of the hook expects. @@ -55,27 +107,88 @@ const hydrateCachedEvent = (event: CachedBridgeEvent) => { }; const getErrorMessage = (error: unknown) => { + const simplifyMessage = (message: string) => { + const normalizedMessage = message.toLowerCase(); + const status = message.match(/status=(\d+)/)?.[1]; + const code = message.match(/code=([A-Z_]+)/)?.[1]; + + if ( + normalizedMessage.includes("usage limit") || + normalizedMessage.includes("rate limit") || + normalizedMessage.includes("too many requests") || + message.includes("429") + ) { + return "RPC rate limit reached while refreshing history"; + } + + if (normalizedMessage.includes("forbidden") || message.includes("403")) { + return "RPC request was rejected while refreshing history"; + } + + if (normalizedMessage.includes("processing response error")) { + return "RPC response error while refreshing history"; + } + + if (message.includes("bad response")) { + return `RPC response error while refreshing history${ + status || code + ? ` (${[status ? `status=${status}` : "", code ? `code=${code}` : ""].filter(Boolean).join(", ")})` + : "" + }`; + } + + if (message.includes("could not detect network")) { + return `RPC network could not be detected${code ? ` (code=${code})` : ""}`; + } + + if (message.includes("missing response")) { + return `RPC did not return a response${code ? ` (code=${code})` : ""}`; + } + + return message.length > 240 ? `${message.slice(0, 237)}...` : message; + }; + if (error instanceof Error && error.message) { - return error.message; + return simplifyMessage(error.message); } if (typeof error === "string") { - return error; + return simplifyMessage(error); } return "Failed to load bridge history from RPC"; }; -const runWithConcurrency = async (tasks: Array<() => Promise>, concurrency: number): Promise => { +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const runSequentiallySettled = async ( + tasks: Array<() => Promise>, + delayMs = HISTORY_REQUEST_DELAY_MS, + options: { stopOnError?: boolean } = {} +) => { const results: T[] = []; + const errors: unknown[] = []; + + for (let index = 0; index < tasks.length; index += 1) { + let shouldStop = false; - // Batch chunk fetches instead of firing every getLogs request at once against the same public RPC. - for (let index = 0; index < tasks.length; index += concurrency) { - const nextResults = await Promise.all(tasks.slice(index, index + concurrency).map(task => task())); - results.push(...nextResults); + try { + results.push(await tasks[index]()); + } catch (error) { + errors.push(error); + shouldStop = Boolean(options.stopOnError); + } + + if (shouldStop) { + break; + } + + if (index < tasks.length - 1 && delayMs > 0) { + await delay(delayMs); + } } - return results; + return { results, errors }; }; const findHistoryStartBlock = async ( @@ -114,14 +227,19 @@ const findHistoryStartBlock = async ( const normalizeProviderLogs = ( contract: ethers.Contract, sourceChainId: SupportedChains, + eventName: BridgeEventName, logs: ethers.providers.Log[] ): CachedBridgeEvent[] => logs.flatMap(log => { try { const parsedLog = contract.interface.parseLog(log); - const targetChainId = parsedLog.args?.targetChainId || parsedLog.args?.[2]; - const amount = parsedLog.args?.amount || parsedLog.args?.[3]; - const timestamp = parsedLog.args?.timestamp || parsedLog.args?.[4]; + const targetChainId = + eventName === "BridgeRequest" ? parsedLog.args?.targetChainId || parsedLog.args?.[2] : sourceChainId; + const amount = + eventName === "BridgeRequest" + ? parsedLog.args?.amount || parsedLog.args?.normalizedAmount || parsedLog.args?.[3] + : parsedLog.args?.amount || parsedLog.args?.normalizedAmount || parsedLog.args?.[2]; + const timestamp = eventName === "BridgeRequest" ? parsedLog.args?.timestamp || parsedLog.args?.[4] : "0"; const bridge = parsedLog.args?.bridge || parsedLog.args?.[5]; const id = parsedLog.args?.id || parsedLog.args?.[6]; @@ -164,54 +282,113 @@ const fetchEventLogs = async ( contract: ethers.Contract, eventName: BridgeEventName, fromBlock: number, - toBlock: number + toBlock: number, + account?: string, + onChunkLogs?: (logs: ethers.providers.Log[]) => void ) => { if (fromBlock > toBlock) { - return [] as ethers.providers.Log[]; + return { + logs: [] as ethers.providers.Log[], + errors: [] as unknown[] + }; } const provider = contract.provider as ethers.providers.Provider; const topic = contract.interface.getEventTopic(eventName); - const chunks = createBlockChunks(fromBlock, toBlock, HISTORY_BLOCK_CHUNK_SIZE); - - // Public RPCs are sensitive to large eth_getLogs windows, so every request stays within 500 blocks. - const logsByChunk = await runWithConcurrency( - chunks.map( - chunk => () => - provider.getLogs({ - address: contract.address, - topics: [topic], - fromBlock: chunk.fromBlock, - toBlock: chunk.toBlock - }) - ), - MAX_PARALLEL_CHUNKS - ); + const accountTopics = createAccountEventTopics(topic, account); + const chunks = createBlockChunks(fromBlock, toBlock, HISTORY_BLOCK_CHUNK_SIZE).reverse(); + const logsByChunk: ethers.providers.Log[] = []; + const errors: unknown[] = []; + const topicPasses = accountTopics.length > 1 ? [[accountTopics[0]], accountTopics.slice(1)] : [accountTopics]; + + // Public RPCs are sensitive to bursty eth_getLogs traffic, so log requests stay within 500 blocks and run + // sequentially with a short pause between requests. Indexed wallet topics keep each request narrow. + for (let passIndex = 0; passIndex < topicPasses.length; passIndex += 1) { + const topicsForPass = topicPasses[passIndex]; + + for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) { + const chunk = chunks[chunkIndex]; + const { + results: chunkLogsByTopic, + errors: chunkErrors + } = await runSequentiallySettled( + topicsForPass.map( + topics => () => + provider.getLogs({ + address: contract.address, + topics: topics as ethers.providers.Filter["topics"], + fromBlock: chunk.fromBlock, + toBlock: chunk.toBlock + }) + ), + HISTORY_REQUEST_DELAY_MS, + { stopOnError: true } + ); + + if (chunkErrors.length) { + errors.push(...chunkErrors); + break; + } - return logsByChunk.flat(); + const chunkLogs = dedupeLogs(chunkLogsByTopic.flat()); + logsByChunk.push(...chunkLogs); + + if (chunkLogs.length) { + onChunkLogs?.(chunkLogs); + } + + if (chunkIndex < chunks.length - 1) { + await delay(HISTORY_REQUEST_DELAY_MS); + } + } + + if (errors.length || passIndex >= topicPasses.length - 1) { + break; + } + } + + return { + logs: dedupeLogs(logsByChunk), + errors + }; +}; + +const getPartialHistoryErrorMessage = (errors: unknown[]) => { + const uniqueMessages = Array.from(new Set(errors.map(getErrorMessage))); + const [firstMessage, secondMessage] = uniqueMessages; + + if (!secondMessage) { + return firstMessage || "Some history ranges could not refresh"; + } + + return `${firstMessage}; ${secondMessage}`; }; -const syncChainHistory = async ( +const getChainHistorySyncPlan = async ( chainId: SupportedChains, contract: ethers.Contract, - currentCache: MPBBridgeHistoryCache, - account?: string + currentCache: MPBBridgeHistoryCache ) => { const provider = contract.provider as ethers.providers.Provider; const latestBlock = await provider.getBlockNumber(); const chainState = currentCache.chains?.[chainId]; + const hasCachedChainEvents = Boolean( + currentCache.BridgeRequest?.some(event => event.sourceChainId === chainId) || + currentCache.ExecutedTransfer?.some(event => event.sourceChainId === chainId) + ); + const isWarmCache = chainState?.lastSyncedBlock !== undefined && hasCachedChainEvents; const targetTimestamp = Math.floor(Date.now() / 1000) - HISTORY_WINDOW_SECONDS; const fromBlock = - // Warm cache: resume from the last synced block. Cold cache: backfill only the rolling history window. - chainState?.lastSyncedBlock !== undefined - ? chainState.lastSyncedBlock + 1 + // Warm cache: resume from the last synced block. Cold cache: backfill the rolling history window. + isWarmCache + ? (chainState?.lastSyncedBlock as number) + 1 : await findHistoryStartBlock(provider, latestBlock, targetTimestamp); if (fromBlock > latestBlock) { return { chainId, - bridgeRequests: [] as CachedBridgeEvent[], - executedTransfers: [] as CachedBridgeEvent[], + latestBlock, + ranges: [] as ChainHistorySyncRange[], chainState: { lastSyncedBlock: latestBlock, lastSuccessfulSyncAt: Date.now() @@ -219,23 +396,77 @@ const syncChainHistory = async ( }; } - const [bridgeRequests, executedTransfers] = await Promise.all([ - fetchEventLogs(contract, "BridgeRequest", fromBlock, latestBlock), - fetchEventLogs(contract, "ExecutedTransfer", fromBlock, latestBlock) - ]); + const recentFromBlock = isWarmCache ? fromBlock : Math.max(fromBlock, latestBlock - HISTORY_FAST_SYNC_BLOCKS); + const ranges = + recentFromBlock > fromBlock + ? [ + { fromBlock: recentFromBlock, toBlock: latestBlock, commitCursor: false }, + { fromBlock, toBlock: recentFromBlock - 1, commitCursor: true } + ] + : [{ fromBlock, toBlock: latestBlock, commitCursor: true }]; + + return { + chainId, + latestBlock, + chainState, + ranges + }; +}; + +const syncChainHistoryRange = async ( + chainId: SupportedChains, + contract: ethers.Contract, + eventName: BridgeEventName, + range: ChainHistorySyncRange, + latestBlock: number, + chainState?: ChainSyncState, + account?: string, + onEvents?: (eventName: BridgeEventName, events: CachedBridgeEvent[]) => void +): Promise => { + const eventResult = await fetchEventLogs( + contract, + eventName, + range.fromBlock, + range.toBlock, + account, + logs => { + const events = filterEventsForAccount(normalizeProviderLogs(contract, chainId, eventName, logs), account); + + if (events.length) { + onEvents?.(eventName, events); + } + } + ); + const errors = eventResult.errors; return { chainId, - bridgeRequests: filterEventsForAccount(normalizeProviderLogs(contract, chainId, bridgeRequests), account), - executedTransfers: filterEventsForAccount(normalizeProviderLogs(contract, chainId, executedTransfers), account), - chainState: { - lastSyncedBlock: latestBlock, - lastSuccessfulSyncAt: Date.now() - } satisfies ChainSyncState + eventName, + events: filterEventsForAccount(normalizeProviderLogs(contract, chainId, eventName, eventResult.logs), account), + hasErrors: errors.length > 0, + chainState: + errors.length > 0 + ? ({ + ...(chainState || {}), + error: { + message: getPartialHistoryErrorMessage(errors), + updatedAt: Date.now() + } + } satisfies ChainSyncState) + : !range.commitCursor + ? ({ + ...(chainState || {}), + lastSuccessfulSyncAt: Date.now(), + error: undefined + } satisfies ChainSyncState) + : ({ + lastSyncedBlock: latestBlock, + lastSuccessfulSyncAt: Date.now() + } satisfies ChainSyncState) }; }; -export const useMPBBridgeHistory = () => { +export const useMPBBridgeHistory = ({ readOnlyUrls, chainIds }: UseMPBBridgeHistoryOptions = {}) => { const { account } = useEthers(); const [cacheLoaded, setCacheLoaded] = useState(false); const [historyCache, setHistoryCache] = useState({}); @@ -243,10 +474,10 @@ export const useMPBBridgeHistory = () => { const [syncing, setSyncing] = useState(false); const historyCacheRef = useRef({}); - const fuseBridgeContract = useGetContract("MpbBridge", true, "base", SupportedChains.FUSE); - const celoBridgeContract = useGetContract("MpbBridge", true, "base", SupportedChains.CELO); - const mainnetBridgeContract = useGetContract("MpbBridge", true, "base", SupportedChains.MAINNET); - const xdcBridgeContract = useGetContract("MpbBridge", true, "base", SupportedChains.XDC); + const fuseBridgeContract = useMPBBridgeHistoryContract(SupportedChains.FUSE, readOnlyUrls); + const celoBridgeContract = useMPBBridgeHistoryContract(SupportedChains.CELO, readOnlyUrls); + const mainnetBridgeContract = useMPBBridgeHistoryContract(SupportedChains.MAINNET, readOnlyUrls); + const xdcBridgeContract = useMPBBridgeHistoryContract(SupportedChains.XDC, readOnlyUrls); const contracts = useMemo( () => ({ @@ -257,6 +488,15 @@ export const useMPBBridgeHistory = () => { }), [celoBridgeContract, fuseBridgeContract, mainnetBridgeContract, xdcBridgeContract] ); + const activeChainIds = useMemo(() => { + const requestedChainIds = chainIds?.length ? chainIds : CHAIN_IDS; + const supportedChainIds = new Set(CHAIN_IDS); + const uniqueChainIds = Array.from( + new Set(requestedChainIds.filter((chainId): chainId is SupportedChains => supportedChainIds.has(chainId))) + ); + + return uniqueChainIds.length ? uniqueChainIds : CHAIN_IDS; + }, [chainIds]); const cacheKey = useMemo(() => { if (!account) return undefined; @@ -314,7 +554,7 @@ export const useMPBBridgeHistory = () => { return; } - const chainContracts = CHAIN_IDS.flatMap(chainId => + const chainContracts = activeChainIds.flatMap(chainId => contracts[chainId] ? [{ chainId, contract: contracts[chainId] as ethers.Contract }] : [] ); @@ -327,50 +567,11 @@ export const useMPBBridgeHistory = () => { // Keep cached rows on screen and expose a separate refreshing state while each chain sync runs. setSyncing(true); - const syncHistory = async () => { - const currentCache = historyCacheRef.current; - // Sync every chain independently so a single failing RPC cannot block the others from updating cache. - const settledChains = await Promise.allSettled( - chainContracts.map(({ chainId, contract }) => syncChainHistory(chainId, contract, currentCache, account)) - ); - - if (cancelled) { - return; - } - - const nextChains: Partial> = {}; - const nextBridgeRequests: CachedBridgeEvent[] = []; - const nextExecutedTransfers: CachedBridgeEvent[] = []; - - settledChains.forEach((result, index) => { - const { chainId } = chainContracts[index]; - - if (result.status === "fulfilled") { - // Successful chains contribute rows and advance only their own cursor/error state. - nextChains[chainId] = result.value.chainState; - nextBridgeRequests.push(...result.value.bridgeRequests); - nextExecutedTransfers.push(...result.value.executedTransfers); - return; - } - - // Failed chains keep their last good cursor and surface a chain-specific error for the UI. - nextChains[chainId] = { - ...(currentCache.chains?.[chainId] || {}), - error: { - message: getErrorMessage(result.reason), - updatedAt: Date.now() - } - }; - }); - - const nextCache = mergeBridgeHistoryCache( - currentCache, - { - BridgeRequest: nextBridgeRequests, - ExecutedTransfer: nextExecutedTransfers - }, - nextChains - ); + const publishHistoryCache = ( + nextEvents: Partial>, + nextChains: Partial> + ) => { + const nextCache = mergeBridgeHistoryCache(historyCacheRef.current, nextEvents, nextChains); setHistoryCache(nextCache); historyCacheRef.current = nextCache; @@ -380,6 +581,71 @@ export const useMPBBridgeHistory = () => { ); }; + const syncHistory = async () => { + // Sync every chain independently and sequentially so one RPC cannot rate-limit the others. + for (const { chainId, contract } of chainContracts) { + try { + const plan = await getChainHistorySyncPlan(chainId, contract, historyCacheRef.current); + + if (cancelled) { + return; + } + + if (!plan.ranges.length) { + publishHistoryCache({}, { [chainId]: plan.chainState }); + continue; + } + + let chainHadErrors = false; + + for (const range of plan.ranges) { + for (const eventName of ["BridgeRequest", "ExecutedTransfer"] as BridgeEventName[]) { + const shouldCommitCursor = eventName === "ExecutedTransfer" && range.commitCursor && !chainHadErrors; + const result = await syncChainHistoryRange( + chainId, + contract, + eventName, + { ...range, commitCursor: shouldCommitCursor }, + plan.latestBlock, + historyCacheRef.current.chains?.[chainId], + account, + (chunkEventName, events) => { + if (!cancelled) { + publishHistoryCache({ [chunkEventName]: events }, {}); + } + } + ); + + if (cancelled) { + return; + } + + chainHadErrors = chainHadErrors || result.hasErrors; + publishHistoryCache({ [result.eventName]: result.events }, { [chainId]: result.chainState }); + } + } + } catch (reason) { + if (cancelled) { + return; + } + + // Failed chains keep their last good cursor and surface a chain-specific error for the UI. + publishHistoryCache( + {}, + { + [chainId]: { + ...(historyCacheRef.current.chains?.[chainId] || {}), + error: { + message: getErrorMessage(reason), + updatedAt: Date.now() + } + } + } + ); + } + } + }; + void syncHistory().finally(() => { if (!cancelled) { setSyncing(false); @@ -389,17 +655,21 @@ export const useMPBBridgeHistory = () => { return () => { cancelled = true; }; - }, [account, cacheKey, cacheLoaded, contracts, refreshTick]); + }, [account, activeChainIds, cacheKey, cacheLoaded, contracts, refreshTick]); const refreshHistory = useCallback(() => { setRefreshTick(current => current + 1); }, []); return useMemo(() => { - const errorsByChain = getErrorsByChain(historyCache); - const hasCachedRows = Boolean( - (historyCache.BridgeRequest || []).length || (historyCache.ExecutedTransfer || []).length - ); + const allErrorsByChain = getErrorsByChain(historyCache); + const activeErrorsByChain = activeChainIds.reduce((result, chainId) => { + if (allErrorsByChain[chainId]) { + result[chainId] = allErrorsByChain[chainId]; + } + + return result; + }, {} as Record); if (!cacheLoaded) { return { @@ -407,7 +677,7 @@ export const useMPBBridgeHistory = () => { historySorted: undefined, initialLoading: true, refreshing: false, - errorsByChain, + errorsByChain: activeErrorsByChain, refreshHistory }; } @@ -462,10 +732,10 @@ export const useMPBBridgeHistory = () => { return { history: historySorted, historySorted, - initialLoading: syncing && !hasCachedRows, + initialLoading: false, refreshing: syncing, - errorsByChain, + errorsByChain: activeErrorsByChain, refreshHistory }; - }, [account, cacheLoaded, historyCache, refreshHistory, syncing]); + }, [account, activeChainIds, cacheLoaded, historyCache, refreshHistory, syncing]); }; From e3113ef24911214f26e1917fb3e3cbe6eb0d8e94 Mon Sep 17 00:00:00 2001 From: LewisB Date: Wed, 15 Jul 2026 23:15:11 +0700 Subject: [PATCH 7/7] simplify solution and shorten range --- .gitignore | 3 +- .../bridge/mpbridge/TransactionHistory.tsx | 30 +- .../feature/useMPBBridgeViewController.ts | 6 + .../hooks/useMPBBridgeHistory.helpers.test.ts | 20 +- .../hooks/useMPBBridgeHistory.helpers.ts | 29 +- .../sdk/mpbridge/hooks/useMPBBridgeHistory.ts | 318 +++++++----------- 6 files changed, 175 insertions(+), 231 deletions(-) diff --git a/.gitignore b/.gitignore index 94096bab..ce4def8c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ yalc.lock .continueignore e2e.log docs/specs/** -codex-resume \ No newline at end of file +codex-resume +.bounties \ No newline at end of file diff --git a/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx b/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx index 5a5ccc34..7197349a 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx +++ b/packages/good-design/src/apps/bridge/mpbridge/TransactionHistory.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Box, Button, HStack, Spinner, Text, VStack } from "native-base"; +import { ExplorerLink } from "../../../core"; import { BridgeTransactionList } from "./MPBBridgeTransactionCard"; import { capitalizeChain, getChainName } from "./utils"; @@ -8,6 +9,8 @@ interface TransactionHistoryProps { historyLoading: boolean; historyRefreshing: boolean; historyErrorsByChain: Record; + explorerChainId?: number; + explorerAddress?: string; onRefresh: () => void; onTxDetailsPress: (tx: any) => void; } @@ -17,6 +20,8 @@ export const TransactionHistory: React.FC = ({ historyLoading, historyRefreshing, historyErrorsByChain, + explorerChainId, + explorerAddress, onRefresh, onTxDetailsPress }) => { @@ -41,6 +46,22 @@ export const TransactionHistory: React.FC = ({ Refresh + + + History builds as you use this bridge. We check up to the latest 5,000 blocks on each supported network. + + + Older transactions or activity from another device may not appear. + + {explorerChainId && explorerAddress ? ( + + ) : null} + {historyRefreshing && !historyLoading ? ( @@ -53,11 +74,12 @@ export const TransactionHistory: React.FC = ({ - Some networks could not refresh right now. + Some transaction history could not be refreshed. - {errorEntries.map(([chainId, message]) => ( + {errorEntries.map(([chainId]) => ( - {capitalizeChain(getChainName(Number(chainId)))}: {message} + Could not fetch history for {capitalizeChain(getChainName(Number(chainId)))} at this time. You can try + to reload or try later. ))} @@ -77,7 +99,7 @@ export const TransactionHistory: React.FC = ({ ) : ( - No recent bridge transactions found + No bridge transactions found in the latest 5,000 blocks Make sure your wallet is connected to see your bridge transactions diff --git a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts index 5ab7535a..5310a1b5 100644 --- a/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts +++ b/packages/good-design/src/apps/bridge/mpbridge/feature/useMPBBridgeViewController.ts @@ -1,4 +1,5 @@ import { useEffect, useCallback, useState, useMemo, useRef } from "react"; +import { useEthers } from "@usedapp/core"; import { SupportedChains, deriveMPBBridgeFlowState } from "@gooddollar/web3sdk-v2"; import { ethers } from "ethers"; @@ -121,6 +122,8 @@ export interface MPBBridgeViewModel { historyLoading: boolean; historyRefreshing: boolean; historyErrorsByChain: Record; + explorerChainId?: number; + explorerAddress?: string; onRefresh: () => void; onTxDetailsPress: (tx: any) => void; }; @@ -144,6 +147,7 @@ export const useMPBBridgeViewController = ({ bridgeProvider: propBridgeProvider, onBridgeProviderChange }: MPBBridgeProps): MPBBridgeViewModel => { + const { account, chainId } = useEthers(); const [isBridging, setBridging] = useState(false); const [localBridgeProvider, setLocalBridgeProvider] = useState("axelar"); const bridgeProvider = propBridgeProvider || localBridgeProvider; @@ -613,6 +617,8 @@ export const useMPBBridgeViewController = ({ historyLoading, historyRefreshing, historyErrorsByChain, + explorerChainId: chainId, + explorerAddress: account, onRefresh: refreshHistory, onTxDetailsPress } diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts index 6aba51e4..9dd1cd74 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.test.ts @@ -6,6 +6,7 @@ import { dedupeLogs, getAddressTopic, getErrorsByChain, + getHistoryStartBlock, mergeBridgeHistoryCache, MPBBridgeHistoryCache } from "./useMPBBridgeHistory.helpers"; @@ -19,6 +20,13 @@ describe("useMPBBridgeHistory helpers", () => { ]); }); + it("limits cold and stale-cursor syncs to the latest 5,000 blocks", () => { + expect(getHistoryStartBlock(10_000)).toBe(5001); + expect(getHistoryStartBlock(10_000, 100)).toBe(5001); + expect(getHistoryStartBlock(10_000, 9800)).toBe(9801); + expect(getHistoryStartBlock(100)).toBe(0); + }); + it("creates indexed account topics for bridge history log filters", () => { const account = "0xc1bA0ACD3030321851889309497663998D87D8d6"; const accountTopic = "0x000000000000000000000000c1ba0acd3030321851889309497663998d87d8d6"; @@ -48,10 +56,9 @@ describe("useMPBBridgeHistory helpers", () => { ]); }); - it("merges history rows, prunes old cache entries, and keeps chain sync state", () => { + it("merges cached history rows and keeps chain sync state", () => { const nowMs = new Date("2026-06-29T00:00:00.000Z").getTime(); const recentTimestamp = Math.floor(nowMs / 1000) - 60; - const oldTimestamp = Math.floor(nowMs / 1000) - 31 * 24 * 60 * 60; const currentCache: MPBBridgeHistoryCache = { BridgeRequest: [ { @@ -65,7 +72,7 @@ describe("useMPBBridgeHistory helpers", () => { to: "0xto", targetChainId: "42220", amount: "10", - timestamp: oldTimestamp.toString(), + timestamp: "1", id: "1" }, { @@ -142,12 +149,11 @@ describe("useMPBBridgeHistory helpers", () => { updatedAt: nowMs } } - }, - nowMs + } ); - expect(nextCache.BridgeRequest).toHaveLength(1); - expect(nextCache.BridgeRequest?.[0].transactionHash).toBe("0xkeep-updated"); + expect(nextCache.BridgeRequest).toHaveLength(2); + expect(nextCache.BridgeRequest?.[1].transactionHash).toBe("0xkeep-updated"); expect(nextCache.ExecutedTransfer).toHaveLength(1); expect(nextCache.chains?.[122]).toEqual({ lastSyncedBlock: 40, diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts index 46916aef..c4e8add0 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.helpers.ts @@ -33,11 +33,12 @@ export type MPBBridgeHistoryCache = { chains?: Partial>; }; -// The cache only keeps a rolling 30-day history so first paint stays useful without growing forever. -export const HISTORY_WINDOW_DAYS = 30; -export const HISTORY_WINDOW_SECONDS = HISTORY_WINDOW_DAYS * 24 * 60 * 60; // Public RPCs were failing on large getLogs windows, so every sync is split into small ranges. export const HISTORY_BLOCK_CHUNK_SIZE = 500; +export const HISTORY_LOOKBACK_BLOCKS = 5000; + +export const getHistoryStartBlock = (latestBlock: number, lastSyncedBlock?: number) => + Math.max(0, latestBlock - HISTORY_LOOKBACK_BLOCKS + 1, (lastSyncedBlock ?? -1) + 1); const getEventCacheKey = (event: CachedBridgeEvent) => event.id ? `${event.sourceChainId}:${event.id}` : `${event.sourceChainId}:${event.transactionHash}`; @@ -100,29 +101,21 @@ export const dedupeLogs = - events.filter(event => Number(event.timestamp || 0) >= minTimestamp); - -export const getHistoryWindowStartTimestamp = (nowMs = Date.now()) => Math.floor(nowMs / 1000) - HISTORY_WINDOW_SECONDS; - export const mergeBridgeHistoryCache = ( current: MPBBridgeHistoryCache, nextEvents: Partial>, - nextChains: Partial>, - nowMs = Date.now() + nextChains: Partial> ): MPBBridgeHistoryCache => { - const minTimestamp = getHistoryWindowStartTimestamp(nowMs); const mergedRequests = new Map(); const mergedTransfers = new Map(); - // Merge new rows into the cached window and let the event key dedupe repeated fetches across refreshes. - pruneExpiredEvents((current.BridgeRequest || []).concat(nextEvents.BridgeRequest || []), minTimestamp).forEach( - event => mergedRequests.set(getEventCacheKey(event), event) - ); + (current.BridgeRequest || []) + .concat(nextEvents.BridgeRequest || []) + .forEach(event => mergedRequests.set(getEventCacheKey(event), event)); - pruneExpiredEvents((current.ExecutedTransfer || []).concat(nextEvents.ExecutedTransfer || []), minTimestamp).forEach( - event => mergedTransfers.set(getEventCacheKey(event), event) - ); + (current.ExecutedTransfer || []) + .concat(nextEvents.ExecutedTransfer || []) + .forEach(event => mergedTransfers.set(getEventCacheKey(event), event)); return { BridgeRequest: sortBridgeEvents(Array.from(mergedRequests.values())), diff --git a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts index 6ebcebcc..8e2d8f0c 100644 --- a/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts +++ b/packages/sdk-v2/src/sdk/mpbridge/hooks/useMPBBridgeHistory.ts @@ -14,19 +14,17 @@ import { ChainSyncState, MPBBridgeHistoryCache, HISTORY_BLOCK_CHUNK_SIZE, - HISTORY_WINDOW_SECONDS, createAccountEventTopics, createBlockChunks, dedupeLogs, getErrorsByChain, + getHistoryStartBlock, mergeBridgeHistoryCache } from "./useMPBBridgeHistory.helpers"; -const HISTORY_CACHE_VERSION = 6; +const HISTORY_CACHE_VERSION = 7; const CHAIN_IDS = [SupportedChains.FUSE, SupportedChains.CELO, SupportedChains.MAINNET, SupportedChains.XDC]; const HISTORY_REQUEST_DELAY_MS = 500; -// Celo is a fast chain, so one day of history is far more than a few thousand blocks. -const HISTORY_FAST_SYNC_BLOCKS = HISTORY_BLOCK_CHUNK_SIZE * 240; export type MPBBridgeHistoryReadOnlyUrls = Partial>; @@ -38,15 +36,13 @@ export type UseMPBBridgeHistoryOptions = { type ChainHistorySyncRange = { fromBlock: number; toBlock: number; - commitCursor: boolean; }; type ChainHistoryEventSyncResult = { chainId: SupportedChains; eventName: BridgeEventName; events: CachedBridgeEvent[]; - hasErrors: boolean; - chainState: ChainSyncState; + error?: string; }; const useMPBBridgeHistoryContract = (chainId: SupportedChains, readOnlyUrls?: MPBBridgeHistoryReadOnlyUrls) => { @@ -161,69 +157,6 @@ const getErrorMessage = (error: unknown) => { const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -const runSequentiallySettled = async ( - tasks: Array<() => Promise>, - delayMs = HISTORY_REQUEST_DELAY_MS, - options: { stopOnError?: boolean } = {} -) => { - const results: T[] = []; - const errors: unknown[] = []; - - for (let index = 0; index < tasks.length; index += 1) { - let shouldStop = false; - - try { - results.push(await tasks[index]()); - } catch (error) { - errors.push(error); - shouldStop = Boolean(options.stopOnError); - } - - if (shouldStop) { - break; - } - - if (index < tasks.length - 1 && delayMs > 0) { - await delay(delayMs); - } - } - - return { results, errors }; -}; - -const findHistoryStartBlock = async ( - provider: ethers.providers.Provider, - latestBlockNumber: number, - targetTimestamp: number -) => { - const latestBlock = await provider.getBlock(latestBlockNumber); - - if (!latestBlock || latestBlock.timestamp <= targetTimestamp) { - return latestBlockNumber; - } - - let low = 0; - let high = latestBlockNumber; - - // Binary search keeps the 30-day backfill exact without relying on chain-specific block-time guesses. - while (low < high) { - const mid = Math.floor((low + high) / 2); - const block = await provider.getBlock(mid); - - if (!block) { - throw new Error(`Failed to fetch block ${mid} while backfilling bridge history`); - } - - if (block.timestamp < targetTimestamp) { - low = mid + 1; - } else { - high = mid; - } - } - - return low; -}; - const normalizeProviderLogs = ( contract: ethers.Contract, sourceChainId: SupportedChains, @@ -308,33 +241,27 @@ const fetchEventLogs = async ( for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) { const chunk = chunks[chunkIndex]; - const { - results: chunkLogsByTopic, - errors: chunkErrors - } = await runSequentiallySettled( - topicsForPass.map( - topics => () => + try { + const chunkLogsByTopic = await Promise.all( + topicsForPass.map(topics => provider.getLogs({ address: contract.address, topics: topics as ethers.providers.Filter["topics"], fromBlock: chunk.fromBlock, toBlock: chunk.toBlock }) - ), - HISTORY_REQUEST_DELAY_MS, - { stopOnError: true } - ); + ) + ); - if (chunkErrors.length) { - errors.push(...chunkErrors); - break; - } - - const chunkLogs = dedupeLogs(chunkLogsByTopic.flat()); - logsByChunk.push(...chunkLogs); + const chunkLogs = dedupeLogs(chunkLogsByTopic.flat()); + logsByChunk.push(...chunkLogs); - if (chunkLogs.length) { - onChunkLogs?.(chunkLogs); + if (chunkLogs.length) { + onChunkLogs?.(chunkLogs); + } + } catch (error) { + errors.push(error); + break; } if (chunkIndex < chunks.length - 1) { @@ -372,23 +299,13 @@ const getChainHistorySyncPlan = async ( const provider = contract.provider as ethers.providers.Provider; const latestBlock = await provider.getBlockNumber(); const chainState = currentCache.chains?.[chainId]; - const hasCachedChainEvents = Boolean( - currentCache.BridgeRequest?.some(event => event.sourceChainId === chainId) || - currentCache.ExecutedTransfer?.some(event => event.sourceChainId === chainId) - ); - const isWarmCache = chainState?.lastSyncedBlock !== undefined && hasCachedChainEvents; - const targetTimestamp = Math.floor(Date.now() / 1000) - HISTORY_WINDOW_SECONDS; - const fromBlock = - // Warm cache: resume from the last synced block. Cold cache: backfill the rolling history window. - isWarmCache - ? (chainState?.lastSyncedBlock as number) + 1 - : await findHistoryStartBlock(provider, latestBlock, targetTimestamp); + const fromBlock = getHistoryStartBlock(latestBlock, chainState?.lastSyncedBlock); if (fromBlock > latestBlock) { return { chainId, latestBlock, - ranges: [] as ChainHistorySyncRange[], + range: undefined, chainState: { lastSyncedBlock: latestBlock, lastSuccessfulSyncAt: Date.now() @@ -396,20 +313,11 @@ const getChainHistorySyncPlan = async ( }; } - const recentFromBlock = isWarmCache ? fromBlock : Math.max(fromBlock, latestBlock - HISTORY_FAST_SYNC_BLOCKS); - const ranges = - recentFromBlock > fromBlock - ? [ - { fromBlock: recentFromBlock, toBlock: latestBlock, commitCursor: false }, - { fromBlock, toBlock: recentFromBlock - 1, commitCursor: true } - ] - : [{ fromBlock, toBlock: latestBlock, commitCursor: true }]; - return { chainId, latestBlock, chainState, - ranges + range: { fromBlock, toBlock: latestBlock } satisfies ChainHistorySyncRange }; }; @@ -418,51 +326,23 @@ const syncChainHistoryRange = async ( contract: ethers.Contract, eventName: BridgeEventName, range: ChainHistorySyncRange, - latestBlock: number, - chainState?: ChainSyncState, account?: string, onEvents?: (eventName: BridgeEventName, events: CachedBridgeEvent[]) => void ): Promise => { - const eventResult = await fetchEventLogs( - contract, - eventName, - range.fromBlock, - range.toBlock, - account, - logs => { - const events = filterEventsForAccount(normalizeProviderLogs(contract, chainId, eventName, logs), account); - - if (events.length) { - onEvents?.(eventName, events); - } + const eventResult = await fetchEventLogs(contract, eventName, range.fromBlock, range.toBlock, account, logs => { + const events = filterEventsForAccount(normalizeProviderLogs(contract, chainId, eventName, logs), account); + + if (events.length) { + onEvents?.(eventName, events); } - ); + }); const errors = eventResult.errors; return { chainId, eventName, events: filterEventsForAccount(normalizeProviderLogs(contract, chainId, eventName, eventResult.logs), account), - hasErrors: errors.length > 0, - chainState: - errors.length > 0 - ? ({ - ...(chainState || {}), - error: { - message: getPartialHistoryErrorMessage(errors), - updatedAt: Date.now() - } - } satisfies ChainSyncState) - : !range.commitCursor - ? ({ - ...(chainState || {}), - lastSuccessfulSyncAt: Date.now(), - error: undefined - } satisfies ChainSyncState) - : ({ - lastSyncedBlock: latestBlock, - lastSuccessfulSyncAt: Date.now() - } satisfies ChainSyncState) + error: errors.length ? getPartialHistoryErrorMessage(errors) : undefined }; }; @@ -473,6 +353,7 @@ export const useMPBBridgeHistory = ({ readOnlyUrls, chainIds }: UseMPBBridgeHist const [refreshTick, setRefreshTick] = useState(0); const [syncing, setSyncing] = useState(false); const historyCacheRef = useRef({}); + const storageWriteRef = useRef>(Promise.resolve()); const fuseBridgeContract = useMPBBridgeHistoryContract(SupportedChains.FUSE, readOnlyUrls); const celoBridgeContract = useMPBBridgeHistoryContract(SupportedChains.CELO, readOnlyUrls); @@ -575,77 +456,95 @@ export const useMPBBridgeHistory = ({ readOnlyUrls, chainIds }: UseMPBBridgeHist setHistoryCache(nextCache); historyCacheRef.current = nextCache; - // Persist the merged cache after every refresh so the next mount can render immediately from storage. - void AsyncStorage.setItem(cacheKey, nextCache).catch(error => - console.warn("Failed to store MPB bridge history cache", error) - ); + storageWriteRef.current = storageWriteRef.current + .then(() => AsyncStorage.setItem(cacheKey, nextCache)) + .catch(error => console.warn("Failed to store MPB bridge history cache", error)); }; - const syncHistory = async () => { - // Sync every chain independently and sequentially so one RPC cannot rate-limit the others. - for (const { chainId, contract } of chainContracts) { - try { - const plan = await getChainHistorySyncPlan(chainId, contract, historyCacheRef.current); - - if (cancelled) { - return; - } + const syncChain = async ({ chainId, contract }: (typeof chainContracts)[number]) => { + try { + const plan = await getChainHistorySyncPlan(chainId, contract, historyCacheRef.current); - if (!plan.ranges.length) { - publishHistoryCache({}, { [chainId]: plan.chainState }); - continue; - } + if (cancelled) { + return; + } - let chainHadErrors = false; - - for (const range of plan.ranges) { - for (const eventName of ["BridgeRequest", "ExecutedTransfer"] as BridgeEventName[]) { - const shouldCommitCursor = eventName === "ExecutedTransfer" && range.commitCursor && !chainHadErrors; - const result = await syncChainHistoryRange( - chainId, - contract, - eventName, - { ...range, commitCursor: shouldCommitCursor }, - plan.latestBlock, - historyCacheRef.current.chains?.[chainId], - account, - (chunkEventName, events) => { - if (!cancelled) { - publishHistoryCache({ [chunkEventName]: events }, {}); - } - } - ); + if (!plan.range) { + publishHistoryCache({}, { [chainId]: plan.chainState }); + return; + } - if (cancelled) { - return; + let chainError: string | undefined; + + for (const eventName of ["BridgeRequest", "ExecutedTransfer"] as BridgeEventName[]) { + const result = await syncChainHistoryRange( + chainId, + contract, + eventName, + plan.range, + account, + (chunkEventName, events) => { + if (!cancelled) { + publishHistoryCache({ [chunkEventName]: events }, {}); } - - chainHadErrors = chainHadErrors || result.hasErrors; - publishHistoryCache({ [result.eventName]: result.events }, { [chainId]: result.chainState }); } - } - } catch (reason) { + ); + if (cancelled) { return; } - // Failed chains keep their last good cursor and surface a chain-specific error for the UI. - publishHistoryCache( - {}, - { - [chainId]: { - ...(historyCacheRef.current.chains?.[chainId] || {}), - error: { - message: getErrorMessage(reason), - updatedAt: Date.now() + chainError = chainError || result.error; + publishHistoryCache({ [result.eventName]: result.events }, {}); + } + + publishHistoryCache( + {}, + { + [chainId]: chainError + ? { + ...(historyCacheRef.current.chains?.[chainId] || {}), + error: { message: chainError, updatedAt: Date.now() } + } + : { + lastSyncedBlock: plan.latestBlock, + lastSuccessfulSyncAt: Date.now() } + } + ); + } catch (reason) { + if (cancelled) { + return; + } + + publishHistoryCache( + {}, + { + [chainId]: { + ...(historyCacheRef.current.chains?.[chainId] || {}), + error: { + message: getErrorMessage(reason), + updatedAt: Date.now() } } - ); - } + } + ); } }; + const syncHistory = async () => { + // Each chain has its own RPC, cursor, and error state, so bounded recent scans can run together. + await Promise.all( + chainContracts.map(async chainContract => { + try { + await syncChain(chainContract); + } catch (reason) { + console.warn("Unexpected MPB bridge history sync failure", reason); + } + }) + ); + }; + void syncHistory().finally(() => { if (!cancelled) { setSyncing(false); @@ -658,8 +557,25 @@ export const useMPBBridgeHistory = ({ readOnlyUrls, chainIds }: UseMPBBridgeHist }, [account, activeChainIds, cacheKey, cacheLoaded, contracts, refreshTick]); const refreshHistory = useCallback(() => { + const clearedCache: MPBBridgeHistoryCache = { + ...historyCacheRef.current, + chains: Object.entries(historyCacheRef.current.chains || {}).reduce((result, [chainId, state]) => { + result[Number(chainId)] = { ...state, error: undefined }; + return result; + }, {} as Partial>) + }; + + historyCacheRef.current = clearedCache; + setHistoryCache(clearedCache); + + if (cacheKey) { + storageWriteRef.current = storageWriteRef.current + .then(() => AsyncStorage.setItem(cacheKey, clearedCache)) + .catch(error => console.warn("Failed to clear MPB bridge history errors", error)); + } + setRefreshTick(current => current + 1); - }, []); + }, [cacheKey]); return useMemo(() => { const allErrorsByChain = getErrorsByChain(historyCache);