πŸ“„ migration_plan.md
D-OPEN SOVEREIGN
1# Migration Plan: Native + ERC20 Stablecoin Payments
2
3Goal: support both native-token donations (current CORE / Base flow with Pyth oracle) and ERC20 stablecoin donations (CELO with cUSD/USDC, no oracle), via an abstract `Accountant` base + two concrete implementations selected at compile time per chain.
4
5---
6
7## 1. Contracts
8
9**New file structure:**
10- `contracts/Accountant.sol` β†’ becomes the **abstract base** (split logic, treasury wallets, `MIN_DONATION_USD = 20`, `getBalanceOfAddress`, `fundAddress`-passthrough). Define a virtual `_payDollars(...)` and `_minDonationCheck(msg.value or amount)`.
11- `contracts/AccountantNative.sol` (NEW) β€” current `donateFunds() payable` + Pyth oracle. The `receive()` fallback stays here only.
12- `contracts/AccountantERC20.sol` (NEW) β€” `donateFundsToken(uint256 amount)`, uses `SafeERC20.safeTransferFrom(msg.sender, address(this), amount)` then splits via `safeTransfer`. Needs `IERC20 public token`, `uint8 public tokenDecimals`, `uint256 public minDonation` (= 20 * 10^decimals). NO `receive()` / NO Pyth.
13- `contracts/Factory.sol` β†’ split into `FactoryNative.sol` and `FactoryERC20.sol` (each inheriting the matching Accountant). The shared `register()` signature changes: ERC20 version becomes `register(country, language, referredBy, uint256 amount)` β€” no `payable`, donor must `approve()` beforehand.
14- `contracts/ErrorLibrary.sol` β†’ add `ERC20TransferFailed`, `InsufficientAllowance`, `ZeroToken`.
15- `contracts/mocks/MockERC20.sol` (NEW, test only) β€” simple ERC20 with custom decimals so we can simulate USDC (6) and cUSD (18).
16- Add OpenZeppelin: `pnpm add @openzeppelin/contracts` (for `IERC20`, `SafeERC20`, `IERC20Metadata`).
17
18**Stripe path:** `registerStripe`, `fundStripe`, `recordStripeDonation` already operate in dollar votes β€” no change to logic, but on the ERC20 chain they should NOT pull tokens (Stripe is off-chain payment). Keep them as-is in the abstract base.
19
20**Reentrancy:** add `ReentrancyGuard` to `donateFundsToken` and `register` on the ERC20 side (ERC777 hooks, malicious tokens).
21
22**Tests:** new `test/AccountantERC20.spec.js` parallel to `Accountant.spec.js` using `MockERC20` (test once at 18 decimals, once at 6 decimals to catch normalization bugs). Reuse `test-helpers.js` with a new `setupFactoryERC20WithBouncerStorage()`.
23
24---
25
26## 2. Compilation Steps
27
28- `pnpm run build` (Hardhat compiles both Factory variants β€” they coexist as separate top-level contracts).
29- `pnpm run sync:abis` β€” `tools/sync_abis` must now copy **both** `FactoryNative.json` and `FactoryERC20.json` ABIs into `packages/web3-contracts/src/abis/`, plus the ERC20 token ABI (a generic `IERC20.json`).
30- `pnpm run sync:config` β€” `tools/generate_contract_config.js` reads `paymentMode: 'native'|'erc20'` from `config/networks.js` and emits per-chain config including `tokenAddress`, `tokenSymbol`, `tokenDecimals`. The generated `config.ts` should expose a discriminated-union type so the frontend gets compile-time errors if it mixes paths.
31
32---
33
34## 3. Deployment Steps
35
36- `config/networks.js` per-chain additions: `paymentMode: 'native' | 'erc20'`, `token: { address, symbol, decimals }`
37  - CELO mainnet β†’ cUSD `0x765DE816845861e75A25fCA122bb6898B8B1282a` or USDC `0xcebA9300f2b948710d2653dD7B07f33A8B32118C`
38  - Alfajores (chainId 11142220 / `CELO_SPOLIA`) β†’ cUSD testnet address
39- `deploy/core/04-deploy-factory.js` β†’ branch on `paymentMode`: deploys `FactoryNative` (current 5-arg constructor) or `FactoryERC20` (constructor: `projects, maintenance, marketing, tokenAddress, minDonationDollars`). Reads token decimals on-chain rather than trusting config.
40- New deploy artifact convention: `deploy/d-code/evm/CELO/Factory.json` keeps the same key but contains `paymentMode` and `tokenAddress` fields so the frontend can branch.
41- `05-sync-registry.js` should also register the token address (consider adding a `stableCoin` slot in `AddressRegistry.sol` for clean lookup; otherwise just embed in generated config).
42- `pnpm run evm:deploy-staging` for CELO Alfajores first; verify with a dcode diagnostic before mainnet.
43
44---
45
46## 4. Frontend β€” Yes, the API Surface Changes
47
48This is the largest work item, not a sprinkle.
49
50**Behavior change β€” two-step flow:** Native is one tx; ERC20 is `approve` β†’ `donateFundsToken`. Two distinct UX states (and two gas payments). Best UX is **EIP-2612 permit** if cUSD/USDC support it on the target chain β€” single transaction with off-chain signature. Both cUSD and CELO-USDC support permit; recommend implementing permit-first with `approve` as fallback.
51
52**Files needing changes:**
53- `packages/web3-evm` β€” add `donateFundsToken(amount)`, `approveToken(spender, amount)`, `getTokenAllowance(owner, spender)`, `getTokenBalance(addr)`, `permit(...)` helper. The existing `donateFunds`/`register` calls become chain-conditional.
54- `packages/web3-vue` β€” composable splits: `useDonate()` internally checks `paymentMode` and dispatches.
55- `packages/web3-contracts` β€” bundle generic ERC20 ABI + permit ABI.
56- `apps/vue.datapond.earth/src/pages/web3/transaction/execute.vue` β€” currently sends `value: native`, now must conditionally call approve+donate or permit+donate. The `Math.ceil(native * 10000) / 10000` rounding logic disappears for stable coins (no oracle needed: 1 USDC = 1 vote, just multiply by 10^decimals).
57- `apps/vue.datapond.earth/src/pages/web3/login.vue` and `project.vue` β€” the "donate" buttons.
58- `apps/vue.datapond.earth/src/components/web3/NetworkInfo.vue` and `NetworkStats.vue` — show "cUSD" or "USDC" instead of native symbol; remove ETH→USD conversion display.
59- `apps/vue.datapond.earth/public/js/web3-contracts.global.js` β€” UMD rebuild.
60- `packages/web3-i18n` β€” new strings: `approveTokenPrompt`, `approvalPending`, `signPermit`, `tokenAllowanceTooLow`.
61
62**Surface API change is real but contained**: anything currently doing `register({ value })` becomes `register(amount)` after `approve(amount)`. The `balance()` view return type does not change (still dollar votes).
63
64---
65
66## 5. Things Likely Forgotten
67
681. **Token decimals normalization** β€” USDC = 6 decimals, cUSD = 18. Get it wrong by one zero and you've bricked the contract. Read `IERC20Metadata.decimals()` in the constructor and store it; do all min-donation math in raw token units.
692. **Fee-on-transfer / rebasing tokens** β€” cUSD and USDC are safe, but document this contract is **not** safe for arbitrary tokens. Add a comment + reject obvious attempts.
703. **`receive()` fallback removal** β€” on the ERC20 contract, native sent by mistake should `revert`, not silently sit (we removed `fallback()` last session for the same reason).
714. **EIP-2612 permit** β€” strongly consider for UX. Without permit, every donation is two MetaMask popups, which kills conversion.
725. **Lambda/Stripe** — `paymentIntentId, votes, chainid, factoryAddress` signing scheme stays identical (already chain-specific via `chainid`). But the Lambda needs to know which chains use stable coins so its USD→votes math doesn't apply Pyth conversion.
736. **`fundStripe` / `registerStripe` on CELO** β€” these grant votes WITHOUT pulling tokens (Stripe pre-paid in fiat). Makes sense β€” but be intentional that the dollar accounting is now decoupled from any on-chain token movement on those paths. Treasury wallets receive nothing on Stripe-funded registrations on the CELO chain.
747. **Min donation enforcement** β€” `MIN_DONATION = $20` becomes `20 * 10^decimals` set at deploy. On the native chain that comes from the oracle each call. Don't accidentally hard-code `20 ether` on the ERC20 path.
758. **AddressRegistry** β€” add a `stableCoin` slot or the frontend has to special-case CELO config lookup.
769. **Allowance stuck after failed tx** β€” if `donateFundsToken` reverts after `approve`, the user has dangling allowance. Recommend `approve(exactAmount)` per donation; warn against unlimited allowance.
7710. **Token address upgradability** β€” once deployed, the token address is immutable. If cUSD ever migrates contracts (it has, historically), you need a redeploy strategy. Consider an `onlyOwner setToken()` with a timelock β€” or accept redeployment as the migration path and document it.
7811. **Tests on real CELO Alfajores fork** β€” Hardhat fork tests against Alfajores cUSD catch decimals/permit edge cases that mocks miss.
7912. **Worker bundles** β€” `pnpm run cp:workers` regenerates UMD outputs; verify `web3-contracts.global.js` exposes the ERC20 ABI.
8013. **Block explorer verification** β€” verify both Factory variants on Sourcify/Blockscout for transparency; the ERC20 one will be different bytecode.
8114. **Stripe webhook β†’ vote accounting** β€” confirm the Lambda's per-chain `factoryAddress` lookup includes CELO, otherwise oracle signatures will be valid for the wrong contract.
8215. **Existing user balances on CORE/Sepolia** β€” none of this touches them (separate chains, separate deployments). But `web3-session` persistence keys should include chainId so a user switching to CELO doesn't see their CORE balance.
8316. **Documentation / `CLAUDE.md`** β€” update the EVM Networks table and the deploy-order section to reflect the two Factory variants.
84
85---
86
87## Suggested Implementation Order
88
891. **Contracts first** β€” abstract `Accountant` + `AccountantERC20` + `MockERC20`, with full unit tests at 6 and 18 decimals.
902. **Factory split** β€” `FactoryERC20.sol` mirroring `FactoryNative.sol`, with Stripe paths shared.
913. **Deploy scripts + config sync** β€” branch on `paymentMode`; deploy to Alfajores, run dcode diagnostics.
924. **Web3 SDK layer** β€” `web3-evm` token helpers, then `web3-vue` composable.
935. **Frontend pages** β€” `execute.vue` first (the most complex), then surrounding pages.
946. **Lambda update** β€” branch by chainId for vote calculation.
957. **End-to-end on Alfajores** β€” register, donate, Stripe path, full diagnostic before mainnet.