Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add SCRHoldingBadge #57

Merged
merged 3 commits into from
Nov 26, 2024
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
4 changes: 4 additions & 0 deletions src/Common.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
150 changes: 150 additions & 0 deletions src/badge/examples/SCRHoldingBadge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 = 10_000 ether;
uint256 private constant LEVEL_SIX_SCR_AMOUNT = 100_000 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 IScrollBadge
function hasBadge(address user) public view virtual override (IScrollBadge, ScrollBadge) returns (bool) {
uint256 balance = IERC20(scr).balanceOf(user);
return balance >= LEVEL_ONE_SCR_AMOUNT;
}

/// @inheritdoc ScrollBadgeDefaultURI
function getBadgeTokenURI(bytes32 uid) internal view override returns (string memory) {
Attestation memory attestation = getAndValidateBadge(uid);
bytes memory payload = getPayload(attestation);
uint256 level = decodePayloadData(payload);

return string(abi.encodePacked(defaultBadgeURI, Strings.toString(level), ".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;
Thegaram marked this conversation as resolved.
Show resolved Hide resolved
}

/// @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;
}
}
2 changes: 1 addition & 1 deletion src/badge/extensions/ScrollBadgeDefaultURI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 virtual override returns (string memory) {
if (uid == bytes32(0)) {
return defaultBadgeURI;
}
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/IScrollBadgeResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions src/interfaces/IScrollSelfAttestationBadge.sol
Original file line number Diff line number Diff line change
@@ -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);
}
38 changes: 36 additions & 2 deletions src/resolver/ScrollBadgeResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;

/**
*
Expand Down Expand Up @@ -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) {
Expand All @@ -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 *
Expand Down
Loading
Loading