diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b1bc09..0f126cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitmodules b/.gitmodules index 40000dd..92311bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/the-guild-smart-contracts/.env.example b/the-guild-smart-contracts/.env.example new file mode 100644 index 0000000..09c5f0d --- /dev/null +++ b/the-guild-smart-contracts/.env.example @@ -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= \ No newline at end of file diff --git a/the-guild-smart-contracts/README.md b/the-guild-smart-contracts/README.md index 9e55465..6283590 100644 --- a/the-guild-smart-contracts/README.md +++ b/the-guild-smart-contracts/README.md @@ -25,6 +25,14 @@ https://book.getfoundry.sh/ $ forge build ``` +### Dependencies + +OpenZeppelin contracts (for ERC20): + +```shell +forge install OpenZeppelin/openzeppelin-contracts +``` + ### Test ```shell @@ -55,6 +63,57 @@ $ anvil $ forge script script/Counter.s.sol:CounterScript --rpc-url --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 \ + --private-key \ + --broadcast + +# Use a specific salt value (hex string -> uint) +export CREATE2_SALT=123456 +forge script script/TheGuildBadgeRegistry.s.sol:TheGuildBadgeRegistryScript --rpc-url --private-key --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 \ + --private-key \ + --broadcast +``` + ### Cast ```shell diff --git a/the-guild-smart-contracts/foundry.lock b/the-guild-smart-contracts/foundry.lock index 5643642..6eb988d 100644 --- a/the-guild-smart-contracts/foundry.lock +++ b/the-guild-smart-contracts/foundry.lock @@ -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" + } } } \ No newline at end of file diff --git a/the-guild-smart-contracts/foundry.toml b/the-guild-smart-contracts/foundry.toml index 25b918f..0103a75 100644 --- a/the-guild-smart-contracts/foundry.toml +++ b/the-guild-smart-contracts/foundry.toml @@ -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 diff --git a/the-guild-smart-contracts/lib/eas-contracts b/the-guild-smart-contracts/lib/eas-contracts new file mode 160000 index 0000000..d223e17 --- /dev/null +++ b/the-guild-smart-contracts/lib/eas-contracts @@ -0,0 +1 @@ +Subproject commit d223e17208aa110dd5ec694d77324a2321d93201 diff --git a/the-guild-smart-contracts/lib/openzeppelin-contracts b/the-guild-smart-contracts/lib/openzeppelin-contracts new file mode 160000 index 0000000..c64a1ed --- /dev/null +++ b/the-guild-smart-contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit c64a1edb67b6e3f4a15cca8909c9482ad33a02b0 diff --git a/the-guild-smart-contracts/lib/openzeppelin-contracts-upgradeable b/the-guild-smart-contracts/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..e725abd --- /dev/null +++ b/the-guild-smart-contracts/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit e725abddf1e01cf05ace496e950fc8e243cc7cab diff --git a/the-guild-smart-contracts/lib/openzeppelin-foundry-upgrades b/the-guild-smart-contracts/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..cbce1e0 --- /dev/null +++ b/the-guild-smart-contracts/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit cbce1e00305e943aa1661d43f41e5ac72c662b07 diff --git a/the-guild-smart-contracts/script/TheGuildActivityToken.s.sol b/the-guild-smart-contracts/script/TheGuildActivityToken.s.sol new file mode 100644 index 0000000..3560dcf --- /dev/null +++ b/the-guild-smart-contracts/script/TheGuildActivityToken.s.sol @@ -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(); + } +} diff --git a/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol b/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol index aea97ac..abe6a44 100644 --- a/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol +++ b/the-guild-smart-contracts/script/TheGuildBadgeRegistry.s.sol @@ -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(); } } diff --git a/the-guild-smart-contracts/src/TheGuildActivityToken.sol b/the-guild-smart-contracts/src/TheGuildActivityToken.sol new file mode 100644 index 0000000..118a649 --- /dev/null +++ b/the-guild-smart-contracts/src/TheGuildActivityToken.sol @@ -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; + } +} diff --git a/the-guild-smart-contracts/test/TheGuildActivityToken.t.sol b/the-guild-smart-contracts/test/TheGuildActivityToken.t.sol new file mode 100644 index 0000000..9b67759 --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildActivityToken.t.sol @@ -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); + } +}