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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ jobs:
restore-keys: |
${{ runner.os }}-foundry-

- name: Install OpenZeppelin
run: |
cd the-guild-smart-contracts
forge install OpenZeppelin/openzeppelin-contracts

- name: Build contracts
run: |
cd the-guild-smart-contracts
Expand Down
12 changes: 12 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
[submodule "the-guild-smart-contracts/lib/forge-std"]
path = the-guild-smart-contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "the-guild-smart-contracts/lib/openzeppelin-foundry-upgrades"]
path = the-guild-smart-contracts/lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
[submodule "the-guild-smart-contracts/lib/openzeppelin-contracts-upgradeable"]
path = the-guild-smart-contracts/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "the-guild-smart-contracts/lib/openzeppelin-contracts"]
path = the-guild-smart-contracts/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "the-guild-smart-contracts/lib/eas-contracts"]
path = the-guild-smart-contracts/lib/eas-contracts
url = https://github.com/ethereum-attestation-service/eas-contracts
13 changes: 13 additions & 0 deletions the-guild-smart-contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Environment variables for TheGuild smart contracts

# CREATE2 salt used for deterministic deployments
# Accepts uint value; example values: 1, 123456, etc.
CREATE2_SALT=1

# RPC URL and PRIVATE KEY are typically provided at runtime to forge script
# RPC_URL=
# PRIVATE_KEY=

# EAS addresses for networks not hardcoded in the deploy script.
# Generic fallback for any other network (used if chain isn't matched)
EAS_ADDRESS=
59 changes: 59 additions & 0 deletions the-guild-smart-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ https://book.getfoundry.sh/
$ forge build
```

### Dependencies

OpenZeppelin contracts (for ERC20):

```shell
forge install OpenZeppelin/openzeppelin-contracts
```

### Test

```shell
Expand Down Expand Up @@ -55,6 +63,57 @@ $ anvil
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```

### Deterministic Deployments (CREATE2)

Follow the Foundry guide on deterministic deployments using CREATE2: https://getfoundry.sh/guides/deterministic-deployments-using-create2

We configured `foundry.toml` to pin `solc`, set `evm_version`, and disable metadata hashing for stable bytecode, and enabled `always_use_create_2_factory`.

Use the deploy script with a salt (env var `CREATE2_SALT`) to deploy at a deterministic address:

```shell
# Example on Sepolia (replace RPC and PK)
export CREATE2_SALT=1
forge script script/TheGuildBadgeRegistry.s.sol:TheGuildBadgeRegistryScript \
--rpc-url <your_rpc_url> \
--private-key <your_private_key> \
--broadcast

# Use a specific salt value (hex string -> uint)
export CREATE2_SALT=123456
forge script script/TheGuildBadgeRegistry.s.sol:TheGuildBadgeRegistryScript --rpc-url <RPC> --private-key <PK> --broadcast
```

### Tokens

- `TheGuildActivityToken` (symbol `TGA`) is an ERC20 with standard 18 decimals. The deployer is the owner and can mint. See `src/TheGuildActivityToken.sol`.
- The token also acts as an EAS Resolver: it inherits `SchemaResolver` and mints 10 TGA to the attester address on each successful attestation.

#### EAS Resolver behavior (TheGuildActivityToken)

- Inherits `SchemaResolver` and implements:
- `onAttest(attestation, value)`: mints `10 * 10^decimals()` to `attestation.attester` and returns `true`.
- `onRevoke(...)`: no-op, returns `true`.
- Constructor requires the global `IEAS` address. Deployment script `script/TheGuildActivityToken.s.sol` auto-selects the EAS address by `chainid` (Base/Optimism/Arbitrum/Polygon, plus testnets) or falls back to `EAS_ADDRESS` env var.
- To use it as a resolver, set this contract address as the `resolver` when registering your EAS Schema. When EAS processes an attestation for that schema, it will call `attest()` on the resolver, which delegates to `onAttest`.
- Learn more about EAS resolvers in the official docs: [Resolver Contracts](https://docs.attest.org/docs/core--concepts/resolver-contracts).

Quick steps:

1. Deploy TGA (resolver):
- Uses `IEAS` in constructor; see the deploy script for per-chain EAS addresses.
2. Register your schema in EAS with `resolver` set to the deployed TGA address.
3. Create attestations against that schema. Each attestation mints 10 TGA to the attester automatically.

Deploy:

```shell
forge script script/TheGuildActivityToken.s.sol:TheGuildActivityTokenScript \
--rpc-url <your_rpc_url> \
--private-key <your_private_key> \
--broadcast
```

### Cast

```shell
Expand Down
24 changes: 24 additions & 0 deletions the-guild-smart-contracts/foundry.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
{
"lib/eas-contracts": {
"tag": {
"name": "v1.4.0",
"rev": "d223e17208aa110dd5ec694d77324a2321d93201"
}
},
"lib/forge-std": {
"tag": {
"name": "v1.10.0",
"rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
}
},
"lib/openzeppelin-contracts": {
"tag": {
"name": "v5.4.0",
"rev": "c64a1edb67b6e3f4a15cca8909c9482ad33a02b0"
}
},
"lib/openzeppelin-contracts-upgradeable": {
"tag": {
"name": "v5.4.0",
"rev": "e725abddf1e01cf05ace496e950fc8e243cc7cab"
}
},
"lib/openzeppelin-foundry-upgrades": {
"tag": {
"name": "v0.4.0",
"rev": "cbce1e00305e943aa1661d43f41e5ac72c662b07"
}
}
}
12 changes: 12 additions & 0 deletions the-guild-smart-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,16 @@ src = "src"
out = "out"
libs = ["lib"]

