diff --git a/examples/.env.example b/examples/.env.example index c5ebc6e..b2370e7 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -2,11 +2,11 @@ RPC_ENDPOINT='http://127.0.0.1:8545' EAS_MAIN_CONTRACT_ADDRESS='0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' -SCROLL_BADGE_SCHEMA_UID='0xd2c8c891990e52369c94dac301cd6090c885606457fd489ed59343402b1aa7e5' +SCROLL_BADGE_SCHEMA_UID='0x81b69c8f7b364e9f7d8be9c19525df9ec003487dcd39ef647cb1a2f7a241bc08' SCROLL_BADGE_SCHEMA='address badge, bytes payload' -SIMPLE_BADGE_CONTRACT_ADDRESS='0x610178dA211FEF7D417bC0e6FeD39F05609AD788' -SIMPLE_BADGE_ATTESTER_PROXY_CONTRACT_ADDRESS='0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e' +SIMPLE_BADGE_CONTRACT_ADDRESS='0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' +SIMPLE_BADGE_ATTESTER_PROXY_CONTRACT_ADDRESS='0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82' SCROLL_PROFILE_REGISTRY_PROXY_CONTRACT_ADDRESS='0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' diff --git a/examples/src/attest-server.js b/examples/src/attest-server.js index 871f307..28cb752 100644 --- a/examples/src/attest-server.js +++ b/examples/src/attest-server.js @@ -12,7 +12,7 @@ const provider = new ethers.JsonRpcProvider(process.env.RPC_ENDPOINT); const signer = (new ethers.Wallet(process.env.SIGNER_PRIVATE_KEY)).connect(provider); // example query: -// curl 'localhost:3000/api/badge/0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9/claim?recipient=0x0000000000000000000000000000000000000001' +// curl 'localhost:3000/api/badge/0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0/claim?recipient=0x0000000000000000000000000000000000000001' app.get('/api/badge/:address/claim', async (req, res) => { const { recipient } = req.query; const { address } = req.params; diff --git a/examples/src/attest-simple.js b/examples/src/attest-simple.js index 810f948..038d270 100644 --- a/examples/src/attest-simple.js +++ b/examples/src/attest-simple.js @@ -7,7 +7,8 @@ import { createBadge } from './lib.js'; import 'dotenv/config'; const abi = [ - 'error SingletonBadge()' + 'error SingletonBadge()', + 'error Unauthorized()' ] async function main() { diff --git a/examples/test-env.sh b/examples/test-env.sh index 438a7e7..7d4366c 100755 --- a/examples/test-env.sh +++ b/examples/test-env.sh @@ -17,6 +17,6 @@ export SIGNER_ADDRESS=$(cast wallet address "$SIGNER_PRIVATE_KEY") export TREASURY_ADDRESS=$(cast wallet address "$SIGNER_PRIVATE_KEY") pushd .. -forge script script/DeployTestContracts.sol:DeployTestContracts --rpc-url http://localhost:8545 --broadcast 2>&1 +forge script script/DeployTestContracts.sol:DeployTestContracts --rpc-url http://127.0.0.1:8545 --broadcast 2>&1 fg diff --git a/script/DeployCanvasContracts.sol b/script/DeployCanvasContracts.sol new file mode 100644 index 0000000..b86df1e --- /dev/null +++ b/script/DeployCanvasContracts.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +import {EAS} from "@eas/contracts/EAS.sol"; + +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {EmptyContract} from "../src/misc/EmptyContract.sol"; +import {Profile} from "../src/profile/Profile.sol"; +import {ProfileRegistry} from "../src/profile/ProfileRegistry.sol"; +import {ScrollBadgeResolver} from "../src/resolver/ScrollBadgeResolver.sol"; + +contract DeployCanvasContracts is Script { + uint256 DEPLOYER_PRIVATE_KEY = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + address SIGNER_ADDRESS = vm.envAddress("SIGNER_ADDRESS"); + address TREASURY_ADDRESS = vm.envAddress("TREASURY_ADDRESS"); + + address EAS_ADDRESS = vm.envAddress("EAS_ADDRESS"); + + function run() external { + vm.startBroadcast(DEPLOYER_PRIVATE_KEY); + + // deploy proxy admin + ProxyAdmin proxyAdmin = new ProxyAdmin(); + + // deploy profile registry placeholder + address placeholder = address(new EmptyContract()); + address profileRegistryProxy = address(new TransparentUpgradeableProxy(placeholder, address(proxyAdmin), "")); + + // deploy Scroll badge resolver + address resolverImpl = address(new ScrollBadgeResolver(EAS_ADDRESS, profileRegistryProxy)); + address resolverProxy = address(new TransparentUpgradeableProxy(resolverImpl, address(proxyAdmin), "")); + ScrollBadgeResolver resolver = ScrollBadgeResolver(payable(resolverProxy)); + resolver.initialize(); + + bytes32 schema = resolver.schema(); + + // deploy profile implementation and upgrade registry + Profile profileImpl = new Profile(address(resolver)); + ProfileRegistry profileRegistryImpl = new ProfileRegistry(); + proxyAdmin.upgrade(ITransparentUpgradeableProxy(profileRegistryProxy), address(profileRegistryImpl)); + ProfileRegistry(profileRegistryProxy).initialize(TREASURY_ADDRESS, SIGNER_ADDRESS, address(profileImpl)); + + // misc + bytes32[] memory blacklist = new bytes32[](1); + blacklist[0] = keccak256(bytes("vpn")); + ProfileRegistry(profileRegistryProxy).blacklistUsername(blacklist); + + ProfileRegistry(profileRegistryProxy).updateSigner(0x70997970C51812dc3A010C7d01b50e0d17dc79C8); + + // log addresses + logAddress("DEPLOYER_ADDRESS", vm.addr(DEPLOYER_PRIVATE_KEY)); + logAddress("SIGNER_ADDRESS", SIGNER_ADDRESS); + logAddress("TREASURY_ADDRESS", TREASURY_ADDRESS); + logAddress("EAS_ADDRESS", EAS_ADDRESS); + logAddress("SCROLL_PROFILE_REGISTRY_PROXY_ADMIN_ADDRESS", address(proxyAdmin)); + logAddress("SCROLL_PROFILE_REGISTRY_PROXY_CONTRACT_ADDRESS", address(profileRegistryProxy)); + logAddress("SCROLL_BADGE_RESOLVER_CONTRACT_ADDRESS", address(resolver)); + logBytes32("SCROLL_BADGE_SCHEMA_UID", schema); + logAddress("SCROLL_PROFILE_IMPLEMENTATION_CONTRACT_ADDRESS", address(profileImpl)); + logAddress("SCROLL_PROFILE_REGISTRY_IMPLEMENTATION_CONTRACT_ADDRESS", address(profileRegistryImpl)); + + vm.stopBroadcast(); + } + + function logAddress(string memory name, address addr) internal view { + console.log(string(abi.encodePacked(name, "=", vm.toString(address(addr))))); + } + + function logBytes32(string memory name, bytes32 data) internal view { + console.log(string(abi.encodePacked(name, "=", vm.toString(data)))); + } +} diff --git a/script/DeployCanvasTestBadgeContracts.sol b/script/DeployCanvasTestBadgeContracts.sol new file mode 100644 index 0000000..9b815f3 --- /dev/null +++ b/script/DeployCanvasTestBadgeContracts.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +import {Attestation} from "@eas/contracts/IEAS.sol"; + +import {ScrollBadge} from "../src/badge/ScrollBadge.sol"; +import {EthereumYearBadge} from "../src/badge/examples/EthereumYearBadge.sol"; +import {ScrollBadgeTokenOwner} from "../src/badge/examples/ScrollBadgeTokenOwner.sol"; +import {ScrollBadgeSelfAttest} from "../src/badge/extensions/ScrollBadgeSelfAttest.sol"; +import {ScrollBadgeSingleton} from "../src/badge/extensions/ScrollBadgeSingleton.sol"; +import {ScrollBadgeResolver} from "../src/resolver/ScrollBadgeResolver.sol"; + +contract CanvasTestBadge is ScrollBadgeSelfAttest, ScrollBadgeSingleton { + string public sharedTokenURI; + + constructor(address resolver_, string memory tokenUri_) ScrollBadge(resolver_) { + sharedTokenURI = tokenUri_; + } + + function onIssueBadge(Attestation calldata attestation) + internal + virtual + override (ScrollBadgeSelfAttest, ScrollBadgeSingleton) + returns (bool) + { + return super.onIssueBadge(attestation); + } + + function onRevokeBadge(Attestation calldata attestation) + internal + virtual + override (ScrollBadgeSelfAttest, ScrollBadgeSingleton) + returns (bool) + { + return super.onRevokeBadge(attestation); + } + + function badgeTokenURI(bytes32 /*uid*/ ) public view override returns (string memory) { + return sharedTokenURI; + } +} + +contract DeployCanvasTestBadgeContracts is Script { + uint256 DEPLOYER_PRIVATE_KEY = vm.envUint("DEPLOYER_PRIVATE_KEY"); + + address RESOLVER_ADDRESS = vm.envAddress("SCROLL_BADGE_RESOLVER_CONTRACT_ADDRESS"); + + function run() external { + vm.startBroadcast(DEPLOYER_PRIVATE_KEY); + + ScrollBadgeResolver resolver = ScrollBadgeResolver(payable(RESOLVER_ADDRESS)); + + // deploy test badges + CanvasTestBadge badge1 = new CanvasTestBadge( + address(resolver), "ipfs://bafybeibc5sgo2plmjkq2tzmhrn54bk3crhnc23zd2msg4ea7a4pxrkgfna/1" + ); + + CanvasTestBadge badge2 = new CanvasTestBadge( + address(resolver), "ipfs://bafybeibc5sgo2plmjkq2tzmhrn54bk3crhnc23zd2msg4ea7a4pxrkgfna/2" + ); + + CanvasTestBadge badge3 = new CanvasTestBadge( + address(resolver), "ipfs://bafybeibc5sgo2plmjkq2tzmhrn54bk3crhnc23zd2msg4ea7a4pxrkgfna/3" + ); + + // deploy origins NFT badge + address[] memory tokens = new address[](1); + tokens[0] = 0xDd7d857F570B0C211abfe05cd914A85BefEC2464; + + ScrollBadgeTokenOwner badge4 = new ScrollBadgeTokenOwner(address(resolver), tokens); + + // deploy Ethereum year badge + EthereumYearBadge badge5 = new EthereumYearBadge(address(resolver), "https://nft.scroll.io/canvas/year/"); + + // set permissions + resolver.toggleBadge(address(badge1), true); + resolver.toggleBadge(address(badge2), true); + resolver.toggleBadge(address(badge3), true); + resolver.toggleBadge(address(badge4), true); + resolver.toggleBadge(address(badge5), true); + + // log addresses + logAddress("DEPLOYER_ADDRESS", vm.addr(DEPLOYER_PRIVATE_KEY)); + logAddress("SIMPLE_BADGE_A_CONTRACT_ADDRESS", address(badge1)); + logAddress("SIMPLE_BADGE_B_CONTRACT_ADDRESS", address(badge2)); + logAddress("SIMPLE_BADGE_C_CONTRACT_ADDRESS", address(badge3)); + logAddress("ORIGINS_BADGE_ADDRESS", address(badge4)); + logAddress("ETHEREUM_YEAR_BADGE_ADDRESS", address(badge5)); + + vm.stopBroadcast(); + } + + function logAddress(string memory name, address addr) internal view { + console.log(string(abi.encodePacked(name, "=", vm.toString(address(addr))))); + } +} diff --git a/script/DeployTestContracts.sol b/script/DeployTestContracts.sol index 141817c..a70fc6a 100644 --- a/script/DeployTestContracts.sol +++ b/script/DeployTestContracts.sol @@ -38,12 +38,15 @@ contract DeployTestContracts is Script { ProxyAdmin proxyAdmin = new ProxyAdmin(); // deploy profile registry placeholder - EmptyContract placeholder = new EmptyContract(); - address profileRegistryProxy = - address(new TransparentUpgradeableProxy(address(placeholder), address(proxyAdmin), "")); + address placeholder = address(new EmptyContract()); + address profileRegistryProxy = address(new TransparentUpgradeableProxy(placeholder, address(proxyAdmin), "")); // deploy Scroll badge resolver - ScrollBadgeResolver resolver = new ScrollBadgeResolver(address(eas), profileRegistryProxy); + address resolverImpl = address(new ScrollBadgeResolver(address(eas), profileRegistryProxy)); + address resolverProxy = address(new TransparentUpgradeableProxy(resolverImpl, address(proxyAdmin), "")); + ScrollBadgeResolver resolver = ScrollBadgeResolver(payable(resolverProxy)); + resolver.initialize(); + bytes32 schema = resolver.schema(); // deploy profile implementation and upgrade registry diff --git a/src/badge/examples/EthereumYearBadge.sol b/src/badge/examples/EthereumYearBadge.sol index f9f0f5f..ec04124 100644 --- a/src/badge/examples/EthereumYearBadge.sol +++ b/src/badge/examples/EthereumYearBadge.sol @@ -73,7 +73,7 @@ contract EthereumYearBadge is bytes memory payload = getPayload(attestation); uint256 year = decodePayloadData(payload); - return string(abi.encodePacked(baseTokenURI, Strings.toString(year))); + return string(abi.encodePacked(baseTokenURI, Strings.toString(year), ".json")); } /// @inheritdoc ScrollBadgeCustomPayload diff --git a/src/resolver/ScrollBadgeResolver.sol b/src/resolver/ScrollBadgeResolver.sol index 39b3402..e80306b 100644 --- a/src/resolver/ScrollBadgeResolver.sol +++ b/src/resolver/ScrollBadgeResolver.sol @@ -38,10 +38,19 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg */ /// @inheritdoc IScrollBadgeResolver - bytes32 public immutable schema; + address public immutable registry; + + /** + * + * Variables * + * + */ /// @inheritdoc IScrollBadgeResolver - address public immutable registry; + bytes32 public schema; + + // Storage slots reserved for future upgrades. + uint256[49] private __gap; /** * @@ -53,15 +62,20 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg /// @param eas_ The address of the global EAS contract. /// @param registry_ The address of the profile registry contract. constructor(address eas_, address registry_) SchemaResolver(IEAS(eas_)) { + registry = registry_; + _disableInitializers(); + } + + function initialize() external initializer { + __Whitelist_init(); + // register Scroll badge schema, // we do this here to ensure that the resolver is correctly configured - schema = IEAS(eas_).getSchemaRegistry().register( + schema = _eas.getSchemaRegistry().register( SCROLL_BADGE_SCHEMA, ISchemaResolver(address(this)), // resolver true // revocable ); - - registry = registry_; } /** diff --git a/src/resolver/ScrollBadgeResolverWhitelist.sol b/src/resolver/ScrollBadgeResolverWhitelist.sol index e6f0913..47cdd0c 100644 --- a/src/resolver/ScrollBadgeResolverWhitelist.sol +++ b/src/resolver/ScrollBadgeResolverWhitelist.sol @@ -2,15 +2,44 @@ pragma solidity 0.8.19; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +abstract contract ScrollBadgeResolverWhitelist is OwnableUpgradeable { + /** + * + * Variables * + * + */ -abstract contract ScrollBadgeResolverWhitelist is Ownable { // If false, all badges are allowed. - bool public whitelistEnabled = true; + bool public whitelistEnabled; // Authorized badge contracts. mapping(address => bool) public whitelist; + // Storage slots reserved for future upgrades. + uint256[48] private __gap; + + /** + * + * Constructor * + * + */ + constructor() { + _disableInitializers(); + } + + function __Whitelist_init() internal onlyInitializing { + __Ownable_init(); + whitelistEnabled = true; + } + + /** + * + * Restricted Functions * + * + */ + /// @notice Enables or disables a given badge contract. /// @param badge The badge address. /// @param enable True if enable, false if disable. diff --git a/test/EthereumYearBadge.t.sol b/test/EthereumYearBadge.t.sol new file mode 100644 index 0000000..2f426d1 --- /dev/null +++ b/test/EthereumYearBadge.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {ScrollBadgeTestBase} from "./ScrollBadgeTestBase.sol"; + +import {EMPTY_UID, NO_EXPIRATION_TIME} from "@eas/contracts/Common.sol"; +import {AttestationRequest, AttestationRequestData} from "@eas/contracts/IEAS.sol"; + +import {EthereumYearBadge} from "../src/badge/examples/EthereumYearBadge.sol"; + +contract EthereumYearBadgeTest is ScrollBadgeTestBase { + EthereumYearBadge internal badge; + + string baseTokenURI = "http://scroll-canvas.io/"; + + function setUp() public virtual override { + super.setUp(); + + badge = new EthereumYearBadge(address(resolver), baseTokenURI); + resolver.toggleBadge(address(badge), true); + badge.toggleAttester(address(this), true); + } + + function testAttestOnce(address recipient) external { + bytes memory payload = abi.encode(2024); + bytes memory attestationData = abi.encode(badge, payload); + + AttestationRequestData memory _attData = AttestationRequestData({ + recipient: recipient, + expirationTime: NO_EXPIRATION_TIME, + revocable: false, + refUID: EMPTY_UID, + data: attestationData, + value: 0 + }); + + AttestationRequest memory _req = AttestationRequest({schema: schema, data: _attData}); + bytes32 uid = eas.attest(_req); + + string memory uri = badge.badgeTokenURI(uid); + assertEq(uri, "http://scroll-canvas.io/2024.json"); + } +} diff --git a/test/Profile.t.sol b/test/Profile.t.sol index 71350d7..1a96f7d 100644 --- a/test/Profile.t.sol +++ b/test/Profile.t.sol @@ -79,7 +79,12 @@ contract ProfileRegistryTest is Test { eas = new EAS(schemaRegistry); address profileRegistryProxy = address(new TransparentUpgradeableProxy(address(new EmptyContract()), PROXY_ADMIN_ADDRESS, "")); - resolver = new ScrollBadgeResolver(address(eas), profileRegistryProxy); + + address resolverImpl = address(new ScrollBadgeResolver(address(eas), profileRegistryProxy)); + address resolverProxy = address(new TransparentUpgradeableProxy(resolverImpl, PROXY_ADMIN_ADDRESS, "")); + resolver = ScrollBadgeResolver(payable(resolverProxy)); + resolver.initialize(); + badge = new TestBadge(address(resolver)); resolver.toggleBadge(address(badge), true); diff --git a/test/ProfileRegistry.t.sol b/test/ProfileRegistry.t.sol index 78ac145..b59c584 100644 --- a/test/ProfileRegistry.t.sol +++ b/test/ProfileRegistry.t.sol @@ -49,7 +49,11 @@ contract ProfileRegistryTest is Test { eas = new EAS(schemaRegistry); address profileRegistryProxy = address(new TransparentUpgradeableProxy(address(new EmptyContract()), PROXY_ADMIN_ADDRESS, "")); - resolver = new ScrollBadgeResolver(address(eas), profileRegistryProxy); + + address resolverImpl = address(new ScrollBadgeResolver(address(eas), profileRegistryProxy)); + address resolverProxy = address(new TransparentUpgradeableProxy(resolverImpl, PROXY_ADMIN_ADDRESS, "")); + resolver = ScrollBadgeResolver(payable(resolverProxy)); + resolver.initialize(); signer = vm.createWallet(10_001); diff --git a/test/ScrollBadgeTestBase.sol b/test/ScrollBadgeTestBase.sol index f61e450..a82359f 100644 --- a/test/ScrollBadgeTestBase.sol +++ b/test/ScrollBadgeTestBase.sol @@ -17,6 +17,8 @@ import { RevocationRequestData } from "@eas/contracts/IEAS.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + import {ScrollBadgeResolver} from "../src/resolver/ScrollBadgeResolver.sol"; import {ProfileRegistry} from "../src/profile/ProfileRegistry.sol"; @@ -30,6 +32,8 @@ contract ScrollBadgeTestBase is Test { address internal constant alice = address(1); address internal constant bob = address(2); + address private constant PROXY_ADMIN_ADDRESS = 0x2000000000000000000000000000000000000000; + function setUp() public virtual { // EAS infra registry = new SchemaRegistry(); @@ -39,7 +43,12 @@ contract ScrollBadgeTestBase is Test { // no need to initialize the registry, since resolver // only uses it to see if a profile has been minted or not. address profileRegistry = address(new ProfileRegistry()); - resolver = new ScrollBadgeResolver(address(eas), profileRegistry); + + address resolverImpl = address(new ScrollBadgeResolver(address(eas), profileRegistry)); + address resolverProxy = address(new TransparentUpgradeableProxy(resolverImpl, PROXY_ADMIN_ADDRESS, "")); + resolver = ScrollBadgeResolver(payable(resolverProxy)); + resolver.initialize(); + schema = resolver.schema(); }