From 369182e8df25909bdf17e66c29b7f8f88aa7a12f Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Mon, 15 Dec 2025 18:29:38 +0100 Subject: [PATCH 1/8] add contribution token --- .../contribution-tokens-latest.json | 21 ++ .../run_batch_contribution_tokens.sh | 80 +++++++ .../script/DeployTGC.s.sol | 36 ++++ .../script/MintTGCFromJson.s.sol | 203 ++++++++++++++++++ .../script/UpgradeTGCImplementation.s.sol | 38 ++++ .../src/TheGuildContributionToken.sol | 98 +++++++++ 6 files changed, 476 insertions(+) create mode 100644 the-guild-smart-contracts/contribution-tokens-latest.json create mode 100644 the-guild-smart-contracts/run_batch_contribution_tokens.sh create mode 100644 the-guild-smart-contracts/script/DeployTGC.s.sol create mode 100644 the-guild-smart-contracts/script/MintTGCFromJson.s.sol create mode 100644 the-guild-smart-contracts/script/UpgradeTGCImplementation.s.sol create mode 100644 the-guild-smart-contracts/src/TheGuildContributionToken.sol diff --git a/the-guild-smart-contracts/contribution-tokens-latest.json b/the-guild-smart-contracts/contribution-tokens-latest.json new file mode 100644 index 0000000..bf01196 --- /dev/null +++ b/the-guild-smart-contracts/contribution-tokens-latest.json @@ -0,0 +1,21 @@ +{ + "mints": [ + { + "recipient": "0x0000000000000000000000000000000000000001", + "amount": 1000000000000000000, + "reason": "0x536f6c766564206120c3b17265616c20636f6d6d756e697479206973737565" + }, + { + "recipient": "0x0000000000000000000000000000000000000002", + "amount": 2500000000000000000, + "reason": "0x436f6e747269627574656420746f206f70656e2d736f7572636520646f6373" + }, + { + "recipient": "0x0000000000000000000000000000000000000003", + "amount": 5000000000000000000, + "reason": "0x4d656e746f72656420616e642068656c706564206d61696e7461696e20646973636f7264" + } + ] +} + + diff --git a/the-guild-smart-contracts/run_batch_contribution_tokens.sh b/the-guild-smart-contracts/run_batch_contribution_tokens.sh new file mode 100644 index 0000000..8d41688 --- /dev/null +++ b/the-guild-smart-contracts/run_batch_contribution_tokens.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Helper script for running TheGuild batch contribution token mint script +# Usage: ./run_batch_contribution_tokens.sh [json_file] [dry_run] +# json_file: Path to JSON file with contribution token mints (default: contribution-tokens-latest.json) +# dry_run: Set to 'true' for dry run (default: false) + +set -e + +# Source .env file if it exists +if [ -f .env ]; then + source .env +fi + +# Parse arguments - JSON file is optional, defaults to contribution-tokens-latest.json +if [ $# -eq 0 ]; then + # No arguments: use default JSON file + JSON_FILE="contribution-tokens-latest.json" + DRY_RUN="false" +elif [ $# -eq 1 ]; then + # One argument: could be JSON file or dry_run flag + if [ "$1" = "true" ] || [ "$1" = "false" ]; then + # It's a dry_run flag + JSON_FILE="contribution-tokens-latest.json" + DRY_RUN="$1" + else + # It's a JSON file path + JSON_FILE="$1" + DRY_RUN="false" + fi +else + # Two arguments: JSON file and dry_run flag + JSON_FILE="$1" + DRY_RUN="$2" +fi + +if [ ! -f "$JSON_FILE" ]; then + echo "Error: JSON file '$JSON_FILE' not found" + exit 1 +fi + +# Set JSON file path +export JSON_PATH="$JSON_FILE" + +# Set dry run mode +if [ "$DRY_RUN" = "true" ]; then + export DRY_RUN=true + echo "Running in DRY RUN mode..." +else + unset DRY_RUN + echo "Running in PRODUCTION mode..." +fi + +# Check for required environment variables +if [ -z "$PRIVATE_KEY" ]; then + echo "Error: PRIVATE_KEY environment variable not set" + exit 1 +fi + +if [ -z "$RPC_URL" ]; then + echo "Error: RPC_URL environment variable not set" + exit 1 +fi + +if [ -z "$TGC_PROXY_ADDRESS" ]; then + echo "Error: TGC_PROXY_ADDRESS environment variable not set" + exit 1 +fi + +# Run the script +if [ "$DRY_RUN" = "true" ]; then + forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \ + --rpc-url "$RPC_URL" +else + forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \ + --rpc-url "$RPC_URL" \ + --broadcast +fi + + 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..4b99415 --- /dev/null +++ b/the-guild-smart-contracts/script/DeployTGC.s.sol @@ -0,0 +1,36 @@ +// 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"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/// @notice Deploys the upgradable TGC token (implementation + ERC1967/UUPS proxy). +contract DeployTGC is Script { + function run() public { + uint256 pk = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(pk); + + console.log( + "Deploying TheGuildContributionToken (TGC) implementation..." + ); + TheGuildContributionToken implementation = new TheGuildContributionToken(); + + console.log("Deploying ERC1967 proxy for TGC..."); + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + abi.encodeWithSelector( + TheGuildContributionToken.initialize.selector + ) + ); + + console.log("=== TGC Deployment Summary ==="); + console.log("Proxy (TGC) address:", address(proxy)); + console.log("Implementation address:", address(implementation)); + + vm.stopBroadcast(); + } +} + + diff --git a/the-guild-smart-contracts/script/MintTGCFromJson.s.sol b/the-guild-smart-contracts/script/MintTGCFromJson.s.sol new file mode 100644 index 0000000..e9e306f --- /dev/null +++ b/the-guild-smart-contracts/script/MintTGCFromJson.s.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Script, stdJson} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +/// @notice Batch-mints TGC to recipients defined in a JSON file using mintWithReason. +/// @dev JSON format: +/// { +/// "mints": [ +/// { +/// "recipient": "0x...", +/// "amount": "1000000000000000000", +/// "reason": "0x..." // bytes, e.g. abi.encodePacked(...) +/// } +/// ] +/// } +contract MintTGCFromJson is Script { + using stdJson for string; + + uint256 constant MAX_ENTRIES = 1000; + + struct MintData { + address recipient; + uint256 amount; + bytes reason; + } + + function run() public { + bool isDryRun = vm.envOr("DRY_RUN", false); + + console.log("=== TheGuild TGC Mint-From-JSON Script ==="); + console.log("Dry run mode:", isDryRun ? "ENABLED" : "DISABLED"); + + address proxyAddress = vm.envAddress("TGC_PROXY_ADDRESS"); + require(proxyAddress != address(0), "TGC_PROXY_ADDRESS not set"); + TheGuildContributionToken tgc = TheGuildContributionToken( + proxyAddress + ); + + // Read JSON file + string memory jsonPath = vm.envOr( + "JSON_PATH", + string("tgc-mints.json") + ); + console.log("Reading JSON from:", jsonPath); + + string memory jsonData = vm.readFile(jsonPath); + MintData[] memory mints = parseAndValidateJson(jsonData); + + console.log( + string( + abi.encodePacked( + "Parsed ", + vm.toString(mints.length), + " mint entries from JSON" + ) + ) + ); + + if (isDryRun) { + for (uint256 i = 0; i < mints.length; i++) { + console.log( + string( + abi.encodePacked( + "Mint ", + vm.toString(i + 1), + ": to=", + vm.toString(mints[i].recipient), + ", amount=", + vm.toString(mints[i].amount) + ) + ) + ); + } + console.log("Dry run completed successfully!"); + return; + } + + executeMints(tgc, mints); + } + + function parseAndValidateJson( + string memory jsonData + ) internal view returns (MintData[] memory) { + console.log("Parsing JSON mints array..."); + + MintData[] memory tempMints = new MintData[](MAX_ENTRIES); + uint256 count = 0; + + for (uint256 i = 0; i < MAX_ENTRIES; i++) { + string memory basePath = string( + abi.encodePacked(".mints[", vm.toString(i), "]") + ); + + // Detect existence via recipient + bytes memory recipientRaw = vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".recipient")) + ); + if (recipientRaw.length == 0) break; + + address recipient = abi.decode(recipientRaw, (address)); + + uint256 amount = abi.decode( + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".amount")) + ), + (uint256) + ); + + bytes memory reason = abi.decode( + vm.parseJson( + jsonData, + string(abi.encodePacked(basePath, ".reason")) + ), + (bytes) + ); + + tempMints[count] = MintData({ + recipient: recipient, + amount: amount, + reason: reason + }); + + count++; + } + + MintData[] memory mints = new MintData[](count); + for (uint256 i = 0; i < count; i++) { + mints[i] = tempMints[i]; + } + + console.log("Found", mints.length, " mint entries in JSON"); + + require(mints.length > 0, "JSON must contain at least 1 mint entry"); + require( + mints.length <= MAX_ENTRIES, + "Too many mint entries, max 1000 allowed" + ); + + for (uint256 i = 0; i < mints.length; i++) { + require( + mints[i].recipient != address(0), + string( + abi.encodePacked( + "Mint ", + vm.toString(i), + ": invalid recipient address" + ) + ) + ); + require( + mints[i].amount > 0, + string( + abi.encodePacked( + "Mint ", + vm.toString(i), + ": amount must be > 0" + ) + ) + ); + } + + return mints; + } + + function executeMints( + TheGuildContributionToken tgc, + MintData[] memory mints + ) internal { + uint256 pk = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(pk); + + console.log("Starting TGC minting..."); + + for (uint256 i = 0; i < mints.length; i++) { + MintData memory m = mints[i]; + tgc.mintWithReason(m.recipient, m.amount, m.reason); + console.log( + string( + abi.encodePacked( + "Minted ", + vm.toString(m.amount), + " TGC to ", + vm.toString(m.recipient) + ) + ) + ); + } + + vm.stopBroadcast(); + + console.log("=== TGC Mint Summary ==="); + console.log("Proxy (TGC) address:", address(tgc)); + console.log("Total mints processed:", mints.length); + console.log("Execution completed successfully!"); + } +} + + diff --git a/the-guild-smart-contracts/script/UpgradeTGCImplementation.s.sol b/the-guild-smart-contracts/script/UpgradeTGCImplementation.s.sol new file mode 100644 index 0000000..39776ff --- /dev/null +++ b/the-guild-smart-contracts/script/UpgradeTGCImplementation.s.sol @@ -0,0 +1,38 @@ +// 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"; + +/// @notice Deploys a new TGC implementation and upgrades an existing UUPS proxy to it. +/// @dev Requires PRIVATE_KEY to be the current owner of the proxy. +contract UpgradeTGCImplementation is Script { + function run() public { + uint256 pk = vm.envUint("PRIVATE_KEY"); + address proxyAddress = vm.envAddress("TGC_PROXY_ADDRESS"); + + require(proxyAddress != address(0), "TGC_PROXY_ADDRESS not set"); + + vm.startBroadcast(pk); + + console.log( + "Deploying new TheGuildContributionToken implementation..." + ); + TheGuildContributionToken newImplementation = new TheGuildContributionToken(); + console.log("New implementation address:", address(newImplementation)); + + console.log("Upgrading proxy to new implementation..."); + // OZ UUPS v5 only exposes upgradeToAndCall; use it with empty data. + TheGuildContributionToken(proxyAddress).upgradeToAndCall( + address(newImplementation), + bytes("") + ); + + console.log("=== TGC Upgrade Summary ==="); + console.log("Proxy (TGC) address:", proxyAddress); + console.log("New implementation address:", address(newImplementation)); + + vm.stopBroadcast(); + } +} diff --git a/the-guild-smart-contracts/src/TheGuildContributionToken.sol b/the-guild-smart-contracts/src/TheGuildContributionToken.sol new file mode 100644 index 0000000..94c195a --- /dev/null +++ b/the-guild-smart-contracts/src/TheGuildContributionToken.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/// @title TheGuildContributionToken (TGC) +/// @notice Upgradable ERC20 token used to reward contributions within The Guild ecosystem. +/// Deployer is the initial owner and sole minter by default. +contract TheGuildContributionToken is + ERC20Upgradeable, + OwnableUpgradeable, + UUPSUpgradeable +{ + /// @notice Emitted when tokens are minted with an associated reason. + /// @param to The recipient of the minted tokens. + /// @param amount The amount of tokens minted. + /// @param reason Arbitrary bytes describing why the user received contribution tokens. + event MintedWithReason(address indexed to, uint256 amount, bytes reason); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + // Prevent initializing the implementation contract directly + _disableInitializers(); + } + + /// @notice Initialize the upgradable token (called via proxy). + /// @dev This replaces the constructor for upgradeable contracts. + function initialize() public initializer { + __ERC20_init("The Guild Contribution Token", "TGC"); + __Ownable_init(msg.sender); + __UUPSUpgradeable_init(); + } + + /// @notice Mint tokens to a recipient. + /// @dev Only the owner (deployer/admin) can mint. + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + /// @notice Mint tokens to a recipient with an associated reason. + /// @param to Recipient address. + /// @param amount Token amount to mint. + /// @param reason Arbitrary bytes describing why the user received tokens. + function mintWithReason( + address to, + uint256 amount, + bytes calldata reason + ) external onlyOwner { + _mint(to, amount); + emit MintedWithReason(to, amount, reason); + } + + /// @notice Batch mint tokens to multiple recipients. + /// @param recipients Array of recipient addresses. + /// @param amounts Array of token amounts to mint. + function batchMint( + address[] calldata recipients, + uint256[] calldata amounts + ) external onlyOwner { + uint256 length = recipients.length; + require(length == amounts.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < length; i++) { + _mint(recipients[i], amounts[i]); + } + } + + /// @notice Batch mint tokens with reasons to multiple recipients. + /// @param recipients Array of recipient addresses. + /// @param amounts Array of token amounts to mint. + /// @param reasons Array of reasons (bytes) for each mint. + function batchMintWithReason( + address[] calldata recipients, + uint256[] calldata amounts, + bytes[] calldata reasons + ) external onlyOwner { + uint256 length = recipients.length; + require(length == amounts.length, "LENGTH_MISMATCH"); + require(length == reasons.length, "REASONS_LENGTH_MISMATCH"); + + for (uint256 i = 0; i < length; i++) { + _mint(recipients[i], amounts[i]); + emit MintedWithReason(recipients[i], amounts[i], reasons[i]); + } + } + + /// @notice Override decimals to keep normal ERC20 18 decimals. + function decimals() public pure override returns (uint8) { + return 18; + } + + /// @dev Authorize contract upgrades. Only the owner can upgrade. + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} +} From 0fb5d1cedb5cd1c9b2190e25b413c44510bb8d23 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Mon, 15 Dec 2025 18:52:46 +0100 Subject: [PATCH 2/8] add jupyter notebook to generate foile for contribution token distribution --- notebooks/01_github_issues.ipynb | 385 +++++++++++++++++----- notebooks/contribution-tokens-latest.json | 54 +++ scripts/test_projects.sh | 132 ++++++++ 3 files changed, 490 insertions(+), 81 deletions(-) create mode 100644 notebooks/contribution-tokens-latest.json create mode 100755 scripts/test_projects.sh diff --git a/notebooks/01_github_issues.ipynb b/notebooks/01_github_issues.ipynb index e6db440..d90c1e6 100644 --- a/notebooks/01_github_issues.ipynb +++ b/notebooks/01_github_issues.ipynb @@ -24,14 +24,14 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Fetched 47 issues from TheSoftwareDevGuild/TheGuildGenesis\n" + "Fetched 49 issues from TheSoftwareDevGuild/TheGuildGenesis\n" ] }, { @@ -69,18 +69,54 @@ " \n", " \n", " 0\n", + " 154\n", + " Description field of badges should be bytes no...\n", + " open\n", + " 2025-12-14T10:45:06Z\n", + " 2025-12-15T16:57:10Z\n", + " joelamouche\n", + " V-Vaal\n", + " good first issue,solidity,40pts,foundry,web3\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 1\n", + " 153\n", + " Allow batch attestation creation in the front end\n", + " open\n", + " 2025-12-14T10:39:31Z\n", + " 2025-12-14T10:39:31Z\n", + " joelamouche\n", + " \n", + " good first issue,front end,react,ux,typescript\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 2\n", + " 150\n", + " Implement JWT - Front end flow\n", + " open\n", + " 2025-12-08T10:36:57Z\n", + " 2025-12-08T12:40:22Z\n", + " joelamouche\n", + " tusharshah21\n", + " front end,react,typescript\n", + " https://github.com/TheSoftwareDevGuild/TheGuil...\n", + " \n", + " \n", + " 3\n", " 146\n", " Figure out how to admin delete profile\n", " open\n", " 2025-12-03T17:26:37Z\n", - " 2025-12-03T17:26:37Z\n", + " 2025-12-07T06:52:27Z\n", " joelamouche\n", " \n", " good first issue,back-end,planning\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 1\n", + " 4\n", " 145\n", " I think we should allow duplicate attestations\n", " open\n", @@ -92,7 +128,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 2\n", + " 5\n", " 144\n", " Implement Upgradable pattern for TheGuildAttes...\n", " open\n", @@ -104,7 +140,7 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 3\n", + " 6\n", " 143\n", " TheGuildAttestationResolver should have admin ...\n", " open\n", @@ -116,31 +152,31 @@ " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 4\n", + " 7\n", " 142\n", " Add contributor leaderboard\n", " open\n", " 2025-11-24T15:04:07Z\n", - " 2025-11-24T15:04:42Z\n", + " 2025-12-13T17:03:24Z\n", " joelamouche\n", - " \n", + " adityagupta0251\n", " good first issue,front end,react,typescript,80pts\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 5\n", + " 8\n", " 141\n", " Add a new leaderboard page, with link in the s...\n", " open\n", " 2025-11-24T15:01:00Z\n", - " 2025-11-24T15:01:00Z\n", + " 2025-12-14T10:34:19Z\n", " joelamouche\n", - " \n", + " adityagupta0251\n", " good first issue,front end,react,typescript,20...\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", " \n", - " 6\n", + " 9\n", " 140\n", " Create top badge owner leaderboard\n", " open\n", @@ -151,82 +187,46 @@ " good first issue,front end,react,typescript,40pts\n", " https://github.com/TheSoftwareDevGuild/TheGuil...\n", " \n", - " \n", - " 7\n", - " 138\n", - " Public contributor leaderboard\n", - " open\n", - " 2025-11-17T13:58:58Z\n", - " 2025-11-24T15:04:19Z\n", - " joelamouche\n", - " joelamouche\n", - " enhancement,front end,planning\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", - " \n", - " 8\n", - " 137\n", - " Figure out sharing new badges on X\n", - " open\n", - " 2025-11-17T13:58:23Z\n", - " 2025-11-17T13:58:28Z\n", - " joelamouche\n", - " joelamouche\n", - " planning\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", - " \n", - " 9\n", - " 135\n", - " Add projects to the Backend\n", - " open\n", - " 2025-11-17T12:38:30Z\n", - " 2025-11-27T12:15:32Z\n", - " joelamouche\n", - " pheobeayo\n", - " good first issue,rust,back-end,160pts,db,hackt...\n", - " https://github.com/TheSoftwareDevGuild/TheGuil...\n", - " \n", " \n", "\n", "" ], "text/plain": [ " number title state \\\n", - "0 146 Figure out how to admin delete profile open \n", - "1 145 I think we should allow duplicate attestations open \n", - "2 144 Implement Upgradable pattern for TheGuildAttes... open \n", - "3 143 TheGuildAttestationResolver should have admin ... open \n", - "4 142 Add contributor leaderboard open \n", - "5 141 Add a new leaderboard page, with link in the s... open \n", - "6 140 Create top badge owner leaderboard open \n", - "7 138 Public contributor leaderboard open \n", - "8 137 Figure out sharing new badges on X open \n", - "9 135 Add projects to the Backend open \n", + "0 154 Description field of badges should be bytes no... open \n", + "1 153 Allow batch attestation creation in the front end open \n", + "2 150 Implement JWT - Front end flow open \n", + "3 146 Figure out how to admin delete profile open \n", + "4 145 I think we should allow duplicate attestations open \n", + "5 144 Implement Upgradable pattern for TheGuildAttes... open \n", + "6 143 TheGuildAttestationResolver should have admin ... open \n", + "7 142 Add contributor leaderboard open \n", + "8 141 Add a new leaderboard page, with link in the s... open \n", + "9 140 Create top badge owner leaderboard open \n", "\n", - " created_at updated_at user assignees \\\n", - "0 2025-12-03T17:26:37Z 2025-12-03T17:26:37Z joelamouche \n", - "1 2025-12-03T15:18:42Z 2025-12-03T15:18:42Z joelamouche \n", - "2 2025-12-03T10:28:34Z 2025-12-03T10:28:55Z joelamouche \n", - "3 2025-12-03T10:27:07Z 2025-12-03T10:27:51Z joelamouche \n", - "4 2025-11-24T15:04:07Z 2025-11-24T15:04:42Z joelamouche \n", - "5 2025-11-24T15:01:00Z 2025-11-24T15:01:00Z joelamouche \n", - "6 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n", - "7 2025-11-17T13:58:58Z 2025-11-24T15:04:19Z joelamouche joelamouche \n", - "8 2025-11-17T13:58:23Z 2025-11-17T13:58:28Z joelamouche joelamouche \n", - "9 2025-11-17T12:38:30Z 2025-11-27T12:15:32Z joelamouche pheobeayo \n", + " created_at updated_at user assignees \\\n", + "0 2025-12-14T10:45:06Z 2025-12-15T16:57:10Z joelamouche V-Vaal \n", + "1 2025-12-14T10:39:31Z 2025-12-14T10:39:31Z joelamouche \n", + "2 2025-12-08T10:36:57Z 2025-12-08T12:40:22Z joelamouche tusharshah21 \n", + "3 2025-12-03T17:26:37Z 2025-12-07T06:52:27Z joelamouche \n", + "4 2025-12-03T15:18:42Z 2025-12-03T15:18:42Z joelamouche \n", + "5 2025-12-03T10:28:34Z 2025-12-03T10:28:55Z joelamouche \n", + "6 2025-12-03T10:27:07Z 2025-12-03T10:27:51Z joelamouche \n", + "7 2025-11-24T15:04:07Z 2025-12-13T17:03:24Z joelamouche adityagupta0251 \n", + "8 2025-11-24T15:01:00Z 2025-12-14T10:34:19Z joelamouche adityagupta0251 \n", + "9 2025-11-24T14:59:35Z 2025-11-24T15:02:16Z joelamouche \n", "\n", " labels \\\n", - "0 good first issue,back-end,planning \n", - "1 solidity,planning \n", - "2 good first issue,solidity,foundry \n", - "3 good first issue,solidity,foundry \n", - "4 good first issue,front end,react,typescript,80pts \n", - "5 good first issue,front end,react,typescript,20... \n", - "6 good first issue,front end,react,typescript,40pts \n", - "7 enhancement,front end,planning \n", - "8 planning \n", - "9 good first issue,rust,back-end,160pts,db,hackt... \n", + "0 good first issue,solidity,40pts,foundry,web3 \n", + "1 good first issue,front end,react,ux,typescript \n", + "2 front end,react,typescript \n", + "3 good first issue,back-end,planning \n", + "4 solidity,planning \n", + "5 good first issue,solidity,foundry \n", + "6 good first issue,solidity,foundry \n", + "7 good first issue,front end,react,typescript,80pts \n", + "8 good first issue,front end,react,typescript,20... \n", + "9 good first issue,front end,react,typescript,40pts \n", "\n", " url \n", "0 https://github.com/TheSoftwareDevGuild/TheGuil... \n", @@ -241,7 +241,7 @@ "9 https://github.com/TheSoftwareDevGuild/TheGuil... " ] }, - "execution_count": 19, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -817,6 +817,229 @@ "cell_type": "markdown", "metadata": {}, "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate contribution tokens from issues" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fetch closed tickets" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fetched 44 total issues (closed)\n" + ] + } + ], + "source": [ + "import json\n", + "import re\n", + "from datetime import datetime\n", + "from collections import defaultdict\n", + "\n", + "# Fetch all issues (only closed) to get complete picture\n", + "all_issues = fetch_issues(state=\"closed\", per_page=100, max_pages=10)\n", + "print(f\"Fetched {len(all_issues)} total issues (closed)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract contribution labels" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "ISSUE-BASED GITHUB CONTRIBUTION TOKENS (13):\n", + "============================================================\n", + "- tusharshah21: 40pts -- \"Github handle errors\"\n", + "- tusharshah21: 40pts -- \"Implement SIWE for the front end\"\n", + "- pheobeayo: 80pts -- \"Feature: add a github handle to profile in Front end\"\n", + "- pheobeayo: 40pts -- \"Add doc for indexer\"\n", + "- PeterOche: 80pts -- \"Discord Bot foundation\"\n", + "- rainwaters11: 80pts -- \"Add GitHub handles to backend database (associated with profiles)\"\n", + "- joelamouche: 160pts -- \"Add a backend for the contribution points (api, db)\"\n", + "- Teddy1792: 20pts -- \"Improve landing page design with animated background\"\n", + "- pheobeayo: 40pts -- \"On the profile, there should be a section with the badges issued by that profile\"\n", + "- tusharshah21: 20pts -- \"Add copy to clipboard icons next to all ethereum addresses\"\n", + "- tusharshah21: 40pts -- \"Add description on the profile page\"\n", + "- tusharshah21: 320pts -- \"Improve SIWE logic to use dynamic, user-specific nonce - backend\"\n", + "- yash-1104github: 80pts -- \"Improve loading UX for lists\"\n", + "============================================================\n", + "\n" + ] + } + ], + "source": [ + "# Extract contribution labels that match the pattern \"xpts\"\n", + "# Extract contribution labels that match the pattern \"xpts\"\n", + "contributions = []\n", + "\n", + "for issue in all_issues:\n", + " issue_title = issue.get(\"title\", \"\")\n", + " for label in issue.get(\"labels\", []):\n", + " label_name = label.get(\"name\", \"\")\n", + " # Match labels like \"10pts\", \"5pts\", etc.\n", + " points_match = re.match(r\"^(\\d+)pts$\", label_name)\n", + " if points_match:\n", + " amount = int(points_match.group(1))\n", + " user = None\n", + " # Try to get GitHub handle from assignee or user\n", + " if issue.get(\"assignee\"):\n", + " user = issue[\"assignee\"].get(\"login\", \"\")\n", + " else:\n", + " # fallback: creator of the issue\n", + " user = issue.get(\"user\", {}).get(\"login\", \"\")\n", + " justification = issue_title\n", + " if user:\n", + " contributions.append({\n", + " \"amount\": amount,\n", + " \"githandle\": user,\n", + " \"justification\": justification,\n", + " })\n", + "\n", + "print(f\"\\n{'='*60}\")\n", + "print(f\"ISSUE-BASED GITHUB CONTRIBUTION TOKENS ({len(contributions)}):\")\n", + "print(f\"{'='*60}\")\n", + "for c in contributions:\n", + " print(f\"- {c['githandle']}: {c['amount']}pts -- \\\"{c['justification']}\\\"\")\n", + "print(f\"{'='*60}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### get github handles from API" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://theguild-backend-e2df290d177e.herokuapp.com/\n", + "Loaded 6 GitHub username -> address mappings from backend API\n", + "\n", + "Warning: 3 assignees without address mapping:\n", + " - peteroche\n", + " - rainwaters11\n", + " - yash-1104github\n", + "These will be skipped in attestations.\n", + "\n" + ] + } + ], + "source": [ + "# Optionally fetch profiles from backend API to map GitHub usernames to addresses\n", + "BACKEND_API_URL = os.getenv(\"BACKEND_API_URL\", \"http://localhost:3000\")\n", + "print(BACKEND_API_URL)\n", + "github_to_address = {}\n", + "\n", + "try:\n", + " profiles_resp = requests.get(f\"{BACKEND_API_URL}/profiles\", timeout=5)\n", + " if profiles_resp.status_code == 200:\n", + " profiles = profiles_resp.json()\n", + " for profile in profiles:\n", + " if profile.get(\"github_login\"):\n", + " github_to_address[profile[\"github_login\"].lower()] = profile[\"address\"]\n", + " print(f\"Loaded {len(github_to_address)} GitHub username -> address mappings from backend API\")\n", + " else:\n", + " print(f\"Backend API not available (status {profiles_resp.status_code}), will need manual address mapping\")\n", + "except Exception as e:\n", + " print(f\"Could not fetch profiles from backend API ({e}), will need manual address mapping\")\n", + "\n", + "# Show unmapped assignees\n", + "all_assignees = set()\n", + "for issue in all_issues:\n", + " for assignee in issue.get(\"assignees\", []):\n", + " username = assignee.get(\"login\", \"\").lower()\n", + " if username:\n", + " all_assignees.add(username)\n", + "\n", + "unmapped = [u for u in all_assignees if u not in github_to_address]\n", + "if unmapped:\n", + " print(f\"\\nWarning: {len(unmapped)} assignees without address mapping:\")\n", + " for u in sorted(unmapped):\n", + " print(f\" - {u}\")\n", + " print(\"These will be skipped in attestations.\\n\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generate contributions" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Wrote 10 mints to contribution-tokens-latest.json\n" + ] + } + ], + "source": [ + "import json\n", + "\n", + "# Prepare mints list in expected JSON format\n", + "mints = []\n", + "for c in contributions:\n", + " github_login = c['githandle'].lower()\n", + " address = github_to_address.get(github_login)\n", + " if not address:\n", + " continue # skip if we don't have an address\n", + " amount = c['amount']\n", + " justification = c['justification']\n", + " # Prepare reason as bytes and convert to hex string (\"0x...\")\n", + " # Handles possible utf-8 and encodes to hex\n", + " reason_bytes = justification.encode(\"utf-8\")\n", + " reason_hex = \"0x\" + reason_bytes.hex()\n", + " mints.append({\n", + " \"recipient\": address,\n", + " \"amount\": int(amount),\n", + " \"reason\": reason_hex,\n", + " })\n", + "\n", + "out_path = \"contribution-tokens-latest.json\"\n", + "with open(out_path, \"w\") as f:\n", + " json.dump({\"mints\": mints}, f, indent=2)\n", + "print(f\"Wrote {len(mints)} mints to {out_path}\")\n" + ] } ], "metadata": { diff --git a/notebooks/contribution-tokens-latest.json b/notebooks/contribution-tokens-latest.json new file mode 100644 index 0000000..fbbac2d --- /dev/null +++ b/notebooks/contribution-tokens-latest.json @@ -0,0 +1,54 @@ +{ + "mints": [ + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 40, + "reason": "0x4769746875622068616e646c65206572726f7273" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 40, + "reason": "0x496d706c656d656e74205349574520666f72207468652066726f6e7420656e64" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "amount": 80, + "reason": "0x466561747572653a206164642061206769746875622068616e646c6520746f2070726f66696c6520696e2046726f6e7420656e64" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "amount": 40, + "reason": "0x41646420646f6320666f7220696e6465786572" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "amount": 160, + "reason": "0x4164642061206261636b656e6420666f722074686520636f6e747269627574696f6e20706f696e747320286170692c20646229" + }, + { + "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", + "amount": 20, + "reason": "0x496d70726f7665206c616e64696e6720706167652064657369676e207769746820616e696d61746564206261636b67726f756e64" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "amount": 40, + "reason": "0x4f6e207468652070726f66696c652c2074686572652073686f756c6420626520612073656374696f6e207769746820746865206261646765732069737375656420627920746861742070726f66696c65" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 20, + "reason": "0x41646420636f707920746f20636c6970626f6172642069636f6e73206e65787420746f20616c6c20657468657265756d20616464726573736573" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 40, + "reason": "0x416464206465736372697074696f6e206f6e207468652070726f66696c652070616765" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 320, + "reason": "0x496d70726f76652053495745206c6f67696320746f207573652064796e616d69632c20757365722d7370656369666963206e6f6e6365202d206261636b656e64" + } + ] +} \ No newline at end of file diff --git a/scripts/test_projects.sh b/scripts/test_projects.sh new file mode 100755 index 0000000..7b20984 --- /dev/null +++ b/scripts/test_projects.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test project API endpoints: +# - List projects (public) +# - Create a project (protected) using signature headers (no JWT) +# Requirements: curl, node, npm. Installs ethers locally into /tmp by default. +# +# Inputs (env): +# PUBLIC_ADDRESS (required) - wallet address +# PRIVATE_KEY (required) - wallet private key (0x-prefixed) +# API_URL (optional) - defaults to http://localhost:3001 +# PROJECT_NAME (optional) - defaults to "Shell Project" +# PROJECT_DESC (optional) - defaults to "Created via script" +# PROJECT_STATUS (optional) - defaults to "proposal" (proposal|ongoing|rejected) + +API_URL="${API_URL:-http://localhost:3001}" +ADDRESS="${PUBLIC_ADDRESS:-}" +PRIVATE_KEY="${PRIVATE_KEY:-}" + +# If not provided via env, prompt interactively (input hidden for private key). +if [[ -z "${ADDRESS}" ]]; then + read -r -p "Enter PUBLIC_ADDRESS (0x...): " ADDRESS +fi +if [[ -z "${PRIVATE_KEY}" ]]; then + read -r -s -p "Enter PRIVATE_KEY (0x..., hidden): " PRIVATE_KEY + echo +fi +if [[ -z "${ADDRESS}" || -z "${PRIVATE_KEY}" ]]; then + echo "PUBLIC_ADDRESS and PRIVATE_KEY are required. Aborting." + exit 1 +fi + +PROJECT_NAME="${PROJECT_NAME:-Shell Project}" +PROJECT_DESC="${PROJECT_DESC:-Created via script}" +PROJECT_STATUS="${PROJECT_STATUS:-proposal}" + +# Ensure we have ethers available without polluting the repo. +TOOLS_DIR="${TOOLS_DIR:-/tmp/theguildgenesis-login}" +export NODE_PATH="${TOOLS_DIR}/node_modules${NODE_PATH:+:${NODE_PATH}}" +export PATH="${TOOLS_DIR}/node_modules/.bin:${PATH}" +if ! node -e "require('ethers')" >/dev/null 2>&1; then + echo "Installing ethers@6 to ${TOOLS_DIR}..." + mkdir -p "${TOOLS_DIR}" + npm install --prefix "${TOOLS_DIR}" ethers@6 >/dev/null +fi + +echo "Fetching nonce for ${ADDRESS}..." +nonce_resp="$(curl -sS "${API_URL}/auth/nonce/${ADDRESS}")" +echo "Nonce response: ${nonce_resp}" +# Parse nonce safely +nonce="$(RESP="${nonce_resp}" python3 - <<'PY' +import json, os +data = json.loads(os.environ["RESP"]) +print(data["nonce"]) +PY +)" +if [[ -z "${nonce}" ]]; then + echo "Failed to parse nonce from response" + exit 1 +fi + +message=$'Sign this message to authenticate with The Guild.\n\nNonce: '"${nonce}" + +echo "Signing nonce..." +signature="$( + ADDRESS="${ADDRESS}" PRIVATE_KEY="${PRIVATE_KEY}" MESSAGE="${message}" \ + node - <<'NODE' +const { Wallet } = require('ethers'); + +const address = process.env.ADDRESS; +const pk = process.env.PRIVATE_KEY; +const message = process.env.MESSAGE; + +if (!address || !pk || !message) { + console.error("Missing ADDRESS, PRIVATE_KEY or MESSAGE"); + process.exit(1); +} + +const wallet = new Wallet(pk); +if (wallet.address.toLowerCase() !== address.toLowerCase()) { + console.error(`Private key does not match address. Wallet: ${wallet.address}, Provided: ${address}`); + process.exit(1); +} + +(async () => { + const sig = await wallet.signMessage(message); + console.log(sig); +})(); +NODE +)" + +echo "Signature: ${signature}" + +echo "Listing projects (public)..." +list_tmp="$(mktemp)" +list_status="$(curl -sS -o "${list_tmp}" -w "%{http_code}" \ + "${API_URL}/projects")" +list_resp="$(cat "${list_tmp}")" +rm -f "${list_tmp}" +echo "List HTTP ${list_status}: ${list_resp}" +if [[ "${list_status}" != "200" ]]; then + echo "List projects failed with status ${list_status}" + exit 1 +fi + +create_payload=$(cat < Date: Tue, 16 Dec 2025 15:26:38 +0100 Subject: [PATCH 3/8] add test for contribution token --- .../test/TheGuildContributionToken.t.sol | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 the-guild-smart-contracts/test/TheGuildContributionToken.t.sol 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..44b7c21 --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; + +contract TheGuildContributionTokenTest is Test { + TheGuildContributionToken private token; + + address private owner = address(this); + address private user1 = address(0xBEEF); + address private user2 = address(0xCAFE); + + function setUp() public { + // Deploy implementation and initialize it (sufficient for unit tests) + token = new TheGuildContributionToken(); + token.initialize(); + } + + function test_Metadata() public view { + assertEq(token.name(), "The Guild Contribution Token", "name mismatch"); + assertEq(token.symbol(), "TGC", "symbol mismatch"); + assertEq(token.decimals(), 18, "decimals mismatch"); + } + + function test_OwnerIsInitializer() public view { + assertEq(token.owner(), owner, "owner should be test contract"); + } + + function test_MintByOwner() public { + token.mint(user1, 1e18); + + assertEq(token.balanceOf(user1), 1e18, "balance mismatch"); + assertEq(token.totalSupply(), 1e18, "totalSupply mismatch"); + } + + function test_RevertMintIfNotOwner() public { + vm.prank(user1); + vm.expectRevert("Ownable: caller is not the owner"); + token.mint(user1, 1e18); + } + + function test_MintWithReason_EmitsEvent() public { + bytes memory reason = bytes("contribution:issue-123"); + + vm.expectEmit(true, false, false, true); + emit TheGuildContributionToken.MintedWithReason(user1, 5e17, reason); + + token.mintWithReason(user1, 5e17, reason); + + assertEq(token.balanceOf(user1), 5e17, "balance mismatch"); + assertEq(token.totalSupply(), 5e17, "totalSupply mismatch"); + } + + function test_BatchMint() public { + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1e18; + amounts[1] = 2e18; + + token.batchMint(recipients, amounts); + + assertEq(token.balanceOf(user1), 1e18, "user1 balance mismatch"); + assertEq(token.balanceOf(user2), 2e18, "user2 balance mismatch"); + assertEq(token.totalSupply(), 3e18, "totalSupply mismatch"); + } + + function test_BatchMintWithReason() public { + address[] memory recipients = new address[](2); + recipients[0] = user1; + recipients[1] = user2; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1e18; + amounts[1] = 2e18; + + bytes[] memory reasons = new bytes[](2); + reasons[0] = bytes("reason-1"); + reasons[1] = bytes("reason-2"); + + // Expect two MintedWithReason events + vm.expectEmit(true, false, false, true); + emit TheGuildContributionToken.MintedWithReason( + recipients[0], + amounts[0], + reasons[0] + ); + vm.expectEmit(true, false, false, true); + emit TheGuildContributionToken.MintedWithReason( + recipients[1], + amounts[1], + reasons[1] + ); + + token.batchMintWithReason(recipients, amounts, reasons); + + assertEq(token.balanceOf(user1), 1e18, "user1 balance mismatch"); + assertEq(token.balanceOf(user2), 2e18, "user2 balance mismatch"); + assertEq(token.totalSupply(), 3e18, "totalSupply mismatch"); + } +} From dbef7e0a1e350c2722321d7e1c15659c1ed4e336 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Tue, 16 Dec 2025 15:28:29 +0100 Subject: [PATCH 4/8] add TGC instructions in readme --- the-guild-smart-contracts/README.md | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md index 7071a21..7a19692 100644 --- a/the-guild-smart-contracts/README.md +++ b/the-guild-smart-contracts/README.md @@ -169,6 +169,101 @@ forge script script/TheGuildAttestationResolver.s.sol:TheGuildActivityTokenScrip --broadcast ``` +### Contribution Token (TGC) + +`TheGuildContributionToken` (symbol `TGC`) is an **upgradeable ERC20** used to reward contributions. + +- **Contract**: `src/TheGuildContributionToken.sol` + - Upgradable via UUPS (`UUPSUpgradeable`) + - Standard 18 decimals + - `mint(address to, uint256 amount)` – owner-only mint + - `mintWithReason(address to, uint256 amount, bytes reason)` – owner-only mint that emits `MintedWithReason` + - `batchMint(address[] recipients, uint256[] amounts)` – owner-only batch mint + - `batchMintWithReason(address[] recipients, uint256[] amounts, bytes[] reasons)` – owner-only batch mint with reasons +- **Tests**: `test/TheGuildContributionToken.t.sol` + - Covers metadata, ownership, minting, `MintedWithReason` event, and batch mint helpers. + +#### Deploying the upgradable TGC proxy + +Use `script/DeployTGC.s.sol` to deploy the implementation + ERC1967 proxy and call `initialize()` on the proxy: + +```shell +export PRIVATE_KEY=your_private_key +forge script script/DeployTGC.s.sol:DeployTGC \ + --rpc-url \ + --broadcast +``` + +The script logs both the proxy (TGC) address and the implementation address. + +#### Batch minting TGC from JSON + +Use `script/MintTGCFromJson.s.sol` to batch-mint TGC using `mintWithReason` from a JSON file. + +JSON format: + +```json +{ + "mints": [ + { + "recipient": "0x...", + "amount": "1000000000000000000", + "reason": "0x..." + } + ] +} +``` + +- `recipient`: recipient address +- `amount`: amount as a uint256 (string-encoded in JSON) +- `reason`: ABI-encoded bytes explaining the reason (e.g. `abi.encodePacked("issue-123")`) + +Usage: + +```shell +export PRIVATE_KEY=your_private_key +export TGC_PROXY_ADDRESS=0xYourTGCProxy + +# Optional: override JSON path (default: tgc-mints.json) +export JSON_PATH=contribution-tokens-latest.json + +# Dry run +export DRY_RUN=true +forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \ + --rpc-url + +# Production run +unset DRY_RUN +forge script script/MintTGCFromJson.s.sol:MintTGCFromJson \ + --rpc-url \ + --broadcast +``` + +Environment variables: + +- `PRIVATE_KEY`: signer that owns the TGC proxy +- `TGC_PROXY_ADDRESS`: address of the deployed TGC proxy +- `JSON_PATH`: path to the JSON file (default: `tgc-mints.json`) +- `DRY_RUN`: set to `true` to simulate without broadcasting (default: `false`) + +#### Upgrading the TGC implementation + +Use `script/UpgradeTGCImplementation.s.sol` to deploy a new implementation and upgrade the existing proxy. + +```shell +export PRIVATE_KEY=your_private_key +export TGC_PROXY_ADDRESS=0xYourTGCProxy + +forge script script/UpgradeTGCImplementation.s.sol:UpgradeTGCImplementation \ + --rpc-url \ + --broadcast +``` + +The script: +- Deploys a new `TheGuildContributionToken` implementation +- Calls `upgradeToAndCall` on the proxy (with empty data) +- Logs the proxy and new implementation addresses + ### Badge Ranking `TheGuildBadgeRanking` enables voting/ranking of badges for relevancy. Features: From 095ecb1e824530f6713e206da48392f5a7811724 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Tue, 16 Dec 2025 16:28:26 +0100 Subject: [PATCH 5/8] deply on amoy --- notebooks/contribution-tokens-latest.json | 54 ------------------ the-guild-smart-contracts/.env.example | 5 +- the-guild-smart-contracts/README.md | 5 +- .../contribution-tokens-latest.json | 57 +++++++++++++++---- .../script/DeployTGC.s.sol | 26 ++++++--- .../script/MintTGCFromJson.s.sol | 19 +++---- 6 files changed, 79 insertions(+), 87 deletions(-) delete mode 100644 notebooks/contribution-tokens-latest.json diff --git a/notebooks/contribution-tokens-latest.json b/notebooks/contribution-tokens-latest.json deleted file mode 100644 index fbbac2d..0000000 --- a/notebooks/contribution-tokens-latest.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "mints": [ - { - "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", - "amount": 40, - "reason": "0x4769746875622068616e646c65206572726f7273" - }, - { - "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", - "amount": 40, - "reason": "0x496d706c656d656e74205349574520666f72207468652066726f6e7420656e64" - }, - { - "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", - "amount": 80, - "reason": "0x466561747572653a206164642061206769746875622068616e646c6520746f2070726f66696c6520696e2046726f6e7420656e64" - }, - { - "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", - "amount": 40, - "reason": "0x41646420646f6320666f7220696e6465786572" - }, - { - "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", - "amount": 160, - "reason": "0x4164642061206261636b656e6420666f722074686520636f6e747269627574696f6e20706f696e747320286170692c20646229" - }, - { - "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", - "amount": 20, - "reason": "0x496d70726f7665206c616e64696e6720706167652064657369676e207769746820616e696d61746564206261636b67726f756e64" - }, - { - "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", - "amount": 40, - "reason": "0x4f6e207468652070726f66696c652c2074686572652073686f756c6420626520612073656374696f6e207769746820746865206261646765732069737375656420627920746861742070726f66696c65" - }, - { - "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", - "amount": 20, - "reason": "0x41646420636f707920746f20636c6970626f6172642069636f6e73206e65787420746f20616c6c20657468657265756d20616464726573736573" - }, - { - "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", - "amount": 40, - "reason": "0x416464206465736372697074696f6e206f6e207468652070726f66696c652070616765" - }, - { - "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", - "amount": 320, - "reason": "0x496d70726f76652053495745206c6f67696320746f207573652064796e616d69632c20757365722d7370656369666963206e6f6e6365202d206261636b656e64" - } - ] -} \ No newline at end of file diff --git a/the-guild-smart-contracts/.env.example b/the-guild-smart-contracts/.env.example index ce15e2b..bff6004 100644 --- a/the-guild-smart-contracts/.env.example +++ b/the-guild-smart-contracts/.env.example @@ -24,4 +24,7 @@ PRIVATE_KEY= JSON_PATH=./attestations.json SCHEMA_ID=0xbcd7561083784f9b5a1c2b3ddb7aa9db263d43c58f7374cfa4875646824a47de DRY_RUN=false -RPC_URL= \ No newline at end of file +RPC_URL= + +# contribution token mint +TGC_PROXY_ADDRESS= \ No newline at end of file diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md index 7a19692..39bb826 100644 --- a/the-guild-smart-contracts/README.md +++ b/the-guild-smart-contracts/README.md @@ -27,6 +27,9 @@ Salt: "theguild_v_0.1.2" TheGuildActivityToken https://amoy.polygonscan.com/address/0x4649490B118389d0Be8F48b8953eFb235d8CB545 +TheGuildContributionToken (proxy) +https://amoy.polygonscan.com/address/0x14d403EaE3E0b2E2dc6379C9729Df6906fF38bE7 + TheGuildBadgeRegistry https://amoy.polygonscan.com/address/0x94f5F12BE60a338D263882a1A49E81ca8A0c30F4 @@ -243,7 +246,7 @@ Environment variables: - `PRIVATE_KEY`: signer that owns the TGC proxy - `TGC_PROXY_ADDRESS`: address of the deployed TGC proxy -- `JSON_PATH`: path to the JSON file (default: `tgc-mints.json`) +- `JSON_PATH`: path to the JSON file (default: `contribution-tokens-latest.json`) - `DRY_RUN`: set to `true` to simulate without broadcasting (default: `false`) #### Upgrading the TGC implementation diff --git a/the-guild-smart-contracts/contribution-tokens-latest.json b/the-guild-smart-contracts/contribution-tokens-latest.json index bf01196..fbbac2d 100644 --- a/the-guild-smart-contracts/contribution-tokens-latest.json +++ b/the-guild-smart-contracts/contribution-tokens-latest.json @@ -1,21 +1,54 @@ { "mints": [ { - "recipient": "0x0000000000000000000000000000000000000001", - "amount": 1000000000000000000, - "reason": "0x536f6c766564206120c3b17265616c20636f6d6d756e697479206973737565" + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 40, + "reason": "0x4769746875622068616e646c65206572726f7273" }, { - "recipient": "0x0000000000000000000000000000000000000002", - "amount": 2500000000000000000, - "reason": "0x436f6e747269627574656420746f206f70656e2d736f7572636520646f6373" + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 40, + "reason": "0x496d706c656d656e74205349574520666f72207468652066726f6e7420656e64" }, { - "recipient": "0x0000000000000000000000000000000000000003", - "amount": 5000000000000000000, - "reason": "0x4d656e746f72656420616e642068656c706564206d61696e7461696e20646973636f7264" + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "amount": 80, + "reason": "0x466561747572653a206164642061206769746875622068616e646c6520746f2070726f66696c6520696e2046726f6e7420656e64" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "amount": 40, + "reason": "0x41646420646f6320666f7220696e6465786572" + }, + { + "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", + "amount": 160, + "reason": "0x4164642061206261636b656e6420666f722074686520636f6e747269627574696f6e20706f696e747320286170692c20646229" + }, + { + "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", + "amount": 20, + "reason": "0x496d70726f7665206c616e64696e6720706167652064657369676e207769746820616e696d61746564206261636b67726f756e64" + }, + { + "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", + "amount": 40, + "reason": "0x4f6e207468652070726f66696c652c2074686572652073686f756c6420626520612073656374696f6e207769746820746865206261646765732069737375656420627920746861742070726f66696c65" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 20, + "reason": "0x41646420636f707920746f20636c6970626f6172642069636f6e73206e65787420746f20616c6c20657468657265756d20616464726573736573" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 40, + "reason": "0x416464206465736372697074696f6e206f6e207468652070726f66696c652070616765" + }, + { + "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", + "amount": 320, + "reason": "0x496d70726f76652053495745206c6f67696320746f207573652064796e616d69632c20757365722d7370656369666963206e6f6e6365202d206261636b656e64" } ] -} - - +} \ No newline at end of file diff --git a/the-guild-smart-contracts/script/DeployTGC.s.sol b/the-guild-smart-contracts/script/DeployTGC.s.sol index 4b99415..7ac83e0 100644 --- a/the-guild-smart-contracts/script/DeployTGC.s.sol +++ b/the-guild-smart-contracts/script/DeployTGC.s.sol @@ -7,30 +7,42 @@ import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; /// @notice Deploys the upgradable TGC token (implementation + ERC1967/UUPS proxy). +/// @dev Uses CREATE2 with a fixed salt for deterministic deployment, similar to `FullDeploymentScript`. contract DeployTGC is Script { function run() public { + // Use the same pattern as FullDeploymentScript for deterministic addresses. + // Update this salt string when you intentionally want a new deployment address. + bytes32 salt = bytes32("theguild_tgc_v_0.1.2"); + uint256 pk = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(pk); vm.startBroadcast(pk); console.log( "Deploying TheGuildContributionToken (TGC) implementation..." ); - TheGuildContributionToken implementation = new TheGuildContributionToken(); + TheGuildContributionToken implementation = new TheGuildContributionToken{ + salt: salt + }(); console.log("Deploying ERC1967 proxy for TGC..."); - ERC1967Proxy proxy = new ERC1967Proxy( + // Deploy proxy without initialization data so we can call initialize + // directly from the deployer EOA. This ensures Ownable's owner is the + // deployer (and not the CREATE2 factory address). + ERC1967Proxy proxy = new ERC1967Proxy{salt: salt}( address(implementation), - abi.encodeWithSelector( - TheGuildContributionToken.initialize.selector - ) + "" ); + console.log("Initializing TGC proxy..."); + TheGuildContributionToken(address(proxy)).initialize(); + console.log("=== TGC Deployment Summary ==="); + console.logBytes32(salt); console.log("Proxy (TGC) address:", address(proxy)); + console.log("Deployer (and initial owner):", deployer); console.log("Implementation address:", address(implementation)); vm.stopBroadcast(); } } - - diff --git a/the-guild-smart-contracts/script/MintTGCFromJson.s.sol b/the-guild-smart-contracts/script/MintTGCFromJson.s.sol index e9e306f..8f428ae 100644 --- a/the-guild-smart-contracts/script/MintTGCFromJson.s.sol +++ b/the-guild-smart-contracts/script/MintTGCFromJson.s.sol @@ -35,14 +35,12 @@ contract MintTGCFromJson is Script { address proxyAddress = vm.envAddress("TGC_PROXY_ADDRESS"); require(proxyAddress != address(0), "TGC_PROXY_ADDRESS not set"); - TheGuildContributionToken tgc = TheGuildContributionToken( - proxyAddress - ); + TheGuildContributionToken tgc = TheGuildContributionToken(proxyAddress); // Read JSON file string memory jsonPath = vm.envOr( "JSON_PATH", - string("tgc-mints.json") + string("contribution-tokens-latest.json") ); console.log("Reading JSON from:", jsonPath); @@ -111,13 +109,12 @@ contract MintTGCFromJson is Script { (uint256) ); - bytes memory reason = abi.decode( - vm.parseJson( - jsonData, - string(abi.encodePacked(basePath, ".reason")) - ), - (bytes) + // Parse reason as hex string (e.g. "0x...") using stdJson, then convert to bytes. + // Using stdJson.readString avoids type inference issues with parseJson. + string memory reasonStr = jsonData.readString( + string(abi.encodePacked(basePath, ".reason")) ); + bytes memory reason = vm.parseBytes(reasonStr); tempMints[count] = MintData({ recipient: recipient, @@ -199,5 +196,3 @@ contract MintTGCFromJson is Script { console.log("Execution completed successfully!"); } } - - From 520bb36a28c5190be779794b2a056055be1895a2 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Tue, 16 Dec 2025 16:53:05 +0100 Subject: [PATCH 6/8] fix test --- .../test/TheGuildContributionToken.t.sol | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol b/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol index 44b7c21..7eeacd7 100644 --- a/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol +++ b/the-guild-smart-contracts/test/TheGuildContributionToken.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract TheGuildContributionTokenTest is Test { TheGuildContributionToken private token; @@ -12,9 +13,18 @@ contract TheGuildContributionTokenTest is Test { address private user2 = address(0xCAFE); function setUp() public { - // Deploy implementation and initialize it (sufficient for unit tests) - token = new TheGuildContributionToken(); - token.initialize(); + // Deploy implementation (cannot be initialized due to _disableInitializers) + TheGuildContributionToken implementation = new TheGuildContributionToken(); + + // Deploy proxy and initialize it + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + abi.encodeWithSelector( + TheGuildContributionToken.initialize.selector + ) + ); + + token = TheGuildContributionToken(address(proxy)); } function test_Metadata() public view { @@ -36,7 +46,7 @@ contract TheGuildContributionTokenTest is Test { function test_RevertMintIfNotOwner() public { vm.prank(user1); - vm.expectRevert("Ownable: caller is not the owner"); + vm.expectRevert(); token.mint(user1, 1e18); } From 11153bfad8d006e30c5ca047ecf0cced8e4fbf73 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Mon, 22 Dec 2025 10:56:47 +0100 Subject: [PATCH 7/8] index MintedWithReason --- the-guild-smart-contracts/src/TheGuildContributionToken.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/the-guild-smart-contracts/src/TheGuildContributionToken.sol b/the-guild-smart-contracts/src/TheGuildContributionToken.sol index 94c195a..8cd39c2 100644 --- a/the-guild-smart-contracts/src/TheGuildContributionToken.sol +++ b/the-guild-smart-contracts/src/TheGuildContributionToken.sol @@ -17,7 +17,11 @@ contract TheGuildContributionToken is /// @param to The recipient of the minted tokens. /// @param amount The amount of tokens minted. /// @param reason Arbitrary bytes describing why the user received contribution tokens. - event MintedWithReason(address indexed to, uint256 amount, bytes reason); + event MintedWithReason( + address indexed to, + uint256 indexed amount, + bytes indexed reason + ); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { From f4748c99273592d3ed31606728f50347bb9817ea Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Mon, 22 Dec 2025 11:04:02 +0100 Subject: [PATCH 8/8] use explicit reason in contribution json --- .../contribution-tokens-latest.json | 22 +++++++++---------- .../script/MintTGCFromJson.s.sol | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/the-guild-smart-contracts/contribution-tokens-latest.json b/the-guild-smart-contracts/contribution-tokens-latest.json index fbbac2d..b3e657c 100644 --- a/the-guild-smart-contracts/contribution-tokens-latest.json +++ b/the-guild-smart-contracts/contribution-tokens-latest.json @@ -3,52 +3,52 @@ { "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", "amount": 40, - "reason": "0x4769746875622068616e646c65206572726f7273" + "reason": "Github handle errors" }, { "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", "amount": 40, - "reason": "0x496d706c656d656e74205349574520666f72207468652066726f6e7420656e64" + "reason": "Implement SIWE for the front end" }, { "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", "amount": 80, - "reason": "0x466561747572653a206164642061206769746875622068616e646c6520746f2070726f66696c6520696e2046726f6e7420656e64" + "reason": "Feature: add a github handle to profile in Front end" }, { "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", "amount": 40, - "reason": "0x41646420646f6320666f7220696e6465786572" + "reason": "Add doc for indexer" }, { "recipient": "0x0BAd9DaD98143b2E946e8A40E4f27537be2f55E2", "amount": 160, - "reason": "0x4164642061206261636b656e6420666f722074686520636f6e747269627574696f6e20706f696e747320286170692c20646229" + "reason": "Add a backend for the contribution points (api, db)" }, { "recipient": "0xB66442A4Bf0636B6b533D607dB6066AD987368FE", "amount": 20, - "reason": "0x496d70726f7665206c616e64696e6720706167652064657369676e207769746820616e696d61746564206261636b67726f756e64" + "reason": "Improve landing page design with animated background" }, { "recipient": "0x63E5a246937549b3ECcBB410AF42da54F999D172", "amount": 40, - "reason": "0x4f6e207468652070726f66696c652c2074686572652073686f756c6420626520612073656374696f6e207769746820746865206261646765732069737375656420627920746861742070726f66696c65" + "reason": "On the profile, there should be a section with the badges issued by that profile" }, { "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", "amount": 20, - "reason": "0x41646420636f707920746f20636c6970626f6172642069636f6e73206e65787420746f20616c6c20657468657265756d20616464726573736573" + "reason": "Add copy to clipboard icons next to all ethereum addresses" }, { "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", "amount": 40, - "reason": "0x416464206465736372697074696f6e206f6e207468652070726f66696c652070616765" + "reason": "Add description on the profile page" }, { "recipient": "0x2b33E4D2bD2f34310956dCb462d58413d3dCcdf8", "amount": 320, - "reason": "0x496d70726f76652053495745206c6f67696320746f207573652064796e616d69632c20757365722d7370656369666963206e6f6e6365202d206261636b656e64" + "reason": "Improve SIWE logic to use dynamic, user-specific nonce - backend" } ] -} \ No newline at end of file +} diff --git a/the-guild-smart-contracts/script/MintTGCFromJson.s.sol b/the-guild-smart-contracts/script/MintTGCFromJson.s.sol index 8f428ae..a80c99c 100644 --- a/the-guild-smart-contracts/script/MintTGCFromJson.s.sol +++ b/the-guild-smart-contracts/script/MintTGCFromJson.s.sol @@ -12,7 +12,7 @@ import {TheGuildContributionToken} from "../src/TheGuildContributionToken.sol"; /// { /// "recipient": "0x...", /// "amount": "1000000000000000000", -/// "reason": "0x..." // bytes, e.g. abi.encodePacked(...) +/// "reason": "Some human readable text" /// } /// ] /// } @@ -109,12 +109,12 @@ contract MintTGCFromJson is Script { (uint256) ); - // Parse reason as hex string (e.g. "0x...") using stdJson, then convert to bytes. - // Using stdJson.readString avoids type inference issues with parseJson. + // Parse reason as a normal UTF-8 string and convert to bytes. + // This makes the JSON human-readable for non-technical contributors. string memory reasonStr = jsonData.readString( string(abi.encodePacked(basePath, ".reason")) ); - bytes memory reason = vm.parseBytes(reasonStr); + bytes memory reason = bytes(reasonStr); tempMints[count] = MintData({ recipient: recipient,