# Deterministic deployments settings (see: https://getfoundry.sh/guides/deterministic-deployments-using-create2)
solc = "0.8.23"
evm_version = "cancun"
bytecode_hash = "none"
cbor_metadata = false
always_use_create_2_factory = true
remappings = [
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
'@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/',
'eas-contracts/=lib/eas-contracts/contracts/'
]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions the-guild-smart-contracts/lib/eas-contracts
Submodule eas-contracts added at d223e1
1 change: 1 addition & 0 deletions the-guild-smart-contracts/lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at c64a1e
69 changes: 69 additions & 0 deletions the-guild-smart-contracts/script/TheGuildActivityToken.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {IEAS} from "eas-contracts/IEAS.sol";
import {TheGuildActivityToken} from "../src/TheGuildActivityToken.sol";

contract TheGuildActivityTokenScript is Script {
function getEASAddress() internal view returns (address) {
// Base and Optimism chains use canonical predeploy
if (
block.chainid == 8453 ||
block.chainid == 84531 ||
block.chainid == 84532 ||
block.chainid == 10 ||
block.chainid == 11155420
) {
return 0x4200000000000000000000000000000000000021;
}
// Arbitrum Sepolia
if (block.chainid == 421614) {
return 0x2521021fc8BF070473E1e1801D3c7B4aB701E1dE;
}
// Polygon Amoy
if (block.chainid == 80002) {
return 0xb101275a60d8bfb14529C421899aD7CA1Ae5B5Fc;
}
// Linea Goerli
if (block.chainid == 59140) {
return 0xaEF4103A04090071165F78D45D83A0C0782c2B2a;
}
//Mainnet
if (block.chainid == 1) {
return 0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587;
}
//Sepolia
if (block.chainid == 11155111) {
return 0xC2679fBD37d54388Ce493F1DB75320D236e1815e;
}
//Arbitrum One
if (block.chainid == 42161) {
return 0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458;
}
//Polygon
if (block.chainid == 137) {
return 0x5E634ef5355f45A855d02D66eCD687b1502AF790;
}
// Fallback to env var for other networks
address fallbackAddr = vm.envOr("EAS_ADDRESS", address(0));
require(
fallbackAddr != address(0),
"EAS_ADDRESS not set for this network"
);
return fallbackAddr;
}

function run() public {
address eas;
// EAS addresses per https://github.com/ethereum-attestation-service/eas-contracts deployments
// Base mainnet (8453) and Base Goerli/Sepolia (84531/84532) use the canonical predeploy 0x...21
// Optimism mainnet (10) and OP Sepolia (11155420) also use canonical 0x...21

eas = getEASAddress();

vm.startBroadcast();
new TheGuildActivityToken(IEAS(eas));
vm.stopBroadcast();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol";

contract TheGuildBadgeRegistryScript is Script {
function run() public {
// Salt can be provided via env var or defaults to zero salt
bytes32 salt = bytes32(vm.envOr("CREATE2_SALT", uint256(0)));

vm.startBroadcast();
new TheGuildBadgeRegistry();
new TheGuildBadgeRegistry{salt: salt}();
vm.stopBroadcast();
}
}
41 changes: 41 additions & 0 deletions the-guild-smart-contracts/src/TheGuildActivityToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
import {SchemaResolver} from "eas-contracts/resolver/SchemaResolver.sol";
import {IEAS, Attestation} from "eas-contracts/IEAS.sol";

/// @title TheGuildActivityToken (TGA)
/// @notice ERC20 that also serves as an EAS schema resolver; mints on attest.
contract TheGuildActivityToken is ERC20, Ownable, SchemaResolver {
constructor(
IEAS eas
)
ERC20("TheGuildActivityToken", "TGA")
Ownable(msg.sender)
SchemaResolver(eas)
{}

/// @notice Mint tokens to a recipient. Only owner can mint.
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}

/// @inheritdoc SchemaResolver
function onAttest(
Attestation calldata attestation,
uint256
) internal override returns (bool) {
_mint(attestation.attester, 10 * (10 ** decimals()));
return true;
}

/// @inheritdoc SchemaResolver
function onRevoke(
Attestation calldata,
uint256
) internal pure override returns (bool) {
return true;
}
}
39 changes: 39 additions & 0 deletions the-guild-smart-contracts/test/TheGuildActivityToken.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {IEAS} from "eas-contracts/IEAS.sol";
import {TheGuildActivityToken} from "../src/TheGuildActivityToken.sol";

contract TheGuildActivityTokenTest is Test {
TheGuildActivityToken private token;

address private owner = address(this);
address private user = address(0xBEEF);

function setUp() public {
token = new TheGuildActivityToken(IEAS(address(0x1)));
}

function test_Metadata() public view {
assertEq(token.name(), "TheGuildActivityToken");
assertEq(token.symbol(), "TGA");
assertEq(token.decimals(), 18);
}

function test_OwnerIsDeployer() public view {
assertEq(token.owner(), owner);
}

function test_MintByOwner() public {
token.mint(user, 1e18);
assertEq(token.balanceOf(user), 1e18);
assertEq(token.totalSupply(), 1e18);
}

function test_RevertMintIfNotOwner() public {
vm.prank(user);
vm.expectRevert();
token.mint(user, 1e18);
}
}
Loading