Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions the-guild-smart-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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 <your_rpc_url> \
--private-key <your_private_key> \
--broadcast
```

Or use environment variable:

```shell
export BADGE_REGISTRY_ADDRESS=0x8ac95734e778322684f1d318fb7633777baa8427
forge script script/TheGuildBadgeRanking.s.sol:TheGuildBadgeRankingScript \
--rpc-url <your_rpc_url> \
--private-key <your_private_key> \
--broadcast
```

### Cast

```shell
Expand Down
54 changes: 19 additions & 35 deletions the-guild-smart-contracts/script/FullDeploymentScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -23,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";
Expand All @@ -45,31 +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 = new TheGuildBadgeRanking{salt: salt}(badgeRegistry);

// Create some attestations
AttestationRequestData memory data = AttestationRequestData({
Expand Down
24 changes: 24 additions & 0 deletions the-guild-smart-contracts/script/TheGuildBadgeRanking.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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
);

// Salt can be provided via env var or defaults to zero salt
bytes32 salt = bytes32(vm.envOr("CREATE2_SALT", uint256(0)));

vm.startBroadcast();
// Pass the address (cast to TheGuildBadgeRegistry)
new TheGuildBadgeRanking{salt: salt}(TheGuildBadgeRegistry(badgeRegistryAddress));
vm.stopBroadcast();
}
}
52 changes: 52 additions & 0 deletions the-guild-smart-contracts/src/TheGuildBadgeRanking.sol
Original file line number Diff line number Diff line change
@@ -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];
}
}
72 changes: 72 additions & 0 deletions the-guild-smart-contracts/test/TheGuildBadgeRanking.t.sol
Original file line number Diff line number Diff line change
@@ -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)));
}
}
Loading