diff --git a/frontend/.astro/data-store.json b/frontend/.astro/data-store.json index 9e8a7dc..0b2f8b0 100644 --- a/frontend/.astro/data-store.json +++ b/frontend/.astro/data-store.json @@ -1 +1 @@ -[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/node\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/Users/antoineestienne/GithubRepositories/TheGuildGenesis/frontend/node_modules/.astro/sessions\"}}}"] \ No newline at end of file +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.13.7","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/node\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/Users/theodoreabitbol/Desktop/TheGuildGenesis/frontend/node_modules/.astro/sessions\"}}}"] \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2078c1e..77aadbb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3012,19 +3012,20 @@ } }, "node_modules/@metamask/sdk": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.32.0.tgz", - "integrity": "sha512-WmGAlP1oBuD9hk4CsdlG1WJFuPtYJY+dnTHJMeCyohTWD2GgkcLMUUuvu9lO1/NVzuOoSi1OrnjbuY1O/1NZ1g==", + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.33.1.tgz", + "integrity": "sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==", "dependencies": { "@babel/runtime": "^7.26.0", "@metamask/onboarding": "^1.0.1", "@metamask/providers": "16.1.0", - "@metamask/sdk-communication-layer": "0.32.0", - "@metamask/sdk-install-modal-web": "0.32.0", + "@metamask/sdk-analytics": "0.0.5", + "@metamask/sdk-communication-layer": "0.33.1", + "@metamask/sdk-install-modal-web": "0.32.1", "@paulmillr/qr": "^0.2.1", "bowser": "^2.9.0", "cross-fetch": "^4.0.0", - "debug": "^4.3.4", + "debug": "4.3.4", "eciesjs": "^0.4.11", "eth-rpc-errors": "^4.0.3", "eventemitter2": "^6.4.9", @@ -3037,22 +3038,32 @@ "uuid": "^8.3.2" } }, + "node_modules/@metamask/sdk-analytics": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@metamask/sdk-analytics/-/sdk-analytics-0.0.5.tgz", + "integrity": "sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==", + "license": "MIT", + "dependencies": { + "openapi-fetch": "^0.13.5" + } + }, "node_modules/@metamask/sdk-install-modal-web": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.32.0.tgz", - "integrity": "sha512-TFoktj0JgfWnQaL3yFkApqNwcaqJ+dw4xcnrJueMP3aXkSNev2Ido+WVNOg4IIMxnmOrfAC9t0UJ0u/dC9MjOQ==", + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.32.1.tgz", + "integrity": "sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==", "dependencies": { "@paulmillr/qr": "^0.2.1" } }, "node_modules/@metamask/sdk/node_modules/@metamask/sdk-communication-layer": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.32.0.tgz", - "integrity": "sha512-dmj/KFjMi1fsdZGIOtbhxdg3amxhKL/A5BqSU4uh/SyDKPub/OT+x5pX8bGjpTL1WPWY/Q0OIlvFyX3VWnT06Q==", + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.33.1.tgz", + "integrity": "sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==", "dependencies": { + "@metamask/sdk-analytics": "0.0.5", "bufferutil": "^4.0.8", "date-fns": "^2.29.3", - "debug": "^4.3.4", + "debug": "4.3.4", "utf-8-validate": "^5.0.2", "uuid": "^8.3.2" }, @@ -3073,6 +3084,29 @@ "node-fetch": "^2.7.0" } }, + "node_modules/@metamask/sdk/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@metamask/sdk/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, "node_modules/@metamask/superstruct": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.2.1.tgz", @@ -3083,9 +3117,9 @@ } }, "node_modules/@metamask/utils": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-11.7.0.tgz", - "integrity": "sha512-IamqpZF8Lr4WeXJ84fD+Sy+v1Zo05SYuMPHHBrZWpzVbnHAmXQpL4ckn9s5dfA+zylp3WGypaBPb6SBZdOhuNQ==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-11.8.1.tgz", + "integrity": "sha512-DIbsNUyqWLFgqJlZxi1OOCMYvI23GqFCvNJAtzv8/WXWzJfnJnvp1M24j7VvUe3URBi3S86UgQ7+7aWU9p/cnQ==", "license": "ISC", "dependencies": { "@ethereumjs/tx": "^4.2.0", @@ -7087,25 +7121,26 @@ } }, "node_modules/@wagmi/connectors": { - "version": "5.9.9", - "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-5.9.9.tgz", - "integrity": "sha512-6+eqU7P2OtxU2PkIw6kHojfYYUJykYG2K5rSkzVh29RDCAjhJqGEZW5f1b8kV5rUBORip1NpST8QTBNi96JHGQ==", + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-5.11.2.tgz", + "integrity": "sha512-OkiElOI8xXGPDZE5UdG6NgDT3laSkEh9llX1DDapUnfnKecK3Tr/HUf5YzgwDhEoox8mdxp+8ZCjtnTKz56SdA==", "license": "MIT", "dependencies": { "@base-org/account": "1.1.1", "@coinbase/wallet-sdk": "4.3.6", "@gemini-wallet/core": "0.2.0", - "@metamask/sdk": "0.32.0", + "@metamask/sdk": "0.33.1", "@safe-global/safe-apps-provider": "0.18.6", "@safe-global/safe-apps-sdk": "9.1.0", "@walletconnect/ethereum-provider": "2.21.1", - "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3" + "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3", + "porto": "0.2.19" }, "funding": { "url": "https://github.com/sponsors/wevm" }, "peerDependencies": { - "@wagmi/core": "2.20.3", + "@wagmi/core": "2.21.2", "typescript": ">=5.0.4", "viem": "2.x" }, @@ -7116,9 +7151,9 @@ } }, "node_modules/@wagmi/core": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.20.3.tgz", - "integrity": "sha512-gsbuHnWxf0AYZISvR8LvF/vUCIq6/ZwT5f5/FKd6wLA7Wq05NihCvmQpIgrcVbpSJPL67wb6S8fXm3eJGJA1vQ==", + "version": "2.21.2", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.21.2.tgz", + "integrity": "sha512-Rp4waam2z0FQUDINkJ91jq38PI5wFUHCv1YBL2LXzAQswaEk1ZY8d6+WG3vYGhFHQ22DXy2AlQ8IWmj+2EG3zQ==", "license": "MIT", "dependencies": { "eventemitter3": "5.0.1", @@ -12030,6 +12065,15 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hono": { + "version": "4.9.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.9.9.tgz", + "integrity": "sha512-Hxw4wT6zjJGZJdkJzAx9PyBdf7ZpxaTSA0NfxqjLghwMrLBX8p33hJBzoETRakF3UJu6OdNQBZAlNSkGqKFukw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -14705,6 +14749,21 @@ "regex-recursion": "^6.0.2" } }, + "node_modules/openapi-fetch": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.8.tgz", + "integrity": "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -15108,6 +15167,84 @@ "node": ">=12.0.0" } }, + "node_modules/porto": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/porto/-/porto-0.2.19.tgz", + "integrity": "sha512-q1vEJgdtlEOf6byWgD31GHiMwpfLuxFSfx9f7Sw4RGdvpQs2ANBGfnzzardADZegr87ZXsebSp+3vaaznEUzPQ==", + "license": "MIT", + "dependencies": { + "hono": "^4.9.6", + "idb-keyval": "^6.2.1", + "mipd": "^0.0.7", + "ox": "^0.9.6", + "zod": "^4.1.5", + "zustand": "^5.0.1" + }, + "bin": { + "porto": "_dist/cli/bin/index.js" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.59.0", + "@wagmi/core": ">=2.16.3", + "react": ">=18", + "typescript": ">=5.4.0", + "viem": ">=2.37.0", + "wagmi": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/react-query": { + "optional": true + }, + "react": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wagmi": { + "optional": true + } + } + }, + "node_modules/porto/node_modules/ox": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.7.tgz", + "integrity": "sha512-KX9Lvv0Rd+SKZXcT6Y85cuYbkmrQbK8Bz26k+s3X4EiT5bSU/hTDNSK/ApBeorJeIaOZbxEmJD2hHW0q1vIDEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/porto/node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -18060,13 +18197,13 @@ } }, "node_modules/wagmi": { - "version": "2.16.9", - "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.16.9.tgz", - "integrity": "sha512-5NbjvuNNhT0t0lQsDD5otQqZ5RZBM1UhInHoBq/Lpnr6xLLa8AWxYqHg5oZtGCdiUNltys11iBOS6z4mLepIqw==", + "version": "2.17.5", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.17.5.tgz", + "integrity": "sha512-Sk2e40gfo68gbJ6lHkpIwCMkH76rO0+toCPjf3PzdQX37rZo9042DdNTYcSg3zhnx8abFJtrk/5vAWfR8APTDw==", "license": "MIT", "dependencies": { - "@wagmi/connectors": "5.9.9", - "@wagmi/core": "2.20.3", + "@wagmi/connectors": "5.11.2", + "@wagmi/core": "2.21.2", "use-sync-external-store": "1.4.0" }, "funding": { diff --git a/frontend/src/components/AppWrapper.tsx b/frontend/src/components/AppWrapper.tsx index 80a1421..547cf82 100644 --- a/frontend/src/components/AppWrapper.tsx +++ b/frontend/src/components/AppWrapper.tsx @@ -2,7 +2,7 @@ import "@rainbow-me/rainbowkit/styles.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { WagmiProvider } from "wagmi"; import { RainbowKitProvider } from "@rainbow-me/rainbowkit"; -import { config } from "../lib/wagmi"; +import { getWagmiConfig } from "../lib/wagmi"; import { ConnectButton } from "@rainbow-me/rainbowkit"; import { SidebarProvider, @@ -20,6 +20,9 @@ interface AppWrapperProps { } export function AppWrapper({ children }: AppWrapperProps) { + // call the config getter inside the client component so it runs in the browser + const config = getWagmiConfig(); + return ( diff --git a/frontend/src/lib/wagmi.ts b/frontend/src/lib/wagmi.ts index 6fce4e3..90c516a 100644 --- a/frontend/src/lib/wagmi.ts +++ b/frontend/src/lib/wagmi.ts @@ -2,17 +2,20 @@ import { getDefaultConfig } from "@rainbow-me/rainbowkit"; import { http } from "wagmi"; import { polygonAmoy } from "wagmi/chains"; -const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as - | string - | undefined; -console.log(projectId); -export const config = getDefaultConfig({ - appName: "The Guild Genesis", - projectId: projectId ?? "", - chains: [polygonAmoy], - ssr: false, - syncConnectedChain: true, - transports: { - [polygonAmoy.id]: http(), - }, -}); +export function getWagmiConfig() { + const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as + | string + | undefined; + + // In tests or dev without a project id, the consumer can handle an empty string. + return getDefaultConfig({ + appName: "The Guild Genesis", + projectId: projectId ?? "", + chains: [polygonAmoy], + ssr: false, + syncConnectedChain: true, + transports: { + [polygonAmoy.id]: http(), + }, + }); +} diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro index b9664ea..eca83cc 100644 --- a/frontend/src/pages/index.astro +++ b/frontend/src/pages/index.astro @@ -6,8 +6,9 @@ import HomePage from '@/components/pages/HomePage';
- - - + +
+ +
\ No newline at end of file diff --git a/frontend/src/test/AppWrapper.test.tsx b/frontend/src/test/AppWrapper.test.tsx index 1e9dddd..15a60d0 100644 --- a/frontend/src/test/AppWrapper.test.tsx +++ b/frontend/src/test/AppWrapper.test.tsx @@ -36,9 +36,9 @@ vi.mock("@rainbow-me/rainbowkit", () => ({ getDefaultConfig: () => ({}), })); -// Mock the wagmi config +// Mock the wagmi config getter vi.mock("@/lib/wagmi", () => ({ - config: {}, + getWagmiConfig: () => ({}), })); describe("AppWrapper", () => { diff --git a/frontend/src/test/ProfileCard.test.tsx b/frontend/src/test/ProfileCard.test.tsx index 9d1804a..4efcfea 100644 --- a/frontend/src/test/ProfileCard.test.tsx +++ b/frontend/src/test/ProfileCard.test.tsx @@ -41,9 +41,9 @@ vi.mock("@rainbow-me/rainbowkit", () => ({ getDefaultConfig: () => ({}), })); -// Mock the wagmi config +// Mock the wagmi config getter vi.mock("@/lib/wagmi", () => ({ - config: {}, + getWagmiConfig: () => ({}), })); describe("ProfileCard", () => { diff --git a/the-guild-smart-contracts/TGC_README.md b/the-guild-smart-contracts/TGC_README.md new file mode 100644 index 0000000..8cdf215 --- /dev/null +++ b/the-guild-smart-contracts/TGC_README.md @@ -0,0 +1,253 @@ +# TheGuild Contribution Token (TGC) + +<<<<<<< HEAD +A standard ERC20 token designed for rewarding contributions to The Guild community. The token features owner-controlled minting, batch operations for gas efficiency, and a maximum supply cap. + +## Features + +- **Standard ERC20**: Full compatibility with ERC20 standard +- **Ownable**: Only the owner can mint tokens +- **Batch Minting**: Gas-efficient batch operations for multiple recipients +- **Maximum Supply**: Capped at 1 billion tokens to prevent infinite inflation +- **CSV Support**: Scripts designed to work with CSV data for token distribution +- **Comprehensive Testing**: Full test suite covering all functionality + +## Contract Details + +- **Name**: TheGuild Contribution Token +- **Symbol**: TGC +- **Decimals**: 18 (standard) +- **Max Supply**: 1,000,000,000 TGC (1 billion tokens) +- **Initial Supply**: 0 (all tokens must be explicitly minted) + +## Deployment + +### Deploy New Contract + +```bash +# Deploy with initial owner +forge script script/DeployTGC.s.sol --rpc-url --private-key --broadcast + +# Deploy with initial minting (modify recipients/amounts in script first) +forge script script/DeployTGC.s.sol:DeployTGC --rpc-url --private-key --broadcast +``` + +### Mint Tokens on Existing Contract + +```bash +# Set the contract address +export TGC_TOKEN_ADDRESS=0x... + +# Mint tokens (modify recipients/amounts in script first) +forge script script/MintTGC.s.sol --rpc-url --private-key --broadcast +``` + +## CSV Data Format + +Create a CSV file with the following format for basic minting: + +```csv +address,amount +0x1234567890123456789012345678901234567890,1000 +0x2345678901234567890123456789012345678901,2000 +``` + +Or with reasons for GitHub issue tracking: + +```csv +address,amount,reason +0x1234567890123456789012345678901234567890,1000,GitHub-Issue-123 +0x2345678901234567890123456789012345678901,2000,GitHub-Issue-456 +``` + +- **address**: Ethereum address of the recipient +- **amount**: Amount in whole tokens (will be converted to wei automatically) +- **reason**: GitHub issue ID, ticket reference, or contribution identifier + +## Usage Examples + +### 1. Deploy Contract Only + +```solidity +// In DeployTGC.s.sol, modify the run() function: +function run() external { + address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); + deployToken(deployer); +} +``` + +### 2. Deploy with Initial Distribution + +```solidity +// Prepare your recipient data +address[] memory recipients = new address[](3); +uint256[] memory amounts = new uint256[](3); + +recipients[0] = 0x1234567890123456789012345678901234567890; +recipients[1] = 0x2345678901234567890123456789012345678901; +recipients[2] = 0x3456789012345678901234567890123456789012; + +amounts[0] = 1000; // 1000 TGC +amounts[1] = 2000; // 2000 TGC +amounts[2] = 500; // 500 TGC + +// Deploy with initial minting +deployTokenWithInitialMint(deployer, recipients, amounts); +``` + +### 3. Mint on Existing Contract + +```bash +# Set environment variables +export TGC_TOKEN_ADDRESS=0x... # Your deployed contract address +export PRIVATE_KEY=0x... # Owner's private key + +# Run minting script +forge script script/MintTGC.s.sol --rpc-url --broadcast +``` + +## Testing + +Run the complete test suite: + +```bash +# Run all tests +forge test + +# Run tests with verbose output +forge test -vv + +# Run specific test file +forge test --match-path test/TheGuildContributionToken.t.sol + +# Run with gas reporting +forge test --gas-report + +# Run fuzz tests +forge test --fuzz-runs 1000 +``` + +## Contract Functions + +### Owner Functions + +- `mint(address to, uint256 amount)`: Mint tokens to a single recipient +- `mintWithReason(address to, uint256 amount, bytes32 reason)`: Mint tokens with a reason (e.g., GitHub issue reference) +- `batchMint(address[] recipients, uint256[] amounts)`: Mint tokens to multiple recipients +- `batchMintWithReasons(address[] recipients, uint256[] amounts, bytes32[] reasons)`: Mint tokens to multiple recipients with reasons +- `transferOwnership(address newOwner)`: Transfer contract ownership +- `renounceOwnership()`: Renounce ownership permanently + +### View Functions + +- `maxSupply()`: Returns the maximum total supply (1 billion tokens) +- `remainingSupply()`: Returns how many tokens can still be minted +- `totalSupply()`: Returns current total supply +- `balanceOf(address account)`: Returns account balance +- `owner()`: Returns current owner address + +### Standard ERC20 Functions + +- `transfer(address to, uint256 amount)`: Transfer tokens +- `approve(address spender, uint256 amount)`: Approve spending +- `transferFrom(address from, address to, uint256 amount)`: Transfer on behalf + +## Events + +- `Mint(address indexed to, uint256 amount)`: Emitted when tokens are minted to a single recipient +- `ContributionTokenMinted(address indexed recipient, uint256 amount, bytes32 indexed reason)`: Emitted when tokens are minted with a reason +- `BatchMint(address indexed owner, uint256 totalAmount, uint256 recipientCount)`: Emitted during batch minting +- Standard ERC20 events: `Transfer`, `Approval` + +## Security Features + +- **Ownable**: Only the contract owner can mint tokens +- **Zero Address Protection**: Cannot mint to the zero address +- **Zero Amount Protection**: Cannot mint zero tokens +- **Maximum Supply Protection**: Cannot exceed 1 billion total tokens +- **Array Validation**: Batch operations validate array lengths and contents + +## Gas Optimization + +- **Batch Operations**: Use `batchMint()` for multiple recipients to save gas +- **Efficient Storage**: Minimal storage usage for optimal gas costs +- **Event Optimization**: Events designed for efficient indexing + +## Development Notes + +- The contract uses OpenZeppelin's battle-tested implementations +- All functions include proper input validation +- Events are emitted for all important state changes +- The contract is designed to be upgrade-safe (no storage conflicts) + +## Integration with The Guild + +This token is designed to integrate with The Guild's contribution tracking system: + +1. **Deployment**: Deploy the contract with The Guild multisig as owner +2. **GitHub Integration**: Use `mintWithReason()` to reference specific GitHub issues/PRs +3. **Database Updates**: Listen for `ContributionTokenMinted` events to update ticket status to "rewarded" +4. **Distribution**: Use CSV files to distribute tokens based on contribution metrics +5. **Tracking**: The `reason` field allows automatic correlation with GitHub issues +6. **Governance**: Token holders can participate in Guild governance (future feature) + +### GitHub Issue Tracking Workflow + +1. **Contributor completes task**: GitHub issue #123 is closed +2. **Guild admin rewards**: `mintWithReason(contributor, 1000e18, keccak256("GitHub-Issue-123"))` +3. **Event emitted**: `ContributionTokenMinted(contributor, 1000e18, "GitHub-Issue-123")` +4. **Database updated**: Backend service listens for event and marks issue as "rewarded" +5. **Transparency**: All rewards are publicly verifiable on-chain + +## Support + +For questions or issues: +- GitHub Issues: [Repository Issues](https://github.com/tusharrrr1/TheGuildGenesis/issues) +- Discord: [The Guild Discord](https://discord.gg/axCqT23Xhj) +======= +The TheGuild Contribution Token (TGC) is a simple ERC20 token used to reward contributors. It is owner-mintable and supports batch minting with a distribution ID to prevent duplicate distributions. + +## Key features + +- ERC20 token (symbol: TGC) +- Owner-only minting +- `mintWithReason(to, amount, reason)` — mints and emits `ContributionTokenMinted(to, amount, reason)` where `reason` is a `bytes32` reference (e.g., a ticket id) +- `batchMint(distributionId, recipients[], amounts[], reasons[])` — mints to many recipients in one call and marks `distributionId` as executed so it cannot be re-used. Use this to ensure a CSV or distribution is only applied once. + +## `distributionId` + +`distributionId` should be a unique identifier for a distribution batch. Recommended approaches: + +- `keccak256(bytes(csvContents))` — compute a hash of the CSV file contents and use that as the id. +- `keccak256(abi.encodePacked(fileName, timestamp))` — if you want to include a timestamp. + +The contract tracks which `distributionId`s have been executed and will revert if the same id is passed twice. + +## CSV format and scripts + +Scripts in the `script/` folder support minting via CSV input. CSV format: +``` +
,, +``` +- `address` — recipient address (0x...) +- `amount` — integer amount (the Node helper multiplies by token decimals by default) +- `hex32_reason` — a 0x-prefixed hex string up to 32 bytes. You can also provide a plain string and the Node helper will hex-encode it. + +Examples: +- `script/DeployAndMintTGC.s.sol` — deploys a new TGC and batch mints the CSV contents (computes `distributionId = keccak256(csv)`). +- `script/MintTGCExisting.s.sol` — attaches to an existing TGC address and batch mints the CSV contents. +- `script/mint_from_csv.js` — Node helper that performs batched `batchMint` calls. Useful for large lists and retries. + +## Tests + +There are unit tests under `test/` that verify `mintWithReason`, `batchMint`, and the `distributionId` guard. + +## GitHub issue workflow (example) + +1. Contributor closes issue #65 and requests reward. +2. Admin prepares a CSV with recipients, amounts, and reasons (include the issue id in reason where useful). +3. Compute `distributionId = keccak256(csvContents)` and run the deploy/mint script. +4. The contract will mark the `distributionId` executed so you cannot accidentally re-run the same distribution. + +If you want, I can add an example script which computes `distributionId` and calls the Forge script automatically, or a small GitHub Action that runs the Node helper on a CSV uploaded to the repo. +>>>>>>> 9dff903106d20ed0497926497613df5111737be9 diff --git a/the-guild-smart-contracts/examples/recipients.csv b/the-guild-smart-contracts/examples/recipients.csv new file mode 100644 index 0000000..06deb9e --- /dev/null +++ b/the-guild-smart-contracts/examples/recipients.csv @@ -0,0 +1,11 @@ +address,amount,reason +0x1234567890123456789012345678901234567890,1000,GitHub-Issue-123 +0x2345678901234567890123456789012345678901,2000,GitHub-Issue-456 +0x3456789012345678901234567890123456789012,500,GitHub-Issue-789 +0x4567890123456789012345678901234567890123,750,GitHub-Issue-101 +0x5678901234567890123456789012345678901234,1250,GitHub-Issue-202 +0x6789012345678901234567890123456789012345,300,GitHub-Issue-303 +0x7890123456789012345678901234567890123456,1800,GitHub-Issue-404 +0x8901234567890123456789012345678901234567,450,GitHub-Issue-505 +0x9012345678901234567890123456789012345678,2250,GitHub-Issue-606 +0xa123456789012345678901234567890123456789,650,GitHub-Issue-707 \ No newline at end of file diff --git a/the-guild-smart-contracts/script/DeployAndMintTGC.s.sol b/the-guild-smart-contracts/script/DeployAndMintTGC.s.sol new file mode 100644 index 0000000..8a230c4 --- /dev/null +++ b/the-guild-smart-contracts/script/DeployAndMintTGC.s.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +contract DeployAndMintTGC is Script { + function run() public { + // CSV file path relative to project root + string memory csvPath = "script/data/initial_tgc_mints.csv"; + + vm.startBroadcast(); + + TheGuildContributionToken token = new TheGuildContributionToken(); + + // read CSV and prepare arrays for batchMint + string memory csv = vm.readFile(csvPath); + string[] memory lines = csvSplit(csv); + + // count non-empty lines + uint256 count = 0; + for (uint256 i = 0; i < lines.length; i++) if (bytes(lines[i]).length != 0) count++; + + address[] memory recipients = new address[](count); + uint256[] memory amounts = new uint256[](count); + bytes32[] memory reasons = new bytes32[](count); + + uint256 idx = 0; + for (uint256 i = 0; i < lines.length; i++) { + string memory line = lines[i]; + if (bytes(line).length == 0) continue; + (address recipient, uint256 amount, bytes32 reason) = parseLine(line); + recipients[idx] = recipient; + amounts[idx] = amount; + reasons[idx] = reason; + idx++; + } + + // compute distributionId as keccak256(csv) + bytes32 distributionId = keccak256(bytes(csv)); + + // perform batch mint + token.batchMint(distributionId, recipients, amounts, reasons); + + vm.stopBroadcast(); + } + + // --- small CSV parser helpers --- + function csvSplit(string memory s) internal pure returns (string[] memory) { + // naive split on '\n' + bytes memory b = bytes(s); + uint256 linesCount = 1; + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == "\n") linesCount++; + } + string[] memory parts = new string[](linesCount); + uint256 idx = 0; + bytes memory cur; + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == "\n") { + parts[idx] = string(cur); + idx++; + cur = ""; + } else { + cur = abi.encodePacked(cur, b[i]); + } + } + // last + if (cur.length != 0) { + parts[idx] = string(cur); + } + return parts; + } + + function parseLine(string memory line) internal pure returns (address, uint256, bytes32) { + // naive split by comma + bytes memory b = bytes(line); + string[] memory cols = new string[](3); + uint256 col = 0; + bytes memory cur; + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == ",") { + cols[col] = string(cur); + col++; + cur = ""; + } else { + cur = abi.encodePacked(cur, b[i]); + } + } + if (cur.length != 0) cols[col] = string(cur); + + address recipient = parseAddr(cols[0]); + uint256 amount = parseUint(cols[1]); + bytes32 reason = parseBytes32Hex(cols[2]); + return (recipient, amount, reason); + } + + function parseAddr(string memory s) internal pure returns (address) { + bytes memory bb = bytes(s); + // fallback to address(0) if empty + if (bb.length == 0) return address(0); + return parseAddrFromHex(s); + } + + function parseAddrFromHex(string memory s) internal pure returns (address) { + bytes memory _s = bytes(s); + uint256 start = 0; + if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2; + require(_s.length - start == 40, "INVALID_ADDR_LENGTH"); + uint160 addr = 0; + for (uint256 i = start; i < _s.length; i++) { + addr <<= 4; + uint8 c = uint8(_s[i]); + if (c >= 48 && c <= 57) addr |= uint160(c - 48); + else if (c >= 65 && c <= 70) addr |= uint160(c - 55); + else if (c >= 97 && c <= 102) addr |= uint160(c - 87); + else revert("INVALID_HEX_CHAR"); + } + return address(addr); + } + + function parseUint(string memory s) internal pure returns (uint256) { + bytes memory b = bytes(s); + uint256 n = 0; + for (uint256 i = 0; i < b.length; i++) { + uint8 c = uint8(b[i]); + if (c >= 48 && c <= 57) n = n * 10 + (c - 48); + } + return n; + } + + function parseBytes32Hex(string memory s) internal pure returns (bytes32) { + bytes memory _s = bytes(s); + if (_s.length == 0) return bytes32(0); + uint256 start = 0; + if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2; + bytes32 res = bytes32(0); + uint256 chars = _s.length - start; + // read up to 64 hex chars (32 bytes) + uint256 toRead = chars > 64 ? 64 : chars; + for (uint256 i = 0; i < toRead; i++) { + res <<= 4; + uint8 c = uint8(_s[start + i]); + if (c >= 48 && c <= 57) res |= bytes32(uint256(c - 48)); + else if (c >= 65 && c <= 70) res |= bytes32(uint256(c - 55)); + else if (c >= 97 && c <= 102) res |= bytes32(uint256(c - 87)); + else revert("INVALID_HEX_CHAR"); + } + // shift left to make it right-aligned? Keep as parsed (big-endian) + return res; + } +} diff --git a/the-guild-smart-contracts/script/DeployTGC.s.sol b/the-guild-smart-contracts/script/DeployTGC.s.sol new file mode 100644 index 0000000..afbfdff --- /dev/null +++ b/the-guild-smart-contracts/script/DeployTGC.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +/// @title TGC Deployment Script +/// @notice Script to deploy TheGuildContributionToken and optionally mint initial balances +contract DeployTGC is Script { + + /// @notice Deploy the TGC token contract + /// @param initialOwner The address that will own the contract + /// @return token The deployed TGC token contract + function deployToken(address initialOwner) public returns (TheGuildContributionToken) { + vm.startBroadcast(); + + TheGuildContributionToken token = new TheGuildContributionToken(initialOwner); + + vm.stopBroadcast(); + + console.log("TheGuildContributionToken deployed at:", address(token)); + console.log("Owner:", token.owner()); + console.log("Name:", token.name()); + console.log("Symbol:", token.symbol()); + console.log("Decimals:", token.decimals()); + console.log("Max Supply:", token.maxSupply()); + + return token; + } + + /// @notice Deploy token and mint initial balances from CSV data + /// @param initialOwner The address that will own the contract + /// @param recipients Array of recipient addresses + /// @param amounts Array of amounts to mint (in tokens, will be converted to wei) + /// @return token The deployed TGC token contract + function deployTokenWithInitialMint( + address initialOwner, + address[] memory recipients, + uint256[] memory amounts + ) public returns (TheGuildContributionToken) { + TheGuildContributionToken token = deployToken(initialOwner); + + if (recipients.length > 0) { + vm.startBroadcast(); + + // Convert amounts to wei (multiply by 10^18) + uint256[] memory amountsInWei = new uint256[](amounts.length); + for (uint256 i = 0; i < amounts.length; i++) { + amountsInWei[i] = amounts[i] * 10**18; + } + + token.batchMint(recipients, amountsInWei); + + vm.stopBroadcast(); + + console.log("Initial minting completed:"); + console.log("Recipients:", recipients.length); + console.log("Total Supply:", token.totalSupply()); + } + + return token; + } + + /// @notice Main deployment function + function run() external { + // Get deployer address from private key + address deployer = vm.addr(vm.envUint("PRIVATE_KEY")); + + // Example recipients and amounts (replace with actual CSV data) + address[] memory recipients = new address[](3); + uint256[] memory amounts = new uint256[](3); + + // Example data - replace with actual addresses and amounts + recipients[0] = 0x1234567890123456789012345678901234567890; + recipients[1] = 0x2345678901234567890123456789012345678901; + recipients[2] = 0x3456789012345678901234567890123456789012; + + amounts[0] = 1000; // 1000 TGC tokens + amounts[1] = 2000; // 2000 TGC tokens + amounts[2] = 500; // 500 TGC tokens + + // Deploy with initial minting + deployTokenWithInitialMint(deployer, recipients, amounts); + } +} \ No newline at end of file diff --git a/the-guild-smart-contracts/script/MintTGC.s.sol b/the-guild-smart-contracts/script/MintTGC.s.sol new file mode 100644 index 0000000..2fc3cda --- /dev/null +++ b/the-guild-smart-contracts/script/MintTGC.s.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +/// @title TGC Mint Script +/// @notice Script to mint tokens on an already deployed TGC contract +contract MintTGC is Script { + + /// @notice Mint tokens to recipients from CSV data on existing contract + /// @param tokenAddress The address of the deployed TGC contract + /// @param recipients Array of recipient addresses + /// @param amounts Array of amounts to mint (in tokens, will be converted to wei) + function mintTokens( + address tokenAddress, + address[] memory recipients, + uint256[] memory amounts + ) public { + require(tokenAddress != address(0), "Invalid token address"); + require(recipients.length == amounts.length, "Arrays length mismatch"); + require(recipients.length > 0, "Empty arrays"); + + TheGuildContributionToken token = TheGuildContributionToken(tokenAddress); + + vm.startBroadcast(); + + // Convert amounts to wei (multiply by 10^18) + uint256[] memory amountsInWei = new uint256[](amounts.length); + for (uint256 i = 0; i < amounts.length; i++) { + amountsInWei[i] = amounts[i] * 10**18; + } + + // Use batch mint for gas efficiency + token.batchMint(recipients, amountsInWei); + + vm.stopBroadcast(); + + console.log("Minting completed:"); + console.log("Contract Address:", tokenAddress); + console.log("Recipients:", recipients.length); + console.log("New Total Supply:", token.totalSupply()); + console.log("Remaining Supply:", token.remainingSupply()); + } + + /// @notice Mint tokens to a single recipient with reason + /// @param tokenAddress The address of the deployed TGC contract + /// @param recipient The recipient address + /// @param amount Amount to mint (in tokens, will be converted to wei) + /// @param reason The reason for minting (e.g., GitHub issue hash) + function mintToSingleWithReason( + address tokenAddress, + address recipient, + uint256 amount, + bytes32 reason + ) public { + require(tokenAddress != address(0), "Invalid token address"); + require(recipient != address(0), "Invalid recipient address"); + require(amount > 0, "Amount must be greater than zero"); + require(reason != bytes32(0), "Reason cannot be empty"); + + TheGuildContributionToken token = TheGuildContributionToken(tokenAddress); + + vm.startBroadcast(); + + uint256 amountInWei = amount * 10**18; + token.mintWithReason(recipient, amountInWei, reason); + + vm.stopBroadcast(); + + console.log("Single mint with reason completed:"); + console.log("Contract Address:", tokenAddress); + console.log("Recipient:", recipient); + console.log("Amount:", amount, "TGC"); + console.log("Reason:", vm.toString(reason)); + console.log("New Total Supply:", token.totalSupply()); + } + + /// @notice Mint tokens to recipients with reasons from CSV data + /// @param tokenAddress The address of the deployed TGC contract + /// @param recipients Array of recipient addresses + /// @param amounts Array of amounts to mint (in tokens, will be converted to wei) + /// @param reasons Array of reasons for each mint + function mintTokensWithReasons( + address tokenAddress, + address[] memory recipients, + uint256[] memory amounts, + bytes32[] memory reasons + ) public { + require(tokenAddress != address(0), "Invalid token address"); + require(recipients.length == amounts.length, "Recipients/amounts length mismatch"); + require(recipients.length == reasons.length, "Recipients/reasons length mismatch"); + require(recipients.length > 0, "Empty arrays"); + + TheGuildContributionToken token = TheGuildContributionToken(tokenAddress); + + vm.startBroadcast(); + + // Convert amounts to wei (multiply by 10^18) + uint256[] memory amountsInWei = new uint256[](amounts.length); + for (uint256 i = 0; i < amounts.length; i++) { + amountsInWei[i] = amounts[i] * 10**18; + } + + // Use batch mint with reasons for gas efficiency + token.batchMintWithReasons(recipients, amountsInWei, reasons); + + vm.stopBroadcast(); + + console.log("Minting with reasons completed:"); + console.log("Contract Address:", tokenAddress); + console.log("Recipients:", recipients.length); + console.log("New Total Supply:", token.totalSupply()); + console.log("Remaining Supply:", token.remainingSupply()); + } + + /// @notice Main function for minting from CSV data with reasons + function run() external { + // Get the deployed contract address from environment variable or hardcode + address tokenAddress = vm.envAddress("TGC_TOKEN_ADDRESS"); + + // Example recipients, amounts, and reasons (replace with actual CSV data) + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + bytes32[] memory reasons = new bytes32[](2); + + // Example data - replace with actual addresses, amounts, and reasons from CSV + recipients[0] = 0x4567890123456789012345678901234567890123; + recipients[1] = 0x5678901234567890123456789012345678901234; + + amounts[0] = 500; // 500 TGC tokens + amounts[1] = 1500; // 1500 TGC tokens + + // GitHub issue hashes or ticket IDs + reasons[0] = keccak256("GitHub-Issue-123"); + reasons[1] = keccak256("GitHub-Issue-456"); + + // Mint tokens to recipients with reasons + mintTokensWithReasons(tokenAddress, recipients, amounts, reasons); + } +} \ No newline at end of file diff --git a/the-guild-smart-contracts/script/MintTGCExisting.s.sol b/the-guild-smart-contracts/script/MintTGCExisting.s.sol new file mode 100644 index 0000000..0653699 --- /dev/null +++ b/the-guild-smart-contracts/script/MintTGCExisting.s.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +contract MintTGCExisting is Script { + function run(address tokenAddress) public { + string memory csvPath = "script/data/initial_tgc_mints.csv"; + + TheGuildContributionToken token = TheGuildContributionToken(tokenAddress); + + vm.startBroadcast(); + + string memory csv = vm.readFile(csvPath); + string[] memory lines = csvSplit(csv); + + uint256 count = 0; + for (uint256 i = 0; i < lines.length; i++) if (bytes(lines[i]).length != 0) count++; + + address[] memory recipients = new address[](count); + uint256[] memory amounts = new uint256[](count); + bytes32[] memory reasons = new bytes32[](count); + + uint256 idx = 0; + for (uint256 i = 0; i < lines.length; i++) { + string memory line = lines[i]; + if (bytes(line).length == 0) continue; + (address recipient, uint256 amount, bytes32 reason) = parseLine(line); + recipients[idx] = recipient; + amounts[idx] = amount; + reasons[idx] = reason; + idx++; + } + + bytes32 distributionId = keccak256(bytes(csv)); + token.batchMint(distributionId, recipients, amounts, reasons); + + vm.stopBroadcast(); + } + + // reuse simple parsers from deploy script + function csvSplit(string memory s) internal pure returns (string[] memory) { + bytes memory b = bytes(s); + uint256 linesCount = 1; + for (uint256 i = 0; i < b.length; i++) if (b[i] == "\n") linesCount++; + string[] memory parts = new string[](linesCount); + uint256 idx = 0; + bytes memory cur; + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == "\n") { parts[idx] = string(cur); idx++; cur = ""; } + else { cur = abi.encodePacked(cur, b[i]); } + } + if (cur.length != 0) parts[idx] = string(cur); + return parts; + } + + function parseLine(string memory line) internal pure returns (address, uint256, bytes32) { + bytes memory b = bytes(line); + string[] memory cols = new string[](3); + uint256 col = 0; + bytes memory cur; + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == ",") { cols[col] = string(cur); col++; cur = ""; } + else { cur = abi.encodePacked(cur, b[i]); } + } + if (cur.length != 0) cols[col] = string(cur); + return (parseAddrFromHex(cols[0]), parseUint(cols[1]), parseBytes32Hex(cols[2])); + } + + function parseAddrFromHex(string memory s) internal pure returns (address) { + bytes memory _s = bytes(s); + uint256 start = 0; + if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2; + require(_s.length - start == 40, "INVALID_ADDR_LENGTH"); + uint160 addr = 0; + for (uint256 i = start; i < _s.length; i++) { + addr <<= 4; + uint8 c = uint8(_s[i]); + if (c >= 48 && c <= 57) addr |= uint160(c - 48); + else if (c >= 65 && c <= 70) addr |= uint160(c - 55); + else if (c >= 97 && c <= 102) addr |= uint160(c - 87); + else revert("INVALID_HEX_CHAR"); + } + return address(addr); + } + + function parseUint(string memory s) internal pure returns (uint256) { + bytes memory b = bytes(s); uint256 n = 0; for (uint256 i = 0; i < b.length; i++) { uint8 c = uint8(b[i]); if (c >= 48 && c <= 57) n = n * 10 + (c - 48); } return n; + } + + function parseBytes32Hex(string memory s) internal pure returns (bytes32) { + bytes memory _s = bytes(s); + if (_s.length == 0) return bytes32(0); + uint256 start = 0; + if (_s.length >= 2 && _s[0] == '0' && (_s[1] == 'x' || _s[1] == 'X')) start = 2; + bytes32 res = bytes32(0); + uint256 chars = _s.length - start; uint256 toRead = chars > 64 ? 64 : chars; + for (uint256 i = 0; i < toRead; i++) { res <<= 4; uint8 c = uint8(_s[start + i]); if (c >= 48 && c <= 57) res |= bytes32(uint256(c - 48)); else if (c >= 65 && c <= 70) res |= bytes32(uint256(c - 55)); else if (c >= 97 && c <= 102) res |= bytes32(uint256(c - 87)); else revert("INVALID_HEX_CHAR"); } + return res; + } +} diff --git a/the-guild-smart-contracts/script/README.md b/the-guild-smart-contracts/script/README.md new file mode 100644 index 0000000..7bab3ca --- /dev/null +++ b/the-guild-smart-contracts/script/README.md @@ -0,0 +1,60 @@ +Local usage for TGC scripts + +This folder contains scripts and helpers for deploying and minting TheGuild Contribution Token (TGC). + +Files +- DeployAndMintTGC.s.sol - Forge script that deploys TGC and mints entries from `script/data/initial_tgc_mints.csv`. +- MintTGCExisting.s.sol - Forge script that attaches to an existing TGC contract and mints entries from `script/data/initial_tgc_mints.csv`. +- data/initial_tgc_mints.csv - Example CSV data. Format: `address,amount,hex32_reason` (amount in token base units, reason as 0x-prefixed hex up to 32 bytes). +- mint_from_csv.js - (optional) Node.js script that reads a CSV and calls `mintWithReason` on an already-deployed TGC contract via ethers.js. + +Running locally + +Prerequisites +- Node.js >= 18 (for the helper script) +- Foundry (forge & cast) installed for running forge scripts +- An Ethereum RPC URL (local node, ganache, anvil, or public testnet) +- Private key with ETH for gas (or use anvil's default accounts) + +1) Run tests + +In the `the-guild-smart-contracts` folder: + +```bash +forge test -v +``` + +2) Deploy & mint using Forge scripts + +Deploy new TGC and mint using the CSV (reads `script/data/initial_tgc_mints.csv`): + +```bash +forge script script/DeployAndMintTGC.s.sol:DeployAndMintTGC --rpc-url --private-key --broadcast -vvvv +``` + +Mint to an existing deployed token address: + +```bash +forge script script/MintTGCExisting.s.sol:MintTGCExisting --rpc-url --private-key --broadcast -vvvv --sig "run(address)" --json-args '[["0xYourTokenAddress"]]' +``` + +(Forge and versions differ; `--sig`/`--json-args` usage might require different quoting depending on shell.) + +3) Off-chain helper (Node.js) + +The repo includes `mint_from_csv.js` to mint using ethers.js. You can use it instead of the on-chain CSV parser for large lists. + +Install deps and run: + +```bash +cd the-guild-smart-contracts/script +npm install +node mint_from_csv.js --rpc-url --private-key --token --csv data/initial_tgc_mints.csv +``` + +Notes & improvements +- The on-chain CSV parser in Forge scripts is simplistic and intended for small example files. For large lists, use the Node helper (or batch the calls). +- Event `ContributionTokenMinted(address,uint256,bytes32)` is emitted on mintWithReason. Consider indexing the `bytes32` reason for easier log filtering. +- If CSV amounts are in tokens (e.g., 1 means 1 TGC), you may want to multiply by 10**decimals in the helper script. + +If you'd like, I can add a NPM package manifest and install script dependencies for you, or change the Node helper to support batching and retries. \ No newline at end of file diff --git a/the-guild-smart-contracts/script/data/initial_tgc_mints.csv b/the-guild-smart-contracts/script/data/initial_tgc_mints.csv new file mode 100644 index 0000000..075b233 --- /dev/null +++ b/the-guild-smart-contracts/script/data/initial_tgc_mints.csv @@ -0,0 +1,2 @@ +0x0000000000000000000000000000000000000001,1000,0x7469636b65745f31 +0x0000000000000000000000000000000000000002,2000,0x7469636b65745f32 diff --git a/the-guild-smart-contracts/script/mint_from_csv.js b/the-guild-smart-contracts/script/mint_from_csv.js new file mode 100644 index 0000000..a26c47a --- /dev/null +++ b/the-guild-smart-contracts/script/mint_from_csv.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +const fs = require('fs'); +const {ethers} = require('ethers'); +const yargs = require('yargs'); + +const argv = yargs + .option('rpc-url', {type: 'string', demandOption: true}) + .option('private-key', {type: 'string', demandOption: true}) + .option('token', {type: 'string', demandOption: true}) + .option('csv', {type: 'string', default: 'data/initial_tgc_mints.csv'}) + .option('decimals', {type: 'number', default: 18}) + .option('batch', {type: 'number', default: 20}) + .argv; + +const abi = [ + 'function batchMint(bytes32 distributionId, address[] recipients, uint256[] amounts, bytes32[] reasons) external', + 'function decimals() view returns (uint8)' +]; + +async function main() { + const provider = new ethers.providers.JsonRpcProvider(argv['rpc-url']); + const wallet = new ethers.Wallet(argv['private-key'], provider); + const token = new ethers.Contract(argv.token, abi, wallet); + + let decimals = argv.decimals; + try { decimals = await token.decimals(); } catch (e) { console.log('could not read decimals, using', decimals); } + + const csv = fs.readFileSync(argv.csv, 'utf8'); + const lines = csv.split(/\r?\n/).filter(l => l.trim().length > 0); + const entries = lines.map(l => { + const [addr, amt, reason] = l.split(',').map(s => s.trim()); + const amount = ethers.BigNumber.from(amt).mul(ethers.BigNumber.from(10).pow(decimals)); + const r = reason && reason.startsWith('0x') ? reason : '0x' + Buffer.from(reason || '').toString('hex'); + return {addr, amount, reason: r}; + }); + + // perform batched batchMint calls with distribution id derived from CSV content + offset + for (let i=0;i b.addr); + const amounts = batch.map(b => b.amount); + const reasons = batch.map(b => { + // ensure 0x-prefixed 32-byte hex; pad/truncate if necessary + const r = b.reason.startsWith('0x') ? b.reason : '0x' + Buffer.from(b.reason || '').toString('hex'); + return r; + }); + + const distributionId = ethers.keccak256(ethers.toUtf8Bytes(argv.csv + ':' + i)); + console.log('batchMinting', recipients.length, 'recipients, distributionId', distributionId); + const tx = await token.batchMint(distributionId, recipients, amounts, reasons); + console.log('tx', tx.hash); + await tx.wait(); + } +} + +main().catch(err => { console.error(err); process.exit(1); }); diff --git a/the-guild-smart-contracts/script/package.json b/the-guild-smart-contracts/script/package.json new file mode 100644 index 0000000..562a7cb --- /dev/null +++ b/the-guild-smart-contracts/script/package.json @@ -0,0 +1,12 @@ +{ + "name": "tgc-scripts", + "version": "0.1.0", + "license": "MIT", + "scripts": { + "mint": "node mint_from_csv.js" + }, + "dependencies": { + "ethers": "^6.8.0", + "yargs": "^17.7.2" + } +} diff --git a/the-guild-smart-contracts/script/utils/CSVUtils.s.sol b/the-guild-smart-contracts/script/utils/CSVUtils.s.sol new file mode 100644 index 0000000..9ad3e88 --- /dev/null +++ b/the-guild-smart-contracts/script/utils/CSVUtils.s.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, console} from "forge-std/Script.sol"; + +/// @title CSV Processing Utilities for TGC Scripts +/// @notice Helper functions to process CSV data for token operations +library CSVProcessor { + + /// @notice Parse a CSV line into address and amount + /// @dev This is a simplified parser - in production, use off-chain processing + /// @param csvLine The CSV line in format "address,amount" + /// @return recipient The parsed address + /// @return amount The parsed amount + function parseLine(string memory csvLine) internal pure returns (address recipient, uint256 amount) { + // This is a simplified implementation + // In practice, you'd process CSV off-chain and pass arrays to the scripts + // For demonstration purposes only + + // Split by comma (simplified - real implementation would be more robust) + bytes memory lineBytes = bytes(csvLine); + uint256 commaIndex = 0; + + // Find comma + for (uint256 i = 0; i < lineBytes.length; i++) { + if (lineBytes[i] == 0x2C) { // comma + commaIndex = i; + break; + } + } + + require(commaIndex > 0, "Invalid CSV format"); + + // Extract address (first part) + bytes memory addressBytes = new bytes(commaIndex); + for (uint256 i = 0; i < commaIndex; i++) { + addressBytes[i] = lineBytes[i]; + } + + // Extract amount (second part) + bytes memory amountBytes = new bytes(lineBytes.length - commaIndex - 1); + for (uint256 i = 0; i < amountBytes.length; i++) { + amountBytes[i] = lineBytes[commaIndex + 1 + i]; + } + + // Convert to address and uint256 + // Note: This is simplified - use proper parsing in production + recipient = address(0); // Placeholder + amount = 0; // Placeholder + } +} + +/// @title CSV Example Generator +/// @notice Script to generate example CSV files for testing +contract GenerateCSVExample is Script { + + function run() external pure { + console.log("Example CSV format for TGC token distribution:"); + console.log("address,amount"); + console.log("0x1234567890123456789012345678901234567890,1000"); + console.log("0x2345678901234567890123456789012345678901,2000"); + console.log("0x3456789012345678901234567890123456789012,500"); + console.log("0x4567890123456789012345678901234567890123,750"); + console.log("0x5678901234567890123456789012345678901234,1250"); + console.log(""); + console.log("Instructions:"); + console.log("1. Save the above as recipients.csv"); + console.log("2. Replace example addresses with real addresses"); + console.log("3. Adjust amounts as needed (in whole tokens)"); + console.log("4. Process the CSV off-chain to create arrays for the scripts"); + console.log("5. Use the arrays in DeployTGC.s.sol or MintTGC.s.sol"); + } +} \ No newline at end of file diff --git a/the-guild-smart-contracts/src/TheGuildContributionToken.sol b/the-guild-smart-contracts/src/TheGuildContributionToken.sol new file mode 100644 index 0000000..7c4f335 --- /dev/null +++ b/the-guild-smart-contracts/src/TheGuildContributionToken.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title TheGuildContributionToken (TGC) +/// @notice ERC20 token for rewarding contributions to The Guild +/// @dev Standard ERC20 with owner-controlled minting capabilities +contract TheGuildContributionToken is ERC20, Ownable { + /// @notice Maximum total supply to prevent infinite inflation + uint256 public constant MAX_SUPPLY = 1_000_000_000 * 10**18; // 1 billion tokens + + /// @notice Event emitted when tokens are minted to multiple recipients + event BatchMint(address indexed owner, uint256 totalAmount, uint256 recipientCount); + + /// @notice Event emitted when tokens are minted to a single recipient + event Mint(address indexed to, uint256 amount); + + /// @notice Event emitted when tokens are minted with a reason (e.g., GitHub ticket reference) + event ContributionTokenMinted(address indexed recipient, uint256 amount, bytes32 indexed reason); + + /// @dev Constructor sets the token name, symbol, and initial owner + /// @param initialOwner The address that will own the contract + constructor(address initialOwner) + ERC20("TheGuild Contribution Token", "TGC") + Ownable(initialOwner) + { + // Contract starts with zero supply - all tokens must be explicitly minted + } + + /// @notice Mint tokens to a single recipient + /// @dev Only the owner can mint tokens + /// @param to The address to receive the tokens + /// @param amount The amount of tokens to mint (in wei, including decimals) + function mint(address to, uint256 amount) external onlyOwner { + require(to != address(0), "TGC: cannot mint to zero address"); + require(amount > 0, "TGC: amount must be greater than zero"); + require(totalSupply() + amount <= MAX_SUPPLY, "TGC: would exceed max supply"); + + _mint(to, amount); + emit Mint(to, amount); + } + + /// @notice Mint tokens to a recipient with a reason (e.g., GitHub ticket reference) + /// @dev Only the owner can mint tokens. Use this for tracking specific contributions. + /// @param to The address to receive the tokens + /// @param amount The amount of tokens to mint (in wei, including decimals) + /// @param reason The reason for minting (e.g., GitHub issue hash, ticket ID) + function mintWithReason(address to, uint256 amount, bytes32 reason) external onlyOwner { + require(to != address(0), "TGC: cannot mint to zero address"); + require(amount > 0, "TGC: amount must be greater than zero"); + require(reason != bytes32(0), "TGC: reason cannot be empty"); + require(totalSupply() + amount <= MAX_SUPPLY, "TGC: would exceed max supply"); + + _mint(to, amount); + emit ContributionTokenMinted(to, amount, reason); + } + + /// @notice Mint tokens to multiple recipients + /// @dev Only the owner can mint tokens. Gas-efficient batch minting. + /// @param recipients Array of addresses to receive tokens + /// @param amounts Array of amounts to mint to each recipient + function batchMint(address[] calldata recipients, uint256[] calldata amounts) external onlyOwner { + require(recipients.length == amounts.length, "TGC: arrays length mismatch"); + require(recipients.length > 0, "TGC: empty arrays"); + + uint256 totalAmount = 0; + + for (uint256 i = 0; i < recipients.length; i++) { + require(recipients[i] != address(0), "TGC: cannot mint to zero address"); + require(amounts[i] > 0, "TGC: amount must be greater than zero"); + + totalAmount += amounts[i]; + _mint(recipients[i], amounts[i]); + } + + require(totalSupply() <= MAX_SUPPLY, "TGC: would exceed max supply"); + + emit BatchMint(msg.sender, totalAmount, recipients.length); + } + + /// @notice Mint tokens to multiple recipients with reasons + /// @dev Only the owner can mint tokens. Gas-efficient batch minting with contribution tracking. + /// @param recipients Array of addresses to receive tokens + /// @param amounts Array of amounts to mint to each recipient + /// @param reasons Array of reasons for each mint (e.g., GitHub issue hashes) + function batchMintWithReasons( + address[] calldata recipients, + uint256[] calldata amounts, + bytes32[] calldata reasons + ) external onlyOwner { + require(recipients.length == amounts.length, "TGC: recipients/amounts length mismatch"); + require(recipients.length == reasons.length, "TGC: recipients/reasons length mismatch"); + require(recipients.length > 0, "TGC: empty arrays"); + + uint256 totalAmount = 0; + + for (uint256 i = 0; i < recipients.length; i++) { + require(recipients[i] != address(0), "TGC: cannot mint to zero address"); + require(amounts[i] > 0, "TGC: amount must be greater than zero"); + require(reasons[i] != bytes32(0), "TGC: reason cannot be empty"); + + totalAmount += amounts[i]; + _mint(recipients[i], amounts[i]); + emit ContributionTokenMinted(recipients[i], amounts[i], reasons[i]); + } + + require(totalSupply() <= MAX_SUPPLY, "TGC: would exceed max supply"); + + emit BatchMint(msg.sender, totalAmount, recipients.length); + } + + /// @notice Get the maximum supply of tokens + /// @return The maximum total supply + function maxSupply() external pure returns (uint256) { + return MAX_SUPPLY; + } + + /// @notice Get the remaining mintable supply + /// @return The amount of tokens that can still be minted + function remainingSupply() external view returns (uint256) { + return MAX_SUPPLY - totalSupply(); + } +} \ No newline at end of file diff --git a/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol b/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol new file mode 100644 index 0000000..90e2396 --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +<<<<<<< HEAD +import {Test, console} from "forge-std/Test.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +/// @title TheGuildContributionToken Tests +/// @notice Comprehensive test suite for TGC token contract +contract TheGuildContributionTokenTest is Test { + TheGuildContributionToken public token; + address public owner; + address public user1; + address public user2; + address public user3; + + /// @notice Set up test environment + function setUp() public { + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + user3 = makeAddr("user3"); + + vm.prank(owner); + token = new TheGuildContributionToken(owner); + } + + /// @notice Test contract deployment and initial state + function test_Deployment() public view { + assertEq(token.name(), "TheGuild Contribution Token"); + assertEq(token.symbol(), "TGC"); + assertEq(token.decimals(), 18); + assertEq(token.totalSupply(), 0); + assertEq(token.owner(), owner); + assertEq(token.maxSupply(), 1_000_000_000 * 10**18); + assertEq(token.remainingSupply(), 1_000_000_000 * 10**18); + } + + /// @notice Test basic minting functionality + function test_Mint() public { + uint256 amount = 1000 * 10**18; // 1000 tokens + + vm.prank(owner); + token.mint(user1, amount); + + assertEq(token.balanceOf(user1), amount); + assertEq(token.totalSupply(), amount); + assertEq(token.remainingSupply(), token.maxSupply() - amount); + } + + /// @notice Test minting with reason functionality + function test_MintWithReason() public { + uint256 amount = 1000 * 10**18; // 1000 tokens + bytes32 reason = keccak256("GitHub-Issue-123"); + + vm.prank(owner); + token.mintWithReason(user1, amount, reason); + + assertEq(token.balanceOf(user1), amount); + assertEq(token.totalSupply(), amount); + assertEq(token.remainingSupply(), token.maxSupply() - amount); + } + + /// @notice Test minting with empty reason fails + function test_MintWithReasonEmptyReasonFails() public { + uint256 amount = 1000 * 10**18; + bytes32 emptyReason = bytes32(0); + + vm.prank(owner); + vm.expectRevert("TGC: reason cannot be empty"); + token.mintWithReason(user1, amount, emptyReason); + } + + /// @notice Test that only owner can mint + function test_OnlyOwnerCanMint() public { + uint256 amount = 1000 * 10**18; + + vm.prank(user1); + vm.expectRevert(); + token.mint(user2, amount); + } + + /// @notice Test minting to zero address fails + function test_MintToZeroAddressFails() public { + uint256 amount = 1000 * 10**18; + + vm.prank(owner); + vm.expectRevert("TGC: cannot mint to zero address"); + token.mint(address(0), amount); + } + + /// @notice Test minting zero amount fails + function test_MintZeroAmountFails() public { + vm.prank(owner); + vm.expectRevert("TGC: amount must be greater than zero"); + token.mint(user1, 0); + } + + /// @notice Test batch minting functionality + function test_BatchMint() public { + address[] memory recipients = new address[](3); + uint256[] memory amounts = new uint256[](3); + + recipients[0] = user1; + recipients[1] = user2; + recipients[2] = user3; + + amounts[0] = 1000 * 10**18; + amounts[1] = 2000 * 10**18; + amounts[2] = 500 * 10**18; + + vm.prank(owner); + token.batchMint(recipients, amounts); + + assertEq(token.balanceOf(user1), amounts[0]); + assertEq(token.balanceOf(user2), amounts[1]); + assertEq(token.balanceOf(user3), amounts[2]); + assertEq(token.totalSupply(), amounts[0] + amounts[1] + amounts[2]); + } + + /// @notice Test batch minting with reasons functionality + function test_BatchMintWithReasons() public { + address[] memory recipients = new address[](3); + uint256[] memory amounts = new uint256[](3); + bytes32[] memory reasons = new bytes32[](3); + + recipients[0] = user1; + recipients[1] = user2; + recipients[2] = user3; + + amounts[0] = 1000 * 10**18; + amounts[1] = 2000 * 10**18; + amounts[2] = 500 * 10**18; + + reasons[0] = keccak256("GitHub-Issue-123"); + reasons[1] = keccak256("GitHub-Issue-456"); + reasons[2] = keccak256("GitHub-Issue-789"); + + vm.prank(owner); + token.batchMintWithReasons(recipients, amounts, reasons); + + assertEq(token.balanceOf(user1), amounts[0]); + assertEq(token.balanceOf(user2), amounts[1]); + assertEq(token.balanceOf(user3), amounts[2]); + assertEq(token.totalSupply(), amounts[0] + amounts[1] + amounts[2]); + } + + /// @notice Test batch mint with reasons array length mismatch fails + function test_BatchMintWithReasonsMismatchedArraysFails() public { + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + bytes32[] memory reasons = new bytes32[](3); // Wrong length + + recipients[0] = user1; + recipients[1] = user2; + + amounts[0] = 1000 * 10**18; + amounts[1] = 2000 * 10**18; + + reasons[0] = keccak256("GitHub-Issue-123"); + reasons[1] = keccak256("GitHub-Issue-456"); + reasons[2] = keccak256("GitHub-Issue-789"); + + vm.prank(owner); + vm.expectRevert("TGC: recipients/reasons length mismatch"); + token.batchMintWithReasons(recipients, amounts, reasons); + } + + /// @notice Test batch mint with empty reason fails + function test_BatchMintWithReasonsEmptyReasonFails() public { + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + bytes32[] memory reasons = new bytes32[](2); + + recipients[0] = user1; + recipients[1] = user2; + + amounts[0] = 1000 * 10**18; + amounts[1] = 2000 * 10**18; + + reasons[0] = keccak256("GitHub-Issue-123"); + reasons[1] = bytes32(0); // Empty reason + + vm.prank(owner); + vm.expectRevert("TGC: reason cannot be empty"); + token.batchMintWithReasons(recipients, amounts, reasons); + } + + /// @notice Test batch mint with mismatched arrays fails + function test_BatchMintMismatchedArraysFails() public { + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](3); + + recipients[0] = user1; + recipients[1] = user2; + + amounts[0] = 1000 * 10**18; + amounts[1] = 2000 * 10**18; + amounts[2] = 500 * 10**18; + + vm.prank(owner); + vm.expectRevert("TGC: arrays length mismatch"); + token.batchMint(recipients, amounts); + } + + /// @notice Test batch mint with empty arrays fails + function test_BatchMintEmptyArraysFails() public { + address[] memory recipients = new address[](0); + uint256[] memory amounts = new uint256[](0); + + vm.prank(owner); + vm.expectRevert("TGC: empty arrays"); + token.batchMint(recipients, amounts); + } + + /// @notice Test batch mint with zero address fails + function test_BatchMintZeroAddressFails() public { + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + + recipients[0] = user1; + recipients[1] = address(0); + + amounts[0] = 1000 * 10**18; + amounts[1] = 2000 * 10**18; + + vm.prank(owner); + vm.expectRevert("TGC: cannot mint to zero address"); + token.batchMint(recipients, amounts); + } + + /// @notice Test batch mint with zero amount fails + function test_BatchMintZeroAmountFails() public { + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + + recipients[0] = user1; + recipients[1] = user2; + + amounts[0] = 1000 * 10**18; + amounts[1] = 0; + + vm.prank(owner); + vm.expectRevert("TGC: amount must be greater than zero"); + token.batchMint(recipients, amounts); + } + + /// @notice Test max supply enforcement + function test_MaxSupplyEnforcement() public { + uint256 maxSupply = token.maxSupply(); + + vm.prank(owner); + vm.expectRevert("TGC: would exceed max supply"); + token.mint(user1, maxSupply + 1); + } + + /// @notice Test max supply enforcement in batch mint + function test_BatchMintMaxSupplyEnforcement() public { + uint256 maxSupply = token.maxSupply(); + + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + + recipients[0] = user1; + recipients[1] = user2; + + amounts[0] = maxSupply / 2; + amounts[1] = (maxSupply / 2) + 1; + + vm.prank(owner); + vm.expectRevert("TGC: would exceed max supply"); + token.batchMint(recipients, amounts); + } + + /// @notice Test minting up to max supply works + function test_MintToMaxSupply() public { + uint256 maxSupply = token.maxSupply(); + + vm.prank(owner); + token.mint(user1, maxSupply); + + assertEq(token.balanceOf(user1), maxSupply); + assertEq(token.totalSupply(), maxSupply); + assertEq(token.remainingSupply(), 0); + } + + /// @notice Test standard ERC20 transfer functionality + function test_Transfer() public { + uint256 amount = 1000 * 10**18; + + // Mint tokens to user1 + vm.prank(owner); + token.mint(user1, amount); + + // Transfer from user1 to user2 + uint256 transferAmount = 300 * 10**18; + vm.prank(user1); + bool success = token.transfer(user2, transferAmount); + + assertTrue(success); + assertEq(token.balanceOf(user1), amount - transferAmount); + assertEq(token.balanceOf(user2), transferAmount); + } + + /// @notice Test allowance and transferFrom functionality + function test_Allowance() public { + uint256 amount = 1000 * 10**18; + uint256 allowanceAmount = 300 * 10**18; + + // Mint tokens to user1 + vm.prank(owner); + token.mint(user1, amount); + + // User1 approves user2 to spend tokens + vm.prank(user1); + token.approve(user2, allowanceAmount); + + assertEq(token.allowance(user1, user2), allowanceAmount); + + // User2 transfers from user1 to user3 + uint256 transferAmount = 200 * 10**18; + vm.prank(user2); + bool success = token.transferFrom(user1, user3, transferAmount); + + assertTrue(success); + assertEq(token.balanceOf(user1), amount - transferAmount); + assertEq(token.balanceOf(user3), transferAmount); + assertEq(token.allowance(user1, user2), allowanceAmount - transferAmount); + } + + /// @notice Test ownership transfer + function test_OwnershipTransfer() public { + vm.prank(owner); + token.transferOwnership(user1); + + assertEq(token.owner(), user1); + + // Old owner can't mint anymore + vm.prank(owner); + vm.expectRevert(); + token.mint(user2, 1000 * 10**18); + + // New owner can mint + vm.prank(user1); + token.mint(user2, 1000 * 10**18); + assertEq(token.balanceOf(user2), 1000 * 10**18); + } + + /// @notice Test renouncing ownership + function test_RenounceOwnership() public { + vm.prank(owner); + token.renounceOwnership(); + + assertEq(token.owner(), address(0)); + + // No one can mint after renouncing + vm.prank(owner); + vm.expectRevert(); + token.mint(user1, 1000 * 10**18); + } + + /// @notice Test events are emitted correctly + function test_Events() public { + uint256 amount = 1000 * 10**18; + bytes32 reason = keccak256("GitHub-Issue-123"); + + // Test Mint event + vm.expectEmit(true, false, false, true); + emit TheGuildContributionToken.Mint(user1, amount); + + vm.prank(owner); + token.mint(user1, amount); + + // Test ContributionTokenMinted event + vm.expectEmit(true, false, true, true); + emit TheGuildContributionToken.ContributionTokenMinted(user2, amount, reason); + + vm.prank(owner); + token.mintWithReason(user2, amount, reason); + + // Test BatchMint event + address[] memory recipients = new address[](2); + uint256[] memory amounts = new uint256[](2); + + recipients[0] = user1; + recipients[1] = user3; + amounts[0] = 500 * 10**18; + amounts[1] = 750 * 10**18; + + uint256 totalAmount = amounts[0] + amounts[1]; + + vm.expectEmit(true, false, false, true); + emit TheGuildContributionToken.BatchMint(owner, totalAmount, 2); + + vm.prank(owner); + token.batchMint(recipients, amounts); + } + + /// @notice Fuzz test minting various amounts + function testFuzz_Mint(uint256 amount) public { + vm.assume(amount > 0 && amount <= token.maxSupply()); + + vm.prank(owner); + token.mint(user1, amount); + + assertEq(token.balanceOf(user1), amount); + assertEq(token.totalSupply(), amount); + } + + /// @notice Test gas consumption for batch operations + function test_GasConsumption() public { + uint256 batchSize = 10; + address[] memory recipients = new address[](batchSize); + uint256[] memory amounts = new uint256[](batchSize); + + for (uint256 i = 0; i < batchSize; i++) { + recipients[i] = makeAddr(string(abi.encodePacked("user", i))); + amounts[i] = (i + 1) * 100 * 10**18; + } + + uint256 gasBefore = gasleft(); + + vm.prank(owner); + token.batchMint(recipients, amounts); + + uint256 gasUsed = gasBefore - gasleft(); + console.log("Gas used for batch mint of", batchSize, "recipients:", gasUsed); + + // Ensure gas usage is reasonable (adjust threshold as needed) + assertLt(gasUsed, 500000); // Less than 500k gas + } +} +======= +import "forge-std/Test.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +contract TheGuildContributionTokenTest is Test { + TheGuildContributionToken token; + address owner = address(0xABCD); + address alice = address(0x1); + + function setUp() public { + vm.prank(owner); + token = new TheGuildContributionToken(); + } + + function testMintOnlyOwner() public { + vm.prank(owner); + token.mint(alice, 1000); + assertEq(token.balanceOf(alice), 1000); + } + + function testMintWithReasonEmits() public { + bytes32 reason = bytes32("ticket_1"); + vm.prank(owner); + vm.expectEmit(true, false, false, true); + emitContributionMinted(alice, 500, reason); + token.mintWithReason(alice, 500, reason); + assertEq(token.balanceOf(alice), 500); + } + + function testBatchMintAndDistributionId() public { + address bob = address(0x2); + address carol = address(0x3); + + address[] memory recipients = new address[](2); + recipients[0] = alice; + recipients[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + + bytes32[] memory reasons = new bytes32[](2); + reasons[0] = bytes32("t1"); + reasons[1] = bytes32("t2"); + + bytes32 distributionId = keccak256(abi.encodePacked(block.timestamp, address(this))); + + vm.prank(owner); + token.batchMint(distributionId, recipients, amounts, reasons); + + assertEq(token.balanceOf(alice), 100); + assertEq(token.balanceOf(bob), 200); + assertTrue(token.isDistributionExecuted(distributionId)); + + // Replaying should revert + vm.prank(owner); + vm.expectRevert("TGC: distribution already executed"); + token.batchMint(distributionId, recipients, amounts, reasons); + } + + // helper to satisfy expectEmit signature + event ContributionTokenMinted(address indexed recipient, uint256 amount, bytes32 reason); + + function emitContributionMinted(address recipient, uint256 amount, bytes32 reason) internal { + emit ContributionTokenMinted(recipient, amount, reason); + } +} +>>>>>>> 9dff903106d20ed0497926497613df5111737be9 diff --git a/the-guild-smart-contracts/test/TheGuildContributionTokenBatch.t.sol b/the-guild-smart-contracts/test/TheGuildContributionTokenBatch.t.sol new file mode 100644 index 0000000..6b045d0 --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildContributionTokenBatch.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "forge-std/Test.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +contract TheGuildContributionTokenBatchTest is Test { + TheGuildContributionToken token; + address owner = address(0xABCD); + address alice = address(0x1); + address bob = address(0x2); + + function setUp() public { + vm.prank(owner); + token = new TheGuildContributionToken(); + } + + function testBatchMintSuccessAndDistributionGuard() public { + address[] memory recipients = new address[](2); + recipients[0] = alice; + recipients[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + + bytes32[] memory reasons = new bytes32[](2); + reasons[0] = bytes32("ticket1"); + reasons[1] = bytes32("ticket2"); + + bytes32 distributionId = keccak256(abi.encodePacked("dist-1")); + + vm.prank(owner); + token.batchMint(distributionId, recipients, amounts, reasons); + + assertEq(token.balanceOf(alice), 100); + assertEq(token.balanceOf(bob), 200); + assertTrue(token.isDistributionExecuted(distributionId)); + + // Re-running should revert + vm.prank(owner); + vm.expectRevert(bytes("TGC: distribution already executed")); + token.batchMint(distributionId, recipients, amounts, reasons); + } + + function testBatchMintRevertsOnLengthMismatch() public { + address[] memory recipients = new address[](1); + recipients[0] = alice; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 100; + amounts[1] = 200; + + bytes32[] memory reasons = new bytes32[](1); + reasons[0] = bytes32("ticket1"); + + bytes32 distributionId = keccak256(abi.encodePacked("dist-2")); + + vm.prank(owner); + vm.expectRevert(bytes("TGC: arrays length mismatch")); + token.batchMint(distributionId, recipients, amounts, reasons); + } +}