From 3c03e3c79983385f893ce7573d1917409fb871ff Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Tue, 28 Oct 2025 11:19:32 +0000 Subject: [PATCH 1/5] Implement badge voting/ranking system to allow community members to vote on badge relevancy --- .../src/TheGuildBadgeRanking.sol | 52 ++++++++++++++ .../test/TheGuildBadgeRanking.t.sol | 72 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 the-guild-smart-contracts/src/TheGuildBadgeRanking.sol create mode 100644 the-guild-smart-contracts/test/TheGuildBadgeRanking.t.sol diff --git a/the-guild-smart-contracts/src/TheGuildBadgeRanking.sol b/the-guild-smart-contracts/src/TheGuildBadgeRanking.sol new file mode 100644 index 0000000..dc3b954 --- /dev/null +++ b/the-guild-smart-contracts/src/TheGuildBadgeRanking.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {TheGuildBadgeRegistry} from "./TheGuildBadgeRegistry.sol"; + +/// @title TheGuildBadgeRanking +/// @notice Allows voting on badge relevancy to filter spam and rank badges. +contract TheGuildBadgeRanking { + TheGuildBadgeRegistry public badgeRegistry; + + /// @notice Emitted when a badge receives an upvote. + event BadgeUpvoted( + bytes32 indexed badgeName, + address indexed voter + ); + + // badgeName => upvote count + mapping(bytes32 => uint256) public upvotes; + // badgeName => voter => hasVoted + mapping(bytes32 => mapping(address => bool)) private hasVoted; + + constructor(TheGuildBadgeRegistry _badgeRegistry) { + require(address(_badgeRegistry) != address(0), "INVALID_REGISTRY"); + badgeRegistry = _badgeRegistry; + } + + /// @notice Upvote a badge for relevancy. + /// @param badgeName The name of the badge to upvote. + /// @dev Reverts if badge doesn't exist or address has already voted for this badge. + function upvoteBadge(bytes32 badgeName) external { + require(badgeRegistry.exists(badgeName), "BADGE_NOT_FOUND"); + require(!hasVoted[badgeName][msg.sender], "ALREADY_VOTED"); + + hasVoted[badgeName][msg.sender] = true; + upvotes[badgeName]++; + + emit BadgeUpvoted(badgeName, msg.sender); + } + + /// @notice Check if an address has voted for a specific badge. + /// @param badgeName The name of the badge. + /// @param voter The address to check. + function hasVotedForBadge(bytes32 badgeName, address voter) external view returns (bool) { + return hasVoted[badgeName][voter]; + } + + /// @notice Get the upvote count for a badge. + /// @param badgeName The name of the badge. + function getUpvotes(bytes32 badgeName) external view returns (uint256) { + return upvotes[badgeName]; + } +} diff --git a/the-guild-smart-contracts/test/TheGuildBadgeRanking.t.sol b/the-guild-smart-contracts/test/TheGuildBadgeRanking.t.sol new file mode 100644 index 0000000..ba91b97 --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildBadgeRanking.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; +import {TheGuildBadgeRanking} from "../src/TheGuildBadgeRanking.sol"; + +contract TheGuildBadgeRankingTest is Test { + TheGuildBadgeRegistry private registry; + TheGuildBadgeRanking private ranking; + + address private voter1 = address(0x1); + address private voter2 = address(0x2); + bytes32 private badgeName = bytes32("TEST_BADGE"); + + function setUp() public { + registry = new TheGuildBadgeRegistry(); + ranking = new TheGuildBadgeRanking(registry); + + // Create a badge for testing + registry.createBadge(badgeName, bytes32("A test badge")); + } + + function test_UpvoteBadge_SucceedsAndEmitsEvent() public { + vm.prank(voter1); + vm.expectEmit(true, true, false, false); + emit TheGuildBadgeRanking.BadgeUpvoted(badgeName, voter1); + + ranking.upvoteBadge(badgeName); + assertEq(ranking.getUpvotes(badgeName), 1, "upvotes should be 1"); + } + + function test_UpvoteBadge_MultipleVoters() public { + vm.prank(voter1); + ranking.upvoteBadge(badgeName); + + vm.prank(voter2); + ranking.upvoteBadge(badgeName); + + assertEq(ranking.getUpvotes(badgeName), 2, "upvotes should be 2"); + } + + function test_UpvoteBadge_RevertOnDuplicateVote() public { + vm.prank(voter1); + ranking.upvoteBadge(badgeName); + + vm.expectRevert(bytes("ALREADY_VOTED")); + vm.prank(voter1); + ranking.upvoteBadge(badgeName); + } + + function test_UpvoteBadge_RevertOnNonexistentBadge() public { + bytes32 nonexistentBadge = bytes32("NONEXISTENT"); + + vm.prank(voter1); + vm.expectRevert(bytes("BADGE_NOT_FOUND")); + ranking.upvoteBadge(nonexistentBadge); + } + + function test_HasVotedForBadge_ReturnsTrueAfterVoting() public { + vm.prank(voter1); + ranking.upvoteBadge(badgeName); + + assertTrue(ranking.hasVotedForBadge(badgeName, voter1), "should have voted"); + assertFalse(ranking.hasVotedForBadge(badgeName, voter2), "should not have voted"); + } + + function test_Constructor_RevertOnInvalidRegistry() public { + vm.expectRevert(bytes("INVALID_REGISTRY")); + new TheGuildBadgeRanking(TheGuildBadgeRegistry(address(0))); + } +} From 0465d281b5a56409e52f3b524a321a87ef028fd3 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Tue, 28 Oct 2025 18:25:40 +0000 Subject: [PATCH 2/5] implement badge voting/ranking system --- the-guild-smart-contracts/README.md | 37 +++++++++++++++++++ .../script/FullDeploymentScript.s.sol | 14 +++++++ .../script/TheGuildBadgeRanking.s.sol | 25 +++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md index 6707ba6..f67039c 100644 --- a/the-guild-smart-contracts/README.md +++ b/the-guild-smart-contracts/README.md @@ -7,6 +7,9 @@ Anybody can create a badge. The idea is to let the community input whatever they We will create a smart contract TheGuildBadgeRegistry that will have a list of badges with unique non-duplicate names. +### Badge Ranking +Community members can vote on badge relevancy to filter spam and promote the most relevant badges. The BadgeRanking contract tracks upvotes per badge and prevents duplicate voting from the same address. + ### Attestations Then, we let users create an attestation of a badge to another user. The attestation can contain an optional justification (link to a project, or text explanation). @@ -153,6 +156,40 @@ forge script script/TheGuildActivityToken.s.sol:TheGuildActivityTokenScript \ --broadcast ``` +### Badge Ranking + +`TheGuildBadgeRanking` enables voting/ranking of badges for relevancy. Features: + +- Tracks upvotes per badge +- Prevents duplicate voting from the same address on the same badge +- Integrates with `TheGuildBadgeRegistry` to validate badges before voting +- Emits `BadgeUpvoted` event for off-chain indexing + +**Key Functions:** +- `upvoteBadge(bytes32 badgeName)`: Vote for a badge (one vote per address per badge) +- `getUpvotes(bytes32 badgeName)`: Get total upvote count for a badge +- `hasVotedForBadge(bytes32 badgeName, address voter)`: Check if address has voted for a badge + +Deploy individually: + +```shell +# Set BADGE_REGISTRY_ADDRESS to your deployed registry, or use default (Amoy dev) +forge script script/TheGuildBadgeRanking.s.sol:TheGuildBadgeRankingScript \ + --rpc-url \ + --private-key \ + --broadcast +``` + +Or use environment variable: + +```shell +export BADGE_REGISTRY_ADDRESS=0x8ac95734e778322684f1d318fb7633777baa8427 +forge script script/TheGuildBadgeRanking.s.sol:TheGuildBadgeRankingScript \ + --rpc-url \ + --private-key \ + --broadcast +``` + ### Cast ```shell diff --git a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol index 9090698..71842f3 100644 --- a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol +++ b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol @@ -7,6 +7,7 @@ import {AttestationRequestData, AttestationRequest} from "eas-contracts/IEAS.sol import {SchemaRegistry} from "eas-contracts/SchemaRegistry.sol"; import {TheGuildActivityToken} from "../src/TheGuildActivityToken.sol"; import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; +import {TheGuildBadgeRanking} from "../src/TheGuildBadgeRanking.sol"; import {EASUtils} from "./utils/EASUtils.s.sol"; import {console} from "forge-std/console.sol"; @@ -71,6 +72,19 @@ contract FullDeploymentScript is Script { ); } + // Deploy or attach to existing badge ranking via CREATE2 + TheGuildBadgeRanking badgeRanking; + try new TheGuildBadgeRanking{salt: salt}(badgeRegistry) returns ( + TheGuildBadgeRanking deployed + ) { + badgeRanking = deployed; + } catch { + // If already deployed with same salt + initCode, attach to the predicted address + badgeRanking = TheGuildBadgeRanking( + 0x0000000000000000000000000000000000000000 // TODO: update with actual deployed address + ); + } + // Create some attestations AttestationRequestData memory data = AttestationRequestData({ // TheGuild test account diff --git a/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol b/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol new file mode 100644 index 0000000..9845a01 --- /dev/null +++ b/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; +import {TheGuildBadgeRanking} from "../src/TheGuildBadgeRanking.sol"; + +contract TheGuildBadgeRankingScript is Script { + function run() public { + // Badge registry address - can be provided via env var or defaults to Amoy dev address + address badgeRegistryAddress = vm.envOr( + "BADGE_REGISTRY_ADDRESS", + address(0x8aC95734e778322684f1D318fb7633777baa8427) // Amoy dev registry + ); + + TheGuildBadgeRegistry badgeRegistry = TheGuildBadgeRegistry(badgeRegistryAddress); + + // Salt can be provided via env var or defaults to zero salt + bytes32 salt = bytes32(vm.envOr("CREATE2_SALT", uint256(0))); + + vm.startBroadcast(); + new TheGuildBadgeRanking{salt: salt}(badgeRegistry); + vm.stopBroadcast(); + } +} \ No newline at end of file From 1397449322cc76e424dc808e389635c838aeaa0c Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Wed, 29 Oct 2025 09:33:20 +0000 Subject: [PATCH 3/5] improve: use environment variable for badge ranking fallback address --- the-guild-smart-contracts/script/FullDeploymentScript.s.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol index 71842f3..431d90f 100644 --- a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol +++ b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol @@ -80,8 +80,12 @@ contract FullDeploymentScript is Script { badgeRanking = deployed; } catch { // If already deployed with same salt + initCode, attach to the predicted address + // Use environment variable to override or computed address on first deployment badgeRanking = TheGuildBadgeRanking( - 0x0000000000000000000000000000000000000000 // TODO: update with actual deployed address + vm.envOr( + "BADGE_RANKING_ADDRESS", + address(0) // Will fail on catch if not set; set env var after first deployment + ) ); } From aeaafc3dc4ba5675b9edb16f471796fcfe7b270d Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Wed, 29 Oct 2025 17:54:45 +0000 Subject: [PATCH 4/5] updated the code as per request --- .../script/FullDeploymentScript.s.sol | 27 +++++++++++++------ .../script/TheGuildBadgeRanking.s.sol | 7 +++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol index 431d90f..fd61f0a 100644 --- a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol +++ b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol @@ -79,14 +79,9 @@ contract FullDeploymentScript is Script { ) { badgeRanking = deployed; } catch { - // If already deployed with same salt + initCode, attach to the predicted address - // Use environment variable to override or computed address on first deployment - badgeRanking = TheGuildBadgeRanking( - vm.envOr( - "BADGE_RANKING_ADDRESS", - address(0) // Will fail on catch if not set; set env var after first deployment - ) - ); + // If already deployed with same salt + initCode, attach to the predicted CREATE2 address + address predictedAddress = computeCreate2Address(salt, badgeRegistry); + badgeRanking = TheGuildBadgeRanking(predictedAddress); } // Create some attestations @@ -107,4 +102,20 @@ contract FullDeploymentScript is Script { eas.attest(request); vm.stopBroadcast(); } + + function computeCreate2Address(bytes32 salt, TheGuildBadgeRegistry badgeRegistry) internal pure returns (address) { + // CREATE2 factory address + address factory = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + + // Get the init code for TheGuildBadgeRanking(badgeRegistry) + bytes memory initCode = abi.encodePacked( + type(TheGuildBadgeRanking).creationCode, + abi.encode(badgeRegistry) + ); + + bytes32 initCodeHash = keccak256(initCode); + bytes32 data = keccak256(abi.encodePacked(bytes1(0xff), factory, salt, initCodeHash)); + + return address(uint160(uint256(data))); + } } diff --git a/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol b/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol index 9845a01..d91b2ea 100644 --- a/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol +++ b/the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol @@ -13,13 +13,12 @@ contract TheGuildBadgeRankingScript is Script { address(0x8aC95734e778322684f1D318fb7633777baa8427) // Amoy dev registry ); - TheGuildBadgeRegistry badgeRegistry = TheGuildBadgeRegistry(badgeRegistryAddress); - // Salt can be provided via env var or defaults to zero salt bytes32 salt = bytes32(vm.envOr("CREATE2_SALT", uint256(0))); - vm.startBroadcast(); - new TheGuildBadgeRanking{salt: salt}(badgeRegistry); + vm.startBroadcast(); + // Pass the address (cast to TheGuildBadgeRegistry) + new TheGuildBadgeRanking{salt: salt}(TheGuildBadgeRegistry(badgeRegistryAddress)); vm.stopBroadcast(); } } \ No newline at end of file From 86b321e4154e8ad9886b331b1a49c828c5c8d0ad Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Thu, 30 Oct 2025 05:43:10 +0000 Subject: [PATCH 5/5] fixed the salt usage use as common --- .../script/FullDeploymentScript.s.sol | 77 ++++--------------- 1 file changed, 16 insertions(+), 61 deletions(-) diff --git a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol index fd61f0a..6972c02 100644 --- a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol +++ b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol @@ -24,17 +24,7 @@ contract FullDeploymentScript is Script { vm.startBroadcast(); // Deploy or attach to existing activity token via CREATE2 - TheGuildActivityToken activityToken; - try new TheGuildActivityToken{salt: salt}(eas) returns ( - TheGuildActivityToken deployed - ) { - activityToken = deployed; - } catch { - // If already deployed with same salt + initCode, attach to the predicted address - activityToken = TheGuildActivityToken( - payable(0x5a79Dd0F66E2C1203948dD49634E506b3D8723A0) - ); - } + TheGuildActivityToken activityToken = new TheGuildActivityToken{salt: salt}(eas); // Register TheGuild Schema string memory schema = "bytes32 badgeName, bytes justification"; @@ -46,43 +36,24 @@ contract FullDeploymentScript is Script { console.logBytes32(schemaId); // Deploy or attach to existing badge registry via CREATE2 - TheGuildBadgeRegistry badgeRegistry; - try new TheGuildBadgeRegistry{salt: salt}() returns ( - TheGuildBadgeRegistry deployed - ) { - badgeRegistry = deployed; + TheGuildBadgeRegistry badgeRegistry = new TheGuildBadgeRegistry{salt: salt}(); - // Create some badges - badgeRegistry.createBadge( - bytes32("Rust"), - bytes32("Know how to code in Rust") - ); - badgeRegistry.createBadge( - bytes32("Solidity"), - bytes32("Know how to code in Solidity") - ); - badgeRegistry.createBadge( - bytes32("TypeScript"), - bytes32("Know how to code in TypeScript") - ); - } catch { - // If already deployed with same salt + initCode, attach to the predicted address - badgeRegistry = TheGuildBadgeRegistry( - 0x8baA0d5135D241bd22a9eB35915300aCfB286307 - ); - } + // Create some badges + badgeRegistry.createBadge( + bytes32("Rust"), + bytes32("Know how to code in Rust") + ); + badgeRegistry.createBadge( + bytes32("Solidity"), + bytes32("Know how to code in Solidity") + ); + badgeRegistry.createBadge( + bytes32("TypeScript"), + bytes32("Know how to code in TypeScript") + ); // Deploy or attach to existing badge ranking via CREATE2 - TheGuildBadgeRanking badgeRanking; - try new TheGuildBadgeRanking{salt: salt}(badgeRegistry) returns ( - TheGuildBadgeRanking deployed - ) { - badgeRanking = deployed; - } catch { - // If already deployed with same salt + initCode, attach to the predicted CREATE2 address - address predictedAddress = computeCreate2Address(salt, badgeRegistry); - badgeRanking = TheGuildBadgeRanking(predictedAddress); - } + TheGuildBadgeRanking badgeRanking = new TheGuildBadgeRanking{salt: salt}(badgeRegistry); // Create some attestations AttestationRequestData memory data = AttestationRequestData({ @@ -102,20 +73,4 @@ contract FullDeploymentScript is Script { eas.attest(request); vm.stopBroadcast(); } - - function computeCreate2Address(bytes32 salt, TheGuildBadgeRegistry badgeRegistry) internal pure returns (address) { - // CREATE2 factory address - address factory = 0x4e59b44847b379578588920cA78FbF26c0B4956C; - - // Get the init code for TheGuildBadgeRanking(badgeRegistry) - bytes memory initCode = abi.encodePacked( - type(TheGuildBadgeRanking).creationCode, - abi.encode(badgeRegistry) - ); - - bytes32 initCodeHash = keccak256(initCode); - bytes32 data = keccak256(abi.encodePacked(bytes1(0xff), factory, salt, initCodeHash)); - - return address(uint160(uint256(data))); - } }