diff --git a/src/Common.sol b/src/Common.sol index a6c5d05..fe5233c 100644 --- a/src/Common.sol +++ b/src/Common.sol @@ -9,3 +9,7 @@ string constant SCROLL_BADGE_SCHEMA = "address badge, bytes payload"; function decodeBadgeData(bytes memory data) pure returns (address, bytes memory) { return abi.decode(data, (address, bytes)); } + +function encodeBadgeData(address badge, bytes memory payload) pure returns (bytes memory) { + return abi.encode(badge, payload); +} \ No newline at end of file diff --git a/src/badge/examples/SCRHoldingBadge.sol b/src/badge/examples/SCRHoldingBadge.sol new file mode 100644 index 0000000..9fe04a0 --- /dev/null +++ b/src/badge/examples/SCRHoldingBadge.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Attestation} from "@eas/contracts/IEAS.sol"; +import {NO_EXPIRATION_TIME} from "@eas/contracts/Common.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +import {IScrollBadgeResolver} from "../../interfaces/IScrollBadgeResolver.sol"; +import {IScrollBadge, IScrollSelfAttestationBadge} from "../../interfaces/IScrollSelfAttestationBadge.sol"; +import {encodeBadgeData} from "../../Common.sol"; +import {ScrollBadge} from "../ScrollBadge.sol"; +import {ScrollBadgeCustomPayload} from "../extensions/ScrollBadgeCustomPayload.sol"; +import {ScrollBadgeDefaultURI} from "../extensions/ScrollBadgeDefaultURI.sol"; + +string constant SCR_HOLDING_BADGE_SCHEMA = "uint256 level"; + +function decodePayloadData(bytes memory data) pure returns (uint256) { + return abi.decode(data, (uint256)); +} + +/// @title SCRHoldingBadge +/// @notice A badge that represents user's SCR holding amount. +contract SCRHoldingBadge is ScrollBadgeCustomPayload, ScrollBadgeDefaultURI, Ownable, IScrollSelfAttestationBadge { + uint256 private constant LEVEL_ONE_SCR_AMOUNT = 1 ether; + uint256 private constant LEVEL_TWO_SCR_AMOUNT = 10 ether; + uint256 private constant LEVEL_THREE_SCR_AMOUNT = 100 ether; + uint256 private constant LEVEL_FOUR_SCR_AMOUNT = 1000 ether; + uint256 private constant LEVEL_FIVE_SCR_AMOUNT = 10000 ether; + uint256 private constant LEVEL_SIX_SCR_AMOUNT = 100000 ether; + + /// @notice The address of SCR token. + address public immutable scr; + + constructor( + address resolver_, + string memory baseTokenURI_, + address scr_ + ) ScrollBadge(resolver_) ScrollBadgeDefaultURI(baseTokenURI_) { + scr = scr_; + } + + /// @notice Update the base token URI. + /// @param baseTokenURI_ The new base token URI. + function updateBaseTokenURI(string memory baseTokenURI_) external onlyOwner { + defaultBadgeURI = baseTokenURI_; + } + + /// @inheritdoc ScrollBadge + function onIssueBadge( + Attestation calldata + ) internal virtual override(ScrollBadge, ScrollBadgeCustomPayload) returns (bool) { + return false; + } + + /// @inheritdoc ScrollBadge + function onRevokeBadge( + Attestation calldata + ) internal virtual override(ScrollBadge, ScrollBadgeCustomPayload) returns (bool) { + return false; + } + + /// @inheritdoc ScrollBadge + function badgeTokenURI( + bytes32 uid + ) public view override(IScrollBadge, ScrollBadge, ScrollBadgeDefaultURI) returns (string memory) { + return ScrollBadgeDefaultURI.badgeTokenURI(uid); + } + + /// @inheritdoc ScrollBadgeDefaultURI + function getBadgeTokenURI(bytes32 uid) internal view override returns (string memory) { + Attestation memory attestation = getAndValidateBadge(uid); + bytes memory payload = getPayload(attestation); + uint256 year = decodePayloadData(payload); + + return string(abi.encodePacked(defaultBadgeURI, Strings.toString(year), ".json")); + } + + /// @inheritdoc ScrollBadgeCustomPayload + function getSchema() public pure override returns (string memory) { + return SCR_HOLDING_BADGE_SCHEMA; + } + + /// @inheritdoc IScrollSelfAttestationBadge + function getBadgeId() external pure returns (uint256) { + return 0; + } + + /// @inheritdoc IScrollSelfAttestationBadge + /// + /// @dev The uid encoding should be + /// ```text + /// [ address | badge id | customized data ] + /// [ 160 bits | 32 bits | 64 bits ] + /// [LSB MSB] + /// ``` + /// The *badge id* and the *customized data* should both be zero. + function getAttestation(bytes32 uid) external view override returns (Attestation memory attestation) { + // invalid uid, return empty badge + if ((uint256(uid) >> 160) > 0) return attestation; + + // extract badge recipient from uid + address recipient; + assembly { + recipient := and(uid, 0xffffffffffffffffffffffffffffffffffffffff) + } + + // compute payload + uint256 level; + uint256 balance = IERC20(scr).balanceOf(recipient); + // not hold enough SCR, return empty badge + if (balance < LEVEL_ONE_SCR_AMOUNT) return attestation; + else if (balance < LEVEL_TWO_SCR_AMOUNT) level = 1; + else if (balance < LEVEL_THREE_SCR_AMOUNT) level = 2; + else if (balance < LEVEL_FOUR_SCR_AMOUNT) level = 3; + else if (balance < LEVEL_FIVE_SCR_AMOUNT) level = 4; + else if (balance < LEVEL_SIX_SCR_AMOUNT) level = 5; + else level = 6; + bytes memory payload = abi.encode(level); + + // fill data in Attestation + attestation.uid = uid; + attestation.schema = IScrollBadgeResolver(resolver).schema(); + attestation.time = uint64(block.timestamp); + attestation.expirationTime = NO_EXPIRATION_TIME; + attestation.refUID = bytes32(0); + attestation.recipient = recipient; + attestation.attester = address(this); + attestation.revocable = false; + attestation.data = encodeBadgeData(address(this), payload); + + return attestation; + } +} diff --git a/src/badge/extensions/ScrollBadgeDefaultURI.sol b/src/badge/extensions/ScrollBadgeDefaultURI.sol index c6a2c3b..1bcf29d 100644 --- a/src/badge/extensions/ScrollBadgeDefaultURI.sol +++ b/src/badge/extensions/ScrollBadgeDefaultURI.sol @@ -14,7 +14,7 @@ abstract contract ScrollBadgeDefaultURI is ScrollBadge { } /// @inheritdoc ScrollBadge - function badgeTokenURI(bytes32 uid) public view override returns (string memory) { + function badgeTokenURI(bytes32 uid) public view override virtual returns (string memory) { if (uid == bytes32(0)) { return defaultBadgeURI; } diff --git a/src/interfaces/IScrollBadgeResolver.sol b/src/interfaces/IScrollBadgeResolver.sol index b87164a..0dd23e4 100644 --- a/src/interfaces/IScrollBadgeResolver.sol +++ b/src/interfaces/IScrollBadgeResolver.sol @@ -32,15 +32,15 @@ interface IScrollBadgeResolver { /// @notice Return the Scroll badge attestation schema. /// @return The GUID of the Scroll badge attestation schema. - function schema() external returns (bytes32); + function schema() external view returns (bytes32); /// @notice The profile registry contract. /// @return The address of the profile registry. - function registry() external returns (address); + function registry() external view returns (address); /// @notice The global EAS contract. /// @return The address of the global EAS contract. - function eas() external returns (address); + function eas() external view returns (address); /// @notice Validate and return a Scroll badge attestation. /// @param uid The attestation UID. diff --git a/src/interfaces/IScrollSelfAttestationBadge.sol b/src/interfaces/IScrollSelfAttestationBadge.sol new file mode 100644 index 0000000..74123d1 --- /dev/null +++ b/src/interfaces/IScrollSelfAttestationBadge.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Attestation} from "@eas/contracts/IEAS.sol"; + +import {IScrollBadge} from "./IScrollBadge.sol"; + +interface IScrollSelfAttestationBadge is IScrollBadge { + /// @notice Return the unique id of this badge. + function getBadgeId() external view returns (uint256); + + /// @notice Returns an existing attestation by UID. + /// @param uid The UID of the attestation to retrieve. + /// @return The attestation data members. + function getAttestation(bytes32 uid) external view returns (Attestation memory); +} diff --git a/src/resolver/ScrollBadgeResolver.sol b/src/resolver/ScrollBadgeResolver.sol index e80306b..9d8a5bd 100644 --- a/src/resolver/ScrollBadgeResolver.sol +++ b/src/resolver/ScrollBadgeResolver.sol @@ -12,6 +12,7 @@ import {IProfile} from "../interfaces/IProfile.sol"; import {IProfileRegistry} from "../interfaces/IProfileRegistry.sol"; import {IScrollBadge} from "../interfaces/IScrollBadge.sol"; import {IScrollBadgeResolver} from "../interfaces/IScrollBadgeResolver.sol"; +import {IScrollSelfAttestationBadge} from "../interfaces/IScrollSelfAttestationBadge.sol"; import {SCROLL_BADGE_SCHEMA, decodeBadgeData} from "../Common.sol"; import {ScrollBadgeResolverWhitelist} from "./ScrollBadgeResolverWhitelist.sol"; @@ -49,8 +50,19 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg /// @inheritdoc IScrollBadgeResolver bytes32 public schema; + /// @notice The list of self attested badges, mapping from badge id to badge address. + /// @dev This is a list of badges with special needs which EAS cannot satisfy, such as + /// auto attest/revoke badge based on certain token holding amount. + /// The uid for the badge is customized in the following way: + /// ```text + /// [ address | badge id | customized data ] + /// [ 160 bits | 32 bits | 64 bits ] + /// [LSB MSB] + /// ``` + mapping(uint256 => address) public selfAttestedBadges; + // Storage slots reserved for future upgrades. - uint256[49] private __gap; + uint256[48] private __gap; /** * @@ -165,8 +177,19 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg function getAndValidateBadge(bytes32 uid) external view returns (Attestation memory) { Attestation memory attestation = _eas.getAttestation(uid); + // if we cannot find the badge in EAS, try self attestation if (attestation.uid == EMPTY_UID) { - revert AttestationNotFound(uid); + // extract badge address from uid and do self attestation + uint256 badgeId = uint256(uid) >> 160 & 0xffffffff; + address badgeAddr = selfAttestedBadges[badgeId]; + if (badgeAddr != address(0)) { + attestation = IScrollSelfAttestationBadge(badgeAddr).getAttestation(uid); + } + if (attestation.uid == EMPTY_UID) { + revert AttestationNotFound(uid); + } else { + return attestation; + } } if (attestation.schema != schema) { @@ -184,6 +207,17 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg return attestation; } + /** + * + * Restricted Functions * + * + */ + + /// @notice Update the address of a self attested badge. + function updateSelfAttestedBadge(uint256 badgeId, address badgeAddress) external onlyOwner { + selfAttestedBadges[badgeId] = badgeAddress; + } + /** * * Internal Functions *