# CLC Software > Technical docs for the Cosmo-Local Credit stack on Gnosis Chain. ## Addresses Gnosis mainnet, chain ID `100`. Explorer: [Gnosisscan](https://gnosisscan.io). ### Registry [`0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389`](https://gnosisscan.io/address/0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389) | Identifier | Proxy | Implementation | | -------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `AccountIndex` | [`0xB1Ec3942dc9258de62056Febb0dc544D9EB1ED1a`](https://gnosisscan.io/address/0xB1Ec3942dc9258de62056Febb0dc544D9EB1ED1a) | AccountsIndex [`0x3cA9AB…BeE`](https://gnosisscan.io/address/0x3cA9AB9b8628b43f1A1c53f29e969BA84Ef88BeE) | | `TokenIndex` | [`0x088352Bf610BFF30BBa39F02fa8EAD0F676d2ac0`](https://gnosisscan.io/address/0x088352Bf610BFF30BBa39F02fa8EAD0F676d2ac0) | TokenUniqueSymbolIndex [`0x0CEB18…2EF`](https://gnosisscan.io/address/0x0CEB18BA6562D3227717D98c044A5849bE8362EF) | | `PoolIndex` | [`0x4cbbd3aC98422774f33a0069AC7A9F8255cEFC69`](https://gnosisscan.io/address/0x4cbbd3aC98422774f33a0069AC7A9F8255cEFC69) | same TokenUniqueSymbolIndex impl | `PoolIndex` is a second `TokenUniqueSymbolIndex` (factory salt tag `poolindex`, so it does not collide with TokenIndex). Owner / proxy admin: [`0x33a573149db22e759fB9A38Bcc461C12855c2645`](https://gnosisscan.io/address/0x33a573149db22e759fB9A38Bcc461C12855c2645). Registry deploy: block `48027888`. ```bash REG=0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389 RPC=https://gnosis-rpc.publicnode.com for id in AccountIndex TokenIndex PoolIndex; do printf '%-16s %s\n' "$id" \ "$(cast call $REG 'addressOf(bytes32)(address)' \ "$(cast format-bytes32-string $id)" --rpc-url $RPC)" done ``` ### Relayer | | Address | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Calibur | [`0x48A910C8e9FF0b14051b78d0c96dB069E57f0729`](https://gnosisscan.io/address/0x48A910C8e9FF0b14051b78d0c96dB069E57f0729) | | clc-core signer (AccountIndex writer) | `0xBfE4784152A35610aF0Dcb726487F5d89118BbeF` | Calibur is the EIP-7702 delegation target, not a proxy. ### Implementations (v1.0.0) solc `0.8.36`, EVM `osaka`, optimizer `200` runs (Calibur: `1000`). Deployer `0x33a573…2645`. Stateful contracts need a proxy + `initialize`. `DecimalQuoter` and `SwapRouter` are stateless: call the implementation. | Contract | Implementation | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | ERC1967Factory | [`0xB286994c648F98fD3a3BA6C43934828a5b88162b`](https://gnosisscan.io/address/0xB286994c648F98fD3a3BA6C43934828a5b88162b) | | ContractRegistry | [`0x273B65EA845E2832f41d8d4366E6Cc3a9Fc67186`](https://gnosisscan.io/address/0x273B65EA845E2832f41d8d4366E6Cc3a9Fc67186) | | AccountsIndex | [`0x3cA9AB9b8628b43f1A1c53f29e969BA84Ef88BeE`](https://gnosisscan.io/address/0x3cA9AB9b8628b43f1A1c53f29e969BA84Ef88BeE) | | TokenUniqueSymbolIndex | [`0x0CEB18BA6562D3227717D98c044A5849bE8362EF`](https://gnosisscan.io/address/0x0CEB18BA6562D3227717D98c044A5849bE8362EF) | | GiftableToken | [`0x34445d13F112A11f72C1d353a7dcdc407F3df2d8`](https://gnosisscan.io/address/0x34445d13F112A11f72C1d353a7dcdc407F3df2d8) | | SwapPool | [`0x9e694D342Cab02e295262B2290b0E64A0334160D`](https://gnosisscan.io/address/0x9e694D342Cab02e295262B2290b0E64A0334160D) | | FeePolicy | [`0x1b97FfFAF2D2e16C8F7de6826F4F658dc898E5b6`](https://gnosisscan.io/address/0x1b97FfFAF2D2e16C8F7de6826F4F658dc898E5b6) | | Limiter | [`0x258AAd6c933F70D7F071E112800a41Fb60434048`](https://gnosisscan.io/address/0x258AAd6c933F70D7F071E112800a41Fb60434048) | | RelativeQuoter | [`0x0B0986c0E580377389337C453Cc65A3933511165`](https://gnosisscan.io/address/0x0B0986c0E580377389337C453Cc65A3933511165) | | OracleQuoter | [`0x3334fd1eA4c7e4dCA51f5E62EE3F5f7Dcbd098BA`](https://gnosisscan.io/address/0x3334fd1eA4c7e4dCA51f5E62EE3F5f7Dcbd098BA) | | ProtocolFeeController | [`0x302E6d520e7D7AeFceA4813e456234B5daA23B4d`](https://gnosisscan.io/address/0x302E6d520e7D7AeFceA4813e456234B5daA23B4d) | | Splitter | [`0x3b1F9bCC82f2dA5607dcDFCb21E47Cf64Ee54274`](https://gnosisscan.io/address/0x3b1F9bCC82f2dA5607dcDFCb21E47Cf64Ee54274) | | CAT | [`0xA12148B6eeb347298F17eCC5AC0377592850202E`](https://gnosisscan.io/address/0xA12148B6eeb347298F17eCC5AC0377592850202E) | | EthFaucet | [`0x3dF58d637f03CD5174c5533FB89cb9B6fd855Ad3`](https://gnosisscan.io/address/0x3dF58d637f03CD5174c5533FB89cb9B6fd855Ad3) | | PeriodSimple | [`0x8608051473603279EE982E87f765A78d3D080b00`](https://gnosisscan.io/address/0x8608051473603279EE982E87f765A78d3D080b00) | | DecimalQuoter | [`0x336f493d5472FD59e9E05128804E2D05Be89c4B7`](https://gnosisscan.io/address/0x336f493d5472FD59e9E05128804E2D05Be89c4B7) | | SwapRouter | [`0x16e3F29dDe22eF75C081A764C4d200Cd48647bcC`](https://gnosisscan.io/address/0x16e3F29dDe22eF75C081A764C4d200Cd48647bcC) | | RescueVault | [`0x3E5D8d8f63c57EA5DD62cF5aeC7212C50bDA69EF`](https://gnosisscan.io/address/0x3E5D8d8f63c57EA5DD62cF5aeC7212C50bDA69EF) | | Calibur | [`0x48A910C8e9FF0b14051b78d0c96dB069E57f0729`](https://gnosisscan.io/address/0x48A910C8e9FF0b14051b78d0c96dB069E57f0729) | ### Celo (legacy) | | | | -------------------------------- | -------------------------------------------- | | ContractRegistry (proxy) | `0xcaeEcFc5359c50365bED9f6dFEaFD332b57d5475` | | ContractRegistry (SarafuNetwork) | `0xd1FB944748aca327a1ba036B082993D9dd9Bfa0C` | | AccountIndex | `0x8B73A2C9e88d780cc2a1Cbec0E8B3A366eb11A69` | | TokenIndex | `0xe2CEf4000d6003958c891D251328850f84654eb9` | | PoolIndex | `0x281951bc238B5c58649cd97D351De23c16Cfcfa9` | | ProtocolFeeController proxy | `0x410e7Ff8410B0f8D15CA762a51F8b8A6Cd6eE9BE` | ## Architecture Two layers: **protocol** (on-chain) and **stack** (off-chain). Apps talk to the stack; the stack talks to Gnosis.
CLC clients talk to the staging host, which talks to Gnosis
### On-chain Discovery starts at `ContractRegistry`. It maps identifiers to index proxies. | Identifier | Contract | What it lists | | -------------- | ------------------------ | --------------------------------------- | | `AccountIndex` | `AccountsIndex` | Registered users (clc-core writes here) | | `TokenIndex` | `TokenUniqueSymbolIndex` | Vouchers | | `PoolIndex` | `TokenUniqueSymbolIndex` | Pools | `eth-tracker` walks TokenIndex and PoolIndex, then each pool's `tokenRegistry()`, and watches those addresses. `clc-core` only needs AccountIndex from the registry. A pool is not one contract. `SwapPool` is the vault; it is injected with a registry, quoter, fee policy, limiter, and optional protocol fee controller. Seal bits lock those slots.
SwapPool calls registry, quoter, fee policy, limiter, and protocol fee controller
Implementations are shared. Each voucher/pool/index is an ERC-1967 proxy from `ERC1967Factory`. `DecimalQuoter`, `SwapRouter`, `RescueVault`, `ERC1967Factory`, and `Calibur` are plain contracts. ### Off-chain | Process | Role | | ------------------ | -------------------------------------------------------------- | | **eth-tracker** | Follows the chain, filters CLC events, publishes to NATS | | **eth-indexer** | Consumes NATS, writes `chain_data` | | **clc-core** | HTTP API, SIWE auth, catalog, deploy jobs, EIP-7702 relayer | | **storage-server** | Signed uploads to R2; browsers post here, not through clc-core | | **clc-app** | Web PWA + admin console | Tracker publishes to NATS; indexer consumes. Indexer does not tail logs. ## Components Repos under [github.com/cosmo-local-credit](https://github.com/cosmo-local-credit) unless noted. | Component | Language | Role | Status | | ------------------------------------------------------------------------------------- | ------------- | -------------------------------------- | --------- | | [clc-protocol](https://github.com/cosmo-local-credit/protocol) | Solidity / Go | Vouchers, pools, indexes, `ge-publish` | active | | [calibur](https://github.com/cosmo-local-credit/calibur) | Solidity | EIP-7702 account implementation | active | | [clc-core](https://github.com/cosmo-local-credit/clc-core) | Go | HTTP API, catalog, deploy, relayer | active | | [eth-tracker](https://github.com/cosmo-local-credit/eth-tracker) | Go | Chain follower → NATS | active | | [eth-indexer](https://github.com/cosmo-local-credit/eth-indexer) | Go | NATS → Postgres `chain_data` | active | | [storage-server](https://github.com/cosmo-local-credit/storage-server) | Go | Signed image uploads (R2) | active | | [clc-app](https://github.com/cosmo-local-credit/clc-app) | TS | Web PWA + admin | active | | [ethutils](https://github.com/grassrootseconomics/ethutils) | Go | Registry loaders, RPC helpers | active | | [gnosis-node](https://github.com/cosmo-local-credit/gnosis-node) | Docker | reth + nimbus full node | active | | [notification-service](https://github.com/cosmo-local-credit/notification-service) | Go | SMS / email | optional | | [ens-offchain-resolver](https://github.com/grassrootseconomics/ens-offchain-resolver) | Go | `sarafu.eth` CCIP-read | Celo only | Images (GHCR, private): ``` ghcr.io/cosmo-local-credit/clc-core:develop ghcr.io/cosmo-local-credit/eth-tracker:v1.14.0-stable ghcr.io/cosmo-local-credit/eth-indexer:latest ghcr.io/cosmo-local-credit/storage-server:v0.8.0-rc ``` Staging compose: `clc-gnosis-repo`. Host `clc-gnosis` on Tailscale. ### License CLC-authored services and protocol contracts: **AGPL-3.0**, except unmodified Solady (MIT) and Calibur (MIT). This docs repo: **CC-BY-SA-4.0**. ## clc-core HTTP API, SIWE sessions, catalog, contract deploy jobs, EIP-7702 relayer. Staging: `https://gnosis-staging.cosmolocal.credit` ### Auth Two credentials, mutually exclusive on a request: | | User | Service | | --------- | ----------------------------------- | ------------------------------------------- | | Proof | SIWE or WebAuthn | EdDSA JWT from `gen-service-token` | | Transport | cookie `__clc_auth` (HttpOnly, 30d) | `Authorization: Bearer` or `__ge_cust_auth` | | Claims | `userId`, `ethereumAddress`, `role` | `publicKey`, `service: true` | Service tokens skip user quotas and **cannot** hit `/api/v2/users/*`. SIWE (EOA / paper wallet): ``` POST /auth/nonce → sign EIP-4361 → POST /auth/signin | /auth/signup | /auth/siwe/verify ``` Nonce TTL 5 minutes, one-time. Verification is local (`go-ethereum` ecrecover), no RPC. Passkey: `POST /auth/webauthn/verify`. No SIWE. Relies on P-256 assertion. User lifecycle: `PENDING` at signup → superadmin `PUT /api/v2/admin/users/:address/approve` → `APPROVED`. Sign-in of a pending address fails. `GET /auth/session`: `204` = no session, `200 { address, chainId }` = ok. ### Shape ``` /auth/* public /api/v2/* cookie or service JWT /api/v2/users/* user JWT only /api/v2/admin/* SUPERADMIN /api/v2/relayer/* see Relayer ``` Public catalog (no auth): `GET /api/v2/vouchers`, `GET /api/v2/vouchers/:address`, offerings under those. Errors: ```json { "ok": false, "description": "Not Found", "code": "E03" } ``` Full route list: `clc-core/docs/API.md` and `docs/swagger.yaml`. ### Chain config ``` CORE_CHAIN__ID=100 CORE_CHAIN__RPC_ENDPOINT=https://gnosis-rpc.publicnode.com CORE_CHAIN__GE_REGISTRY=0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389 CORE_CHAIN__DIVVI_CONSUMER=0x0000000000000000000000000000000000000000 CORE_RELAYER__CALIBUR_ADDRESS=0x48A910C8e9FF0b14051b78d0c96dB069E57f0729 ``` ### Keystore Master signing key is created on first boot and stored in `keystore`. That address must be an AccountsIndex writer. A volume rebuild generates a new key: ```bash docker compose exec -T postgres psql -U postgres -d clc_core -tAc \ "SELECT public_key FROM keystore;" cast send 0xB1Ec3942dc9258de62056Febb0dc544D9EB1ED1a \ "addWriter(address)" \ --rpc-url "$RPC_URL" --private-key $DEPLOYER_KEY ``` ## eth-indexer NATS consumer. Writes `chain_data`. Uses RPC for metadata, not for logs. ``` INDEXER_API__ADDRESS=:5002 INDEXER_POSTGRES__DSN=postgres://postgres:@postgres:5432/chain_data INDEXER_JETSTREAM__ENDPOINT=nats://nats:4222,nats://nats:4222 INDEXER_JETSTREAM__ID=eth-indexer-gnosis-staging INDEXER_CHAIN__CHAINID=100 INDEXER_CHAIN__RPC_ENDPOINT=https://gnosis-rpc.publicnode.com ``` `INDEXER_JETSTREAM__ID` must be unique per deployment. Schema: [chain\_data](/data/schema). ## NATS JetStream, single node on staging (`stream_replicas = 1`). Stream: `TRACKER.*`. Consumers should be **durable + pull**, and ACK after successful write. Redelivery is the retry. MsgID `txHash:logIndex` is the dedup key. Keep `dedup_window_hrs == persist_duration_hrs` or a recovered tracker will republish events that are still stored but no longer deduped. ### Subjects and payloads Envelope is documented on [eth-tracker](/stack/tracker). `payload` by `transactionType`: | Subject | payload | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------- | | `TRACKER.CONTRACT_CREATION` | `{ from }` | | `TRACKER.CUSTODIAL_REGISTRATION` | `{ account }` | | `TRACKER.FAUCET_GIVE` | `{ recipient, token, amount }` | | `TRACKER.INDEX_ADD` / `INDEX_REMOVE` | `{ address }` | | `TRACKER.INDEX_ACTIVE` | `{ address, active }` | | `TRACKER.LIMIT_SET` | `{ token, holder, value }` | | `TRACKER.OWNERSHIP_TRANSFERRED` | `{ previousOwner, newOwner }` | | `TRACKER.POOL_DEPOSIT` | `{ initiator, tokenIn, amountIn }` | | `TRACKER.POOL_SWAP` | `{ initiator, tokenIn, tokenOut, amountIn, amountOut, fee }` + optional settlement fields | | `TRACKER.QUOTER_PRICE_INDEX_UPDATED` | `{ token, exchangeRate }` | | `TRACKER.QUOTER_UPDATED` | `{ newQuoter }` | | `TRACKER.SEAL_STATE_CHANGE` | `{ final, sealState }` | | `TRACKER.TOKEN_APPROVE` | `{ owner, spender, value }` | | `TRACKER.TOKEN_BURN` / `TOKEN_MINT` / `TOKEN_TRANSFER` | standard ERC-20 fields | `POOL_SWAP` from `chain.pool_settlement_block` onward adds `quotedAmountOut`, `nominalAmountOut`, `protocolFee` (from `SwapSettlement`). Below that block the extras are **absent**, not zero. Amounts are raw integers. `fee` and every amount-out are in `tokenOut`. On reverts the tracker still publishes; `success: false` and some payload fields are the type's zero value. ## Notifications `notification-service` (Go). SMS via Africa's Talking, email via Resend. Not in the Gnosis compose. Auth: EdDSA service JWT (`"service": true`). Base `/api/v1`. OpenAPI: `notification-service/openapi.yaml`. ## Stack Staging host: Tailscale `clc-gnosis` (`100.108.72.127`), compose in `clc-gnosis-repo`.
Caddy fronts clc-core and storage-server; tracker and indexer share NATS and Postgres
| Service | Image | Publish | Watchtower | | -------------- | -------------------------------- | ---------------- | ---------- | | postgres 18 | `postgres:18-alpine` | `54432:5432` | no | | nats | `nats:2` (`-js`) | internal | no | | eth-tracker | `eth-tracker:v1.14.0-stable` | `127.0.0.1:5001` | no | | eth-indexer | `eth-indexer:latest` | `127.0.0.1:5002` | yes | | clc-core | `clc-core:develop` | via Caddy | yes | | storage-server | `storage-server:v0.8.0-rc` | via Caddy | yes | | caddy | `caddy-docker-proxy:2.10-alpine` | 80/443 | no | | watchtower | `containrrr/watchtower` | none | n/a | Watchtower: `--interval 120 --label-enable --cleanup --rolling-restart`. Only containers with `com.centurylinklabs.watchtower.enable=true`. Hostnames: * `gnosis-staging.cosmolocal.credit` → clc-core * `gnosis-storage.cosmolocal.credit` → storage-server ### Env mapping `CORE_A__B` → config key `a.b`. A value with a comma or space becomes a **list**. In `core.env` only, a value containing `\n` is passed through unsplit (PEM keys). Gotchas: 1. **Lists.** koanf `Strings()` does not promote a single env value to `[]string`. Repeat the NATS URL (`nats://nats:4222,nats://nats:4222`) in `core.env` / `indexer.env`. Do not do that for `bootstrap.ge_registry` (empty element panics). Put the registry in `tracker-config.toml`. 2. Tracker reads `jetstream.endpoint` (singular). The image's `endpoints` key is unused. 3. `CORE_CHAIN__DIVVI_CONSUMER` cannot be empty. Use the zero address. 4. Do not mount `config.toml` over Watchtower-updated images. Tracker is pinned, so a mount is safe. clc-core / indexer would freeze on a stale file. ### Ordered start ```bash docker compose up -d postgres nats docker compose logs -f postgres | grep -m1 "database system is ready" docker compose up -d eth-tracker docker compose up -d eth-indexer clc-core storage-server docker compose up -d caddy watchtower ``` `init_db.sql` creates `chain_data` only on an empty volume. ## Relayer Gasless path inside clc-core. User signs, clc-core pays, tx lands as an EIP-7702 Calibur batch. Base: `/api/v2/relayer` | Method | Path | | | ------ | ---------------------------------- | --------------------------------------- | | GET | `/health` | relayer address (`executor`) + chain id | | GET | `/nonce/:address` | EOA nonce (first-relay authorization) | | GET | `/calibur-nonce/:address` | sequence for the root key | | GET | `/calibur-nonce/:address/:keyHash` | sequence for a passkey | | POST | `/relay/preview` | digest + resolved nonce | | POST | `/relay` | submit signed batch | | POST | `/delegate` | set code without calls | | GET | `/track/:trackingId` | follow until mined | | GET | `/webauthn/challenge` | passkey recovery | | POST | `/webauthn/recover` | recover from passkey | ### Flow ``` build calls → POST /relay/preview → sign digest → POST /relay → GET /track/:id ``` Each call: `{ to, value, data }`. Batch: `{ calls: [1..16], revertOnFailure }`. Use `revertOnFailure: true`. First relay for a wallet also sends the 7702 authorization (needs `/nonce/:address`). After that, only Calibur sequence numbers. Passkey sign-in uses the WebAuthn assertion, not SIWE. Gas limits: `SETCODE_GAS_LIMIT` / `DELEGATE_GAS_LIMIT` = `200000`. Details and viem examples: `clc-core/docs/RELAYING.md`. ## Storage `storage-server`. Browsers upload **directly**; clc-core never sees the bytes. Auth is an Ed25519 signature whose public key matches `CORE_API__PRIVATE_KEY`. Staging: `https://gnosis-storage.cosmolocal.credit` CDN: `https://cdn.cosmolocal.credit` (R2 bucket `clc`). ``` STORAGE_API__ADDRESS=:5003 STORAGE_API__MAX_BODY_SIZE=8 # MB STORAGE_API__UPLOAD_TIMEOUT=60s STORAGE_API__CDN_BASE_URL=https://cdn.cosmolocal.credit STORAGE_API__ALLOWED_FOLDERS=voucher,profile,pool,report,offering STORAGE_AUTH__PUBLIC_KEY= STORAGE_AUTH__CLOCK_SKEW=30s ``` Generate the pair once: ```bash openssl genpkey -algorithm ED25519 -out private.pem openssl pkey -in private.pem -pubout -out public.pem awk '{printf "%s\\n", $0}' private.pem # CORE_API__PRIVATE_KEY awk '{printf "%s\\n", $0}' public.pem # STORAGE_AUTH__PUBLIC_KEY ``` clc-core mints a short-lived storage token: `POST /auth/storage-token` (authenticated). The app puts that on the upload request. Allowed image widths: `400,800,1280`. Method `6`, quality `82`, max 12.5 MP. ## eth-tracker Follows Gnosis, filters CLC-related txs (including reverts), publishes to NATS JetStream. Pinned image: `ghcr.io/cosmo-local-credit/eth-tracker:v1.14.0-stable`. API `127.0.0.1:5001`. ### Bootstrap Default: load `bootstrap.ge_registry`, resolve TokenIndex + PoolIndex, walk entries, then each pool's `tokenRegistry()`. Cache lives in BoltDB (`tracker_data`) and in memory. Watchlist mode (`bootstrap.watchlist` non-empty): skip the registry, use that set, then apply `blacklist`. Runtime updates from `AddressAdded` / `AddressRemoved`. Staging: ```toml [bootstrap] ge_registry = ["0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389"] watchlist = [""] blacklist = [""] contracts = ["0xB286994c648F98fD3a3BA6C43934828a5b88162b"] ``` `contracts` is extra addresses always watched (here: the factory). ### Chain ``` TRACKER_CHAIN__CHAINID=100 TRACKER_CHAIN__START_BLOCK=48027888 TRACKER_CHAIN__POOL_SETTLEMENT_BLOCK=0 TRACKER_CHAIN__RPC_ENDPOINT=https://gnosis-rpc.publicnode.com TRACKER_CHAIN__WS_ENDPOINT=wss://gnosis-rpc.publicnode.com ``` Losing `tracker_data` restarts catchup from `START_BLOCK`. Staging `pool_size = 4`. See [RPC](/chain/rpc). ### JetStream ``` TRACKER_JETSTREAM__PERSIST_DURATION_HRS=48 TRACKER_JETSTREAM__DEDUP_WINDOW_HRS=48 # must equal persist TRACKER_JETSTREAM__STREAM_REPLICAS=1 # single-node NATS ``` Dedup id: `txHash:logIndex`. Replicas `3` on one node never reaches quorum. Config key is `jetstream.endpoint` (singular). ### Subjects Stream `TRACKER.*`. Payload envelope: ```json { "block": 0, "contractAddress": "0x…", "success": true, "timestamp": 0, "transactionHash": "0x…", "transactionType": "POOL_SWAP", "index": 0, "payload": {} } ``` `index` is the log index. `0` on reverted input-data events. `max uint64` on `CONTRACT_CREATION`. See [NATS](/stack/nats) for per-type payloads. ## ethutils Go helpers: addresses, registry load, index walk, balances scanner, simulate, reverted-reason dump. ```bash go get github.com/grassrootseconomics/ethutils ``` LGPL-3.0. Docs: [pkg.go.dev/github.com/grassrootseconomics/ethutils](https://pkg.go.dev/github.com/grassrootseconomics/ethutils). ### Registry `GetGESmartContracts(rpc, registry)` resolves registry identifiers. Missing / zero addresses are omitted. Tracker uses this to seed the watchlist. Identifier bytes32 = ASCII name, right-padded: `cast format-bytes32-string AccountIndex`. `getAllTokensFromTokenIndex` walks `entryCount` / `entry` on both TokenIndex and PoolIndex. ## ge-publish Go library + CLI in `clc-protocol` (`pkg/publish`, `cmd/ge-publish`). ```bash go build -o ./ge-publish ./cmd/ge-publish ``` | Subcommand | Does | | ---------------------------------------------------------------------------------- | ------------------------------------------------- | | `publish-one` | factory (if needed) + impl + proxy + `initialize` | | `deploy-impl` | bytecode only | | `deploy-proxy` | proxy against an existing factory + impl | | `publish-one` on `decimalquoter` / `swaprouter` / `rescuevault` / `erc1967factory` | plain CREATE | Factory: Arachnid CREATE2 (`0x4e59…`) when `--factory-address` is omitted. On Gnosis reuse [`0xB286994c…162b`](https://gnosisscan.io/address/0xB286994c648F98fD3a3BA6C43934828a5b88162b). `--admin` is required for proxies and must differ from `--owner`. Deterministic proxy salts: high 20 bytes = deployer, low 12 bytes = ASCII tag (`contractregi`, `accountsinde`, `tokenuniques`, `poolindex` + 3 zero bytes). Two proxies of the same contract name collide unless you override the tag, which is why PoolIndex is `poolindex`. Go API, upgrade, and CREATE2 recipes: `clc-protocol/docs/PUBLISH.md`. Deploy flags: [Deploy](/contracts/deploy). ## chain\_data Postgres schema written by eth-indexer. Full column notes: `eth-indexer/DATA_GUIDE.md`. Every event row FKs to `tx` via `tx_id`. Join `contract_address` to `tokens` / `pools` for metadata. ### Tables | Table | Contents | | --------------------------- | -------------------------------------------------------- | | `tx` | `tx_hash`, `block_number`, `date_block` (UTC), `success` | | `token_transfer` | ERC-20 `Transfer` | | `token_mint` / `token_burn` | mint / burn | | `faucet_give` | faucet payouts | | `pool_swap` | `Swap` / `SwapSettlement` | | `pool_deposit` | deposits (including the deposit half of a swap) | | `index_active` | activation flips on an index | | `ownership_change` | `OwnershipTransferred` | | `tokens` / `pools` | metadata; `removed=true` means delisted, row kept | Idempotency: `(tx_id, …, log_index)`. Values are raw integers. Scale with `power(10, token_decimals)`. `pool_swap.fee`, `protocol_fee`, and all out amounts are in `token_out`. `quoted_out_value` / `nominal_out_value` / `protocol_fee` are NULL below `pool_settlement_block`. Treat NULL as unknown, not zero. ### Example ```sql SELECT tt.contract_address, tk.token_symbol, COUNT(*) AS transfer_count, SUM(tt.transfer_value / power(10, tk.token_decimals)) AS total_volume FROM token_transfer tt JOIN tx ON tx.id = tt.tx_id JOIN tokens tk ON tk.contract_address = tt.contract_address WHERE tx.success = true AND tk.removed = false GROUP BY tt.contract_address, tk.token_symbol ORDER BY total_volume DESC; ``` ## Calibur Minimal **non-upgradeable** EIP-7702 delegation target. User EOAs set code to Calibur; clc-core relays batches and pays gas. Gnosis: [`0x48A910C8e9FF0b14051b78d0c96dB069E57f0729`](https://gnosisscan.io/address/0x48A910C8e9FF0b14051b78d0c96dB069E57f0729) (v1.1.0, solc 0.8.36 / osaka / 1000 runs). Commit `249cac5`. Not a proxy. Set `CORE_RELAYER__CALIBUR_ADDRESS` to this. Delegated EOA code: ``` 0xef0100 || 48A910C8e9FF0b14051b78d0c96dB069E57f0729 ``` * ERC-4337 / ERC-7821-style batched calls (clc-core caps at 16) * ERC-1271 signatures * WebAuthn / P-256 keys * ERC-7914 hooks (do not deploy the example hooks in the repo) Audits: OpenZeppelin 05/2025, Cantina 04/2025. MIT. ### Config ``` CORE_RELAYER__CALIBUR_ADDRESS=0x48A910C8e9FF0b14051b78d0c96dB069E57f0729 CORE_RELAYER__SETCODE_GAS_LIMIT=200000 CORE_RELAYER__DELEGATE_GAS_LIMIT=200000 CORE_RELAYER__RELYING_PARTY_ID= CORE_RELAYER__ALLOWED_ORIGINS= ``` Passkeys bind to `RELYING_PARTY_ID`. A mismatch fails sign-in with `origin not allowed`. See [Relayer](/stack/relayer) for the HTTP flow. ## Deploy Full recipes: `clc-protocol/docs/DEPLOY.md` and `docs/PUBLISH.md`. ### Tooling ```bash cd clc-protocol go build -o ./ge-publish ./cmd/ge-publish make all export RPC_URL=https://gnosis-rpc.publicnode.com export CHAIN_ID=100 export PRIVATE_KEY= export OWNER= export ADMIN= GAS="--gas-fee-cap 2000000000 --gas-tip-cap 1000000000" BASE="--rpc-url $RPC_URL --chain-id $CHAIN_ID --private-key $PRIVATE_KEY $GAS" FACTORY=0xB286994c648F98fD3a3BA6C43934828a5b88162b ``` Implementations are already on chain. Reuse them. See [Addresses](/addresses). ### New proxy ```bash ./ge-publish deploy-proxy --contract swappool $BASE \ --factory-address $FACTORY \ --impl-address 0x9e694D342Cab02e295262B2290b0E64A0334160D \ --owner $OWNER --admin $ADMIN # plus contract-specific flags: --token-name, --fee-policy-default, … ``` `--admin` is required and must differ from `--owner` or `ge-publish` exits. ```bash cast send $REGISTRY "set(bytes32,address)" \ $(cast format-bytes32-string AccountIndex) $ACCOUNT_INDEX_PROXY \ $BASE ``` `set` is once per identifier. ### Verify ```bash VERIFY="--chain-id 100 --compiler-version 0.8.36 --evm-version osaka \ --num-of-optimizations 200 --verifier blockscout \ --verifier-url https://gnosisscan.io/api/" forge verify-contract $IMPL src/SwapPool.sol:SwapPool $VERIFY ``` Gnosisscan links ERC-1967 proxies to a verified implementation. You do not verify the proxy separately. Calibur uses 1000 optimizer runs, not 200. ### After a pool exists 1. `register` it on PoolIndex (writer/owner). 2. `register` its vouchers on TokenIndex and the pool's own registry. 3. `setLimitFor` on a later tx than pool deploy. 4. If the pool emits `SwapSettlement`, set `TRACKER_CHAIN__POOL_SETTLEMENT_BLOCK`. ## EthFaucet + PeriodSimple Native xDAI faucet. Registry and period checker are both required. **Proxy:** yes. ```solidity initialize(owner, amount) gimme() // pay msg.sender giveTo(recipient) check(recipient) // false (no revert) if a gate is unset setAmount / setRegistry / setPeriodChecker seal(state) withdraw(recipient, value) // owner, remains available after seal ``` After `initialize`, `registry` and `periodChecker` are zero. Every claim reverts (`RegistryBackend`, then `PeriodBackend`) until both are set. Wire them **before** funding. Expected backends: * `registry.have(address) → bool`: typically AccountsIndex * `periodChecker.have / poke / next`: PeriodSimple Payout uses `transfer` (2300 gas). Contract recipients with heavy `receive` will fail. Seal bits: `REGISTRY_STATE=1`, `PERIODCHECKER_STATE=2`, `VALUE_STATE=4`. Cannot seal an unset field. ### PeriodSimple ```solidity initialize(owner, poker) have(subject) // false if balance ≥ threshold; else cooldown poke(subject) // owner or poker; records lastUsed setPeriod / setPoker / setBalanceThreshold ``` `balanceThreshold > 0` and `subject.balance >= threshold` → ineligible, including first-time. ## Fees Two layers: pool fee (steward) and protocol fee (network). Both come out of the user's output. The pool owner always receives the full pool fee. ### FeePolicy **Proxy:** yes. * `defaultFee` in PPM, `<= 1_000_000`. * Optional per-pair override, **direction-sensitive** (`A→B` ≠ `B→A`). * Pair fee of `0` is treated as unset → falls back to default. You cannot set a literal zero pair fee; `removePairFee` instead. ```solidity getFee(tokenIn, tokenOut) calculateFee(tokenIn, tokenOut, amount) // amount * fee / PPM setDefaultFee / setPairFee / removePairFee // owner ``` `isActive()` always returns true (interface stub). ### ProtocolFeeController **Proxy:** yes. One per network, shared by pools. ```solidity initialize(owner, initialFee, initialRecipient) // recipient ≠ 0, starts active getProtocolFee() // 0 if inactive getProtocolFeeRecipient() setProtocolFee / setProtocolFeeRecipient / setActive ``` Implementation: [`0x302E6d52…23B4d`](https://gnosisscan.io/address/0x302E6d520e7D7AeFceA4813e456234B5daA23B4d). ### How SwapPool combines them ``` totalFee = quoted * feePpm / PPM assumedFee = quoted * 10_000 / PPM // 1% floor effectiveFee = max(totalFee, assumedFee) protocolFee = effectiveFee * protocolFeePpm / PPM net = quoted - totalFee - protocolFee ``` Skipped if controller unset, `protocolFeePpm == 0`, or recipient is zero. Joint constraint: `feePpm * (PPM + protocolFeePpm) < PPM²` whenever `feePpm >= 10_000`. Else `FeeTooHigh` on quote and swap, including dust sizes. ### Splitter **Proxy:** yes. Pays ETH or ERC-20 to a fixed recipient set. Allocations in PPM must sum to exactly `1_000_000`. ≥2 recipients, no duplicates/zero/self. Only the hash of `(accounts, percentAllocations)` is stored; distribute calls must pass the same arrays (`InvalidHash` otherwise). Fractional remainders accrue. Empty balance is a no-op. ## GiftableToken ERC-20 voucher: mint, optional expiry, writer minters. **Proxy:** yes. ```solidity initialize(name, symbol, decimals, owner, expiresAt) // expiresAt=0 → never mintTo(to, amount) // owner or writer burn(amount) // owner, from own balance applyExpiry() // anyone; no-op if expiresAt=0 addWriter / deleteWriter / isWriter ``` ### Behaviour * After expiry, every transfer including mint reverts `TokenExpired`. * Expiry flips on the first transfer at `block.timestamp >= expiresAt`, or via `applyExpiry()`. * Public: `expired`, `totalMinted`, `totalBurned`. Events: `Mint`, `Burn`, `Expired`, `WriterAdded`, `WriterRemoved`. Many instances, one implementation (`0x34445d13…df2d8`). Deploy a proxy per voucher. Symbol must be unique in each `TokenUniqueSymbolIndex` you register it into. ## Indexes Two contracts, three proxies. ### AccountsIndex Enumerable address set with activation. clc-core calls `add` with its master key on user registration. Proxy: [`0xB1Ec3942dc9258de62056Febb0dc544D9EB1ED1a`](https://gnosisscan.io/address/0xB1Ec3942dc9258de62056Febb0dc544D9EB1ED1a). | Call | Who | Effect | | --------------------------- | -------------- | ---------------------------------------- | | `add(account)` | owner / writer | insert; revert if present | | `remove(account)` | owner / writer | swap-and-pop (order not stable) | | `activate` / `deactivate` | owner / writer | toggle `have` without dropping the entry | | `have(account)` | anyone | present and active | | `contains(account)` | anyone | present, including deactivated | | `entry(i)` / `entryCount()` | anyone | enumerate | | `time(account)` | anyone | add timestamp; `NotFound` if absent | Deactivation: `contains` stays true, `have` / `isActive` go false. `add` reverts `Access()` unless `writers[msg.sender] || msg.sender == owner()`. Events: `AddressAdded`, `AddressRemoved`, `AddressActive(account, active)`, `WriterAdded`, `WriterDeleted`. ### TokenUniqueSymbolIndex Token registry keyed by unique ERC-20 `symbol()`. Used as: * Network **TokenIndex** * Network **PoolIndex** (pools, not tokens; same ABI) * Per-pool `SwapPool.tokenRegistry` | Call | Notes | | ------------------------------------- | ------------------------------------------------------------------------- | | `register(token)` / `add(token)` | reads `symbol()`; unique address + unique symbol; ≤32 bytes; owner/writer | | `remove(token)` | owner/writer | | `have(token)` | SwapPool whitelist check | | `addressOf(bytes32 symbolKey)` | lookup | | `entry(i)` / `entryCount()` | enumerate addresses | | `identifier(i)` / `identifierCount()` | enumerate symbol keys | `time` / `activate` / `deactivate` are inert stubs (`0` / `false`) for interface compatibility. `isWriter` on this contract is the raw mapping. It does **not** treat owner as writer. Events: `AddressKey(symbol, token)`, `AddressAdded`, `AddressRemoved`. Tracker bootstrap walks TokenIndex and PoolIndex with `entryCount` / `entry`, then each pool's `tokenRegistry()`. ## Limiter Per-token, per-holder balance cap. SwapPool checks it on deposit against **the pool's own balance**. **Proxy:** yes. ```solidity initialize(owner) limitOf(token, holder) // 0 = no cap set setLimitFor(token, holder, value) // owner or writer ``` SwapPool treats `limitOf == 0` as **block all deposits** for that token. Always set a positive cap before listing. `setLimitFor` requires `holder` to be a contract (`extcodesize > 0`). You cannot set a limit in the same transaction that deploys the pool: `extcodesize` is 0 in a constructor. Second tx. Event: `LimitSet(token, holder, value)`. ## Other contracts ### CAT On-chain preference list: up to 5 ERC-20s per account, index 0 most preferred. ```solidity setTokens(tokens[]) // caller setTokensFor(account, tokens[]) // owner or writer getTokens / tokenAt / tokenCount ``` Replace-all, not a delta. No zero addresses. ### RescueVault Plain CREATE. Occupies a deployer-nonce address so assets sent to a "future contract" can be swept. ```solidity constructor(admin) // payable, admin ≠ 0 sweepETH / sweepERC20 / sweepERC20s sweepERC721 / sweepERC721s sweepERC1155 / sweepERC1155Batch ``` Admin only. Recipient ≠ 0. Implements ERC-721/1155 receiver hooks. Assets are not enumerable. Pass token addresses / IDs. Instance: [`0x3E5D8d8f63c57EA5DD62cF5aeC7212C50bDA69EF`](https://gnosisscan.io/address/0x3E5D8d8f63c57EA5DD62cF5aeC7212C50bDA69EF). ## Contracts Source: [clc-protocol](https://github.com/cosmo-local-credit/protocol). Spec: `docs/SPEC.md`. License: AGPL-3.0 (Solady snippets stay MIT). ### Units Fees and rates use **PPM**: `1_000_000 = 100%`. `10_000 = 1%`. `100_000 = 10%`. ### Deployment pattern Stateful contracts: implementation (`_disableInitializers` in constructor) + ERC-1967 proxy from Solady `ERC1967Factory` + one-time `initialize`. Plain (no proxy): `ERC1967Factory`, `DecimalQuoter`, `SwapRouter`, `RescueVault`, `Calibur`. `ge-publish` refuses `--admin == --owner`. Seal on SwapPool / EthFaucet is only a commitment if a different key can `upgradeTo`. | Contract | Count | Job | | --------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------- | | [GiftableToken](/contracts/giftable-token) | many | Voucher ERC-20 | | [SwapPool](/contracts/swap-pool) | many | Vault + swap | | [TokenUniqueSymbolIndex](/contracts/indexes) | per pool + network Token/Pool indexes | `have(token)` whitelist | | [Limiter](/contracts/limiter) | per pool | Deposit caps | | [FeePolicy](/contracts/fees) | per pool | Swap fee PPM | | [DecimalQuoter](/contracts/quoters) / [RelativeQuoter](/contracts/quoters) / [OracleQuoter](/contracts/quoters) | shared or per pool | `valueFor` / `reverseValueFor` | | [ProtocolFeeController](/contracts/fees) | one per network | Network fee | | [SwapRouter](/contracts/router) | one | Multi-hop quotes (`eth_call` only) | | [ContractRegistry](/contracts/registry) | one | Identifier → address | | [AccountsIndex](/contracts/indexes) | one | Registered users | | [Calibur](/contracts/calibur) | one impl | EIP-7702 account | All proxied contracts reject `owner == address(0)` in `initialize`. ### Writer role Owner grants limited write access without handing over ownership: `GiftableToken` (minters), `Limiter`, `CAT`, `AccountsIndex`, `TokenUniqueSymbolIndex`. `isWriter` includes the owner **except** on `TokenUniqueSymbolIndex`, where it is the raw flag. ### Quoter interface ``` valueFor(out, in, amount) // forward reverseValueFor(out, in, desiredOut) // inverse, rounded up ``` Guarantee: `valueFor(out, in, reverseValueFor(out, in, x)) >= x`. SwapPool uses the forward path for swaps / `getAmountOut` and the reverse path for `getAmountIn`. ## Quoters All implement `IQuoter`. SwapPool calls them during swap / quote. ### DecimalQuoter Stateless, no proxy. Implementation is the instance: [`0x336f493d5472FD59e9E05128804E2D05Be89c4B7`](https://gnosisscan.io/address/0x336f493d5472FD59e9E05128804E2D05Be89c4B7). 1:1 value, scales decimals only. Use when every voucher in the pool is the same unit (e.g. all KES-pegged, mixed 6/18 decimals). ### RelativeQuoter **Proxy:** yes. Per-token rate in PPM. Unset rate = `1_000_000` (1.0). ``` out = adjustedValue * inRate / outRate ``` `adjustedValue` applies the decimal delta **before** the rate, so cross-decimal quotes keep precision. Owner: `setPriceIndexValue(token, exchangeRate)`. Event: `PriceIndexUpdated`. ### OracleQuoter **Proxy:** yes. Chainlink `AggregatorV3` feeds, all in one quote denomination (typically USD). Cross-rates any pair that shares that denominator. ```solidity initialize(owner, baseCurrency) // baseCurrency is metadata only setOracle(token, feed) setOracle(token, feed, maxStaleness) // 0 → global fallback setMaxStaleness(seconds) // default 86400 setMultiplier(ppm) // 900_000–1_000_000 inclusive ``` Missing / stale / non-positive price reverts (`OracleNotSet`, `StaleOraclePrice`, `InvalidOraclePrice`). No fallback rate. `multiplier` haircuts every quote. Above parity is rejected (same factor on both legs, so A→B→A would return `multiplier²`). Uninitialized storage (`0`) is treated as `1_000_000`. | Pool | Quoter | | ------------------------- | -------------- | | Same unit, mixed decimals | DecimalQuoter | | Steward-set rates | RelativeQuoter | | Live FX / stables | OracleQuoter | ## ContractRegistry `bytes32` identifier → address. Identifiers are fixed in `initialize`; each `set` can run once. Proxy: [`0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389`](https://gnosisscan.io/address/0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389). ```solidity function initialize(address owner_, bytes32[] memory _identifiers) external initializer; function set(bytes32 _identifier, address _address) external onlyOwner returns (bool); function addressOf(bytes32) external view returns (address); function identifier(uint256) external view returns (bytes32); function identifierCount() external view returns (uint256); ``` Event: `AddressKey(identifier, address)`. The key is ASCII, right-padded: `cast format-bytes32-string AccountIndex`. Case matters. This registry has `AccountIndex`, `TokenIndex`, `PoolIndex`. clc-core reads AccountIndex; eth-tracker reads TokenIndex and PoolIndex. ### Config Same address in `CORE_CHAIN__GE_REGISTRY` and tracker `bootstrap.ge_registry`. Tracker cannot take a single env value (koanf `Strings()` treats it as a scalar), so put it in `tracker-config.toml` and leave `TRACKER_BOOTSTRAP__GE_REGISTRY` unset: ```toml [bootstrap] ge_registry = ["0xaC95B1A09f2dbaf4542e3Ba8c4Cce0234895D389"] ``` ## SwapRouter Stateless multi-hop quoter. Holds no funds, executes no swaps. Call via `eth_call` (functions are non-`view` because pool quotes may be non-view). Instance: [`0x16e3F29dDe22eF75C081A764C4d200Cd48647bcC`](https://gnosisscan.io/address/0x16e3F29dDe22eF75C081A764C4d200Cd48647bcC). ```solidity struct Hop { address pool; address tokenIn; address tokenOut; } quoteExactInput(Hop[] path, uint256 amountIn) returns (uint256 amountOut) quoteExactOutput(Hop[] path, uint256 amountOut) returns (uint256 amountIn) ``` Empty path → `EmptyPath`. Any hop revert (including `FeeTooHigh` / `InsufficientOutput`) fails the whole quote. There is no `0` meaning "no route". Exact-output walks the path backwards using `getAmountIn`. Roundtrip: `quoteExactInput(path, quoteExactOutput(path, X)) >= X` (+1 wei safety per hop). ```solidity Hop[] memory path = new Hop[](2); path[0] = Hop(poolA, usdt, voucher); path[1] = Hop(poolB, voucher, other); uint256 out = router.quoteExactInput(path, 100e6); ``` ## SwapPool Vault + swap engine. ERC-20 metadata (name/symbol/decimals) but **no LP tokens**. **Proxy:** yes. Implementation [`0x9e694D34…4160D`](https://gnosisscan.io/address/0x9e694D342Cab02e295262B2290b0E64A0334160D). ### Injected deps Set in `initialize` or later via setters, until sealed. | Slot | Interface | If unset | | ----------------------- | ---------------------------- | ---------------------- | | `tokenRegistry` | `have(address)` | any token allowed | | `tokenLimiter` | `limitOf(token, pool)` | uncapped | | `quoter` | `IQuoter` | 1:1 | | `feePolicy` | `getFee` | pool fee 0 | | `feeAddress` | n/a | fees not accrued | | `protocolFeeController` | `getProtocolFee` + recipient | no protocol fee | | `feesDecoupled` | n/a | fees stay in inventory | ### Swap Prefer the bounded form: ```solidity withdraw(tokenOut, tokenIn, value, recipient, minAmountOut, deadline) ``` Shorter overloads send to `msg.sender` and skip slippage/deadline. Flow (`nonReentrant`): 1. `tokenIn == tokenOut` → `InvalidToken`. 2. Deposit `tokenIn`. Registry + limiter apply. `received` is the **balance delta** (fee-on-transfer safe). Zero delta → `TransferFailed`. Emits `Deposit`. 3. `quotedValue = quoter.valueFor(tokenOut, tokenIn, received)` (or `received` if no quoter). 4. Pool fee: `quotedValue * feePpm / PPM`. 5. Protocol fee on top (see [Fees](/contracts/fees)). 6. Recipient gets `quotedValue - poolFee - protocolFee`. Protocol cut is transferred in the same call. 7. Pool fee recorded in `fees[tokenOut]` if `feeAddress != 0`. 8. `Swap` + `SwapSettlement` events. `Swap.initiator` is always `msg.sender`. Expect a `Deposit` immediately before each `Swap`. `getAmountOut` / `getAmountIn` **revert** instead of returning 0 (`FeeTooHigh`, `InsufficientOutput`). Do not treat a revert as a zero quote. ### Seal Bits can only be set, never cleared. | Constant | Value | Locks | | ------------------ | ----- | --------------- | | `FEE_STATE` | 1 | `feePolicy` | | `FEEADDRESS_STATE` | 2 | `feeAddress` | | `QUOTER_STATE` | 4 | `quoter` | | `REGISTRY_STATE` | 8 | `tokenRegistry` | | `LIMITER_STATE` | 16 | `tokenLimiter` | | `maxSealState` | 31 | all five | Sealing an unset required slot reverts `InvalidState`. Seal locks the **slot**, not the callee. A sealed quoter can still change its own rates. `isSealed(0)` compares against the mask stored at init (`fullSealMask`), so adding a bit in a later implementation does not silently unseal old pools. ### Fees in inventory * `feesDecoupled = false` (default): accrued fees count as liquidity. * `true`: available = `balance - fees[token]`. `withdrawLiquidity` cannot take the reserved fees. Owner collects with `withdraw(tokenOut)` / `withdraw(tokenOut, value)` to `feeAddress`. Owner emergency: `withdrawLiquidity(token, to, amount)`. ### Constants `DEFAULT_FEE_PPM = 10_000` (1%). Floor used when computing the protocol cut, so a pool cannot shrink the protocol's share by setting a tiny pool fee. ## Gnosis | | | | --------------- | -------------------------------------- | | Chain ID | `100` | | Native token | xDAI | | Block time | \~5s | | Explorer | [gnosisscan.io](https://gnosisscan.io) | | CLC start block | `48027888` | ``` CORE_CHAIN__ID=100 TRACKER_CHAIN__CHAINID=100 INDEXER_CHAIN__CHAINID=100 TRACKER_CHAIN__START_BLOCK=48027888 ``` `START_BLOCK` is the registry deploy. `POOL_SETTLEMENT_BLOCK` stays `0` until a pool emits `SwapSettlement`; then set it to that pool's first block. ### EIP-7702 Gnosis supports EIP-7702. clc-core delegates user EOAs to [Calibur](/contracts/calibur) and relays batches. First relay for a wallet sets the designation (`0xef0100` + Calibur address). ## Full node Repo: `gnosis-node`. Docker Compose: **reth** (execution) + **nimbus** (consensus). ### Hardware From the [Gnosis node docs](https://docs.gnosischain.com/node): * 4+ CPU threads * 16 GB RAM * 1 TB+ NVMe * Unmetered or high-cap network Open inbound `30303/tcp+udp` (reth P2P) and `9000/tcp+udp` (nimbus P2P). RPC `8545`, WS `8546`, engine `8551`, beacon `5052` bind `127.0.0.1` only. ### Bring-up ```bash apt update && apt upgrade --yes apt install curl chrony git openssl xxd curl -fsSL https://get.docker.com | bash cd gnosis-node vi reth.env nimbus.env ./bootstrap.sh # JWT, datadirs, reth snapshot, nimbus checkpoint docker compose up -d ``` Skip snapshot / checkpoint: ```bash RETH_SNAPSHOT=false NIMBUS_CHECKPOINT=false ./bootstrap.sh ``` Keep chrony running. Consensus clients are sensitive to clock drift. ### Operate ```bash docker compose logs -f --tail=200 reth docker compose logs -f --tail=200 nimbus ``` Client flags live in `reth.env` / `nimbus.env` and are mapped by `scripts/*.sh`. Restart the service after edits. Optional Caddy in `caddy/` if you need TLS in front of RPC. Do not expose engine API. ## RPC Staging uses **publicnode** for every process. WS and HTTP must be the same provider. ``` https://gnosis-rpc.publicnode.com wss://gnosis-rpc.publicnode.com ``` Mixing providers (WS on A, HTTP on B) produces `w3: call failed: not found` on some blocks: the websocket announces a head the HTTP node does not have yet. eth-tracker batches up to 100 JSON-RPC sub-requests per call. Default `pool_size = 0` is `runtime.NumCPU()*3`. Staging sets `pool_size = 4` in `tracker-config.toml`. ### Self-hosted Run [gnosis-node](/chain/node) (reth + nimbus) and point: ``` TRACKER_CHAIN__RPC_ENDPOINT=http://127.0.0.1:8545 TRACKER_CHAIN__WS_ENDPOINT=ws://127.0.0.1:8546 CORE_CHAIN__RPC_ENDPOINT=http://127.0.0.1:8545 INDEXER_CHAIN__RPC_ENDPOINT=http://127.0.0.1:8545 ``` Archive (or a node that has logs from block `48027888`) is required for tracker catchup. ## Admin `clc-app` workspace, `apps/admin`. Superadmin console. ```bash vp dev apps/admin # :3001 ``` Same API and session cookie as web. Routes under `/api/v2/admin/*` require `role = SUPERADMIN`. Origins must be on `CORE_API__CORS` and `CORE_RELAYER__ALLOWED_ORIGINS` (`https://admin.gnosis.cosmolocal.credit`, `https://clc-admin-gnosis.pages.dev`, previews). ## Web app `clc-app` workspace, `apps/web`. PWA. Vite+. ```bash vp install vp dev apps/web # :3000 vp build vp test ``` Node version is `.node-version` in that repo. ### Auth model Public market is the default route. A connected wallet is not enough. Authenticated ⇔ wagmi connected **and** `GET /auth/session` returns that same address. | Client state | Meaning | | -------------------- | ----------------------------------------------- | | `loading` | reconnect / session in flight | | `guest` | no wallet | | `connectedNoSession` | wallet up, no matching cookie | | `authenticated` | wallet + session match | | `sessionCheckError` | 5xx / network. Do not demote mounted private UI | Connectors: * `private-key`: paper wallet, SIWE * `calibur-webauthn`: passkey, `POST /auth/webauthn/verify` ### Gnosis wiring Point the app at: ``` https://gnosis-staging.cosmolocal.credit # API https://gnosis-storage.cosmolocal.credit # uploads ``` Chain ID `100`. Calibur `0x48A910C8e9FF0b14051b78d0c96dB069E57f0729`. `CORE_RELAYER__ALLOWED_ORIGINS` on the API host must include the Pages hostname (`https://app.gnosis.cosmolocal.credit`, `https://clc-app-gnosis.pages.dev`, preview `https://*.clc-app-gnosis.pages.dev`). Passkeys bind to `CORE_RELAYER__RELYING_PARTY_ID` (`gnosis.cosmolocal.credit` on staging).