diff --git a/the-guild-smart-contracts/script/FullDeploymentScript.s.sol b/the-guild-smart-contracts/script/FullDeploymentScript.s.sol index 86f4baf..c666f42 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 {TheGuildAttestationResolver} from "../src/TheGuildAttestationResolver.sol"; +import {TheGuildInternalResolver} from "../src/TheGuildInternalResolver.sol"; import {TheGuildBadgeRegistry} from "../src/TheGuildBadgeRegistry.sol"; import {TheGuildBadgeRanking} from "../src/TheGuildBadgeRanking.sol"; import {EASUtils} from "./utils/EASUtils.s.sol"; @@ -50,23 +51,23 @@ contract FullDeploymentScript is Script { EASUtils.getSchemaRegistryAddress(vm) ); bytes32 schemaId = schemaRegistry.register(schema, resolver, true); - console.logString("Schema ID:"); + console.logString("Badge Attestation Schema ID:"); console.logBytes32(schemaId); - // Create some badges - badgeRegistry.createBadge( - bytes32("Rust"), - bytes("Know how to code in Rust") - ); - badgeRegistry.createBadge( - bytes32("Solidity"), - bytes("Know how to code in Solidity") - ); - badgeRegistry.createBadge( - bytes32("TypeScript"), - bytes("Know how to code in TypeScript") - ); + // Deploy Internal Resolver via CREATE2 + TheGuildInternalResolver internalResolver = new TheGuildInternalResolver{ + salt: salt + }(eas, deployer); + // Register Skill Badge Schema + string memory skillSchema = "string skillDescription, bytes32[] linkedBadges"; + bytes32 skillSchemaId = schemaRegistry.register( + skillSchema, + internalResolver, + true + ); + console.logString("Skill Badge Schema ID:"); + console.logBytes32(skillSchemaId); // Deploy or attach to existing badge ranking via CREATE2 new TheGuildBadgeRanking{salt: salt}(badgeRegistry); diff --git a/the-guild-smart-contracts/script/TheGuildInternalResolver.s.sol b/the-guild-smart-contracts/script/TheGuildInternalResolver.s.sol new file mode 100644 index 0000000..179262b --- /dev/null +++ b/the-guild-smart-contracts/script/TheGuildInternalResolver.s.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {SchemaResolver} from "eas-contracts/resolver/SchemaResolver.sol"; +import {IEAS, Attestation} from "eas-contracts/IEAS.sol"; +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; + +/// @title TheGuildInternalResolver +/// @notice EAS schema resolver that restricts attestations to authorized Guild accounts only. +contract TheGuildInternalResolver is SchemaResolver, Ownable { + mapping(address => bool) private _authorizedAttesters; + + event AttesterAuthorized(address indexed attester, bool authorized); + + constructor(IEAS eas, address initialOwner) SchemaResolver(eas) Ownable(initialOwner) { + _authorizedAttesters[initialOwner] = true; + emit AttesterAuthorized(initialOwner, true); + } + + /// @notice Authorize or deauthorize an account to create attestations. + function setAuthorizedAttester(address attester, bool authorized) external onlyOwner { + _authorizedAttesters[attester] = authorized; + emit AttesterAuthorized(attester, authorized); + } + + /// @notice Check if an account is an authorized attester. + function isAuthorizedAttester(address attester) public view returns (bool) { + return _authorizedAttesters[attester]; + } + + /// @inheritdoc SchemaResolver + function onAttest( + Attestation calldata attestation, + uint256 + ) internal view override returns (bool) { + return _authorizedAttesters[attestation.attester]; + } + + /// @inheritdoc SchemaResolver + function onRevoke( + Attestation calldata attestation, + uint256 + ) internal view override returns (bool) { + return _authorizedAttesters[attestation.attester]; + } +} diff --git a/the-guild-smart-contracts/src/TheGuildInternalResolver.sol b/the-guild-smart-contracts/src/TheGuildInternalResolver.sol new file mode 100644 index 0000000..179262b --- /dev/null +++ b/the-guild-smart-contracts/src/TheGuildInternalResolver.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {SchemaResolver} from "eas-contracts/resolver/SchemaResolver.sol"; +import {IEAS, Attestation} from "eas-contracts/IEAS.sol"; +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; + +/// @title TheGuildInternalResolver +/// @notice EAS schema resolver that restricts attestations to authorized Guild accounts only. +contract TheGuildInternalResolver is SchemaResolver, Ownable { + mapping(address => bool) private _authorizedAttesters; + + event AttesterAuthorized(address indexed attester, bool authorized); + + constructor(IEAS eas, address initialOwner) SchemaResolver(eas) Ownable(initialOwner) { + _authorizedAttesters[initialOwner] = true; + emit AttesterAuthorized(initialOwner, true); + } + + /// @notice Authorize or deauthorize an account to create attestations. + function setAuthorizedAttester(address attester, bool authorized) external onlyOwner { + _authorizedAttesters[attester] = authorized; + emit AttesterAuthorized(attester, authorized); + } + + /// @notice Check if an account is an authorized attester. + function isAuthorizedAttester(address attester) public view returns (bool) { + return _authorizedAttesters[attester]; + } + + /// @inheritdoc SchemaResolver + function onAttest( + Attestation calldata attestation, + uint256 + ) internal view override returns (bool) { + return _authorizedAttesters[attestation.attester]; + } + + /// @inheritdoc SchemaResolver + function onRevoke( + Attestation calldata attestation, + uint256 + ) internal view override returns (bool) { + return _authorizedAttesters[attestation.attester]; + } +} diff --git a/the-guild-smart-contracts/test/TheGuildInternalResolver.t.sol b/the-guild-smart-contracts/test/TheGuildInternalResolver.t.sol new file mode 100644 index 0000000..24bdf9a --- /dev/null +++ b/the-guild-smart-contracts/test/TheGuildInternalResolver.t.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {TheGuildInternalResolver} from "../src/TheGuildInternalResolver.sol"; + + +import {EAS} from "eas-contracts/EAS.sol"; +import {SchemaRegistry} from "eas-contracts/SchemaRegistry.sol"; +import {IEAS, AttestationRequest, AttestationRequestData, Attestation, RevocationRequestData, RevocationRequest} from "eas-contracts/IEAS.sol"; + +contract TheGuildInternalResolverTest is Test { + TheGuildInternalResolver private resolver; + SchemaRegistry private schemaRegistry; + EAS private eas; + + address private owner = address(this); + address private authorizedAttester = address(0xA11CE); + address private unauthorizedAttester = address(0xBEEF); + address private recipient = address(0xCAFE); + + function setUp() public { + + schemaRegistry = new SchemaRegistry(); + eas = new EAS(schemaRegistry); + + + resolver = new TheGuildInternalResolver( + IEAS(address(eas)), + owner + ); + } + + function _registerSchema() internal returns (bytes32) { + + string memory schema = "string skillDescription, bytes32[] linkedBadges"; + + bytes32 schemaId = schemaRegistry.register(schema, resolver, true); + return schemaId; + } + + function test_AttestationByOwnerSucceeds() public { + bytes32 schemaId = _registerSchema(); + + bytes32[] memory linkedBadges = new bytes32[](2); + linkedBadges[0] = bytes32("Solidity"); + linkedBadges[1] = bytes32("Rust"); + + AttestationRequestData memory data = AttestationRequestData({ + recipient: recipient, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("Master of Smart Contracts", linkedBadges), + value: 0 + }); + + AttestationRequest memory request = AttestationRequest({ + schema: schemaId, + data: data + }); + + + eas.attest(request); + + } + + function test_AttestationByAuthorizedAttesterSucceeds() public { + bytes32 schemaId = _registerSchema(); + resolver.setAuthorizedAttester(authorizedAttester, true); + + bytes32[] memory linkedBadges = new bytes32[](1); + linkedBadges[0] = bytes32("Solidity"); + + AttestationRequestData memory data = AttestationRequestData({ + recipient: recipient, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("Solidity Dev", linkedBadges), + value: 0 + }); + + AttestationRequest memory request = AttestationRequest({ + schema: schemaId, + data: data + }); + + vm.prank(authorizedAttester); + eas.attest(request); + + } + + function test_AttestationByUnauthorizedAttesterFails() public { + bytes32 schemaId = _registerSchema(); + + bytes32[] memory linkedBadges = new bytes32[](1); + linkedBadges[0] = bytes32("Solidity"); + + AttestationRequestData memory data = AttestationRequestData({ + recipient: recipient, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("Solidity Dev", linkedBadges), + value: 0 + }); + + AttestationRequest memory request = AttestationRequest({ + schema: schemaId, + data: data + }); + + vm.prank(unauthorizedAttester); + vm.expectRevert(); + eas.attest(request); + } + + function test_RevocationByAuthorizedAttesterSucceeds() public { + bytes32 schemaId = _registerSchema(); + resolver.setAuthorizedAttester(authorizedAttester, true); + + bytes32[] memory linkedBadges = new bytes32[](1); + linkedBadges[0] = bytes32("Solidity"); + + AttestationRequestData memory data = AttestationRequestData({ + recipient: recipient, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("Solidity Dev", linkedBadges), + value: 0 + }); + + AttestationRequest memory request = AttestationRequest({ + schema: schemaId, + data: data + }); + + vm.prank(authorizedAttester); + bytes32 uid = eas.attest(request); + + vm.prank(authorizedAttester); + eas.revoke( + RevocationRequest({ + schema: schemaId, + data: RevocationRequestData({uid: uid, value: 0}) + }) + ); + // success == no revert + } + + function test_RevocationByUnauthorizedAttesterFails() public { + bytes32 schemaId = _registerSchema(); + resolver.setAuthorizedAttester(authorizedAttester, true); + + bytes32[] memory linkedBadges = new bytes32[](1); + linkedBadges[0] = bytes32("Solidity"); + + AttestationRequestData memory data = AttestationRequestData({ + recipient: recipient, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("Solidity Dev", linkedBadges), + value: 0 + }); + + AttestationRequest memory request = AttestationRequest({ + schema: schemaId, + data: data + }); + + vm.prank(authorizedAttester); + bytes32 uid = eas.attest(request); + + vm.prank(unauthorizedAttester); + vm.expectRevert(); + eas.revoke( + RevocationRequest({ + schema: schemaId, + data: RevocationRequestData({uid: uid, value: 0}) + }) + ); + } + + function test_DeauthorizeAttester() public { + bytes32 schemaId = _registerSchema(); + resolver.setAuthorizedAttester(authorizedAttester, true); + assertTrue(resolver.isAuthorizedAttester(authorizedAttester)); + + resolver.setAuthorizedAttester(authorizedAttester, false); + assertFalse(resolver.isAuthorizedAttester(authorizedAttester)); + + bytes32[] memory linkedBadges = new bytes32[](1); + linkedBadges[0] = bytes32("Solidity"); + + AttestationRequest memory request = AttestationRequest({ + schema: schemaId, + data: AttestationRequestData({ + recipient: recipient, + expirationTime: 0, + revocable: true, + refUID: bytes32(0), + data: abi.encode("Solidity Dev", linkedBadges), + value: 0 + }) + }); + + vm.prank(authorizedAttester); + vm.expectRevert(); + eas.attest(request); + } +}