Skip to content

Commit

Permalink
feat: add SCRHoldingBadge (#57)
Browse files Browse the repository at this point in the history
* feat: add SCRHoldingBadge

* naming and missing override

* feat: unit tests for SCRHoldingBadge
zimpha authored Nov 26, 2024
1 parent 92e08a8 commit 5412aea
Showing 7 changed files with 477 additions and 6 deletions.
4 changes: 4 additions & 0 deletions src/Common.sol
Original file line number Diff line number Diff line change
@@ -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;
}

/// @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
@@ -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;
}
6 changes: 3 additions & 3 deletions src/interfaces/IScrollBadgeResolver.sol
Original file line number Diff line number Diff line change
@@ -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.
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
@@ -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 *
266 changes: 266 additions & 0 deletions test/SCRHoldingBadge.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import {Test} from "forge-std/Test.sol";
import {MockERC20} from "forge-std/mocks/MockERC20.sol";

import {EAS} from "@eas/contracts/EAS.sol";
import {EMPTY_UID, NO_EXPIRATION_TIME} from "@eas/contracts/Common.sol";
import {SchemaRegistry, ISchemaRegistry} from "@eas/contracts/SchemaRegistry.sol";
import {IEAS, Attestation, AttestationRequest, AttestationRequestData, RevocationRequest, RevocationRequestData} from "@eas/contracts/IEAS.sol";

import {Strings} from "@openzeppelin/contracts/utils/Strings.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 {ScrollBadge} from "../src/badge/ScrollBadge.sol";
import {ScrollBadgeResolver} from "../src/resolver/ScrollBadgeResolver.sol";
import {SCRHoldingBadge} from "../src/badge/examples/SCRHoldingBadge.sol";

import {encodeBadgeData} from "../src/Common.sol";
import {AttestationNotFound} from "../src/Errors.sol";

contract Token is MockERC20 {
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}

contract SCRHoldingBadgeTest is Test {
address private constant TREASURY_ADDRESS = 0x1000000000000000000000000000000000000000;

address private constant PROXY_ADMIN_ADDRESS = 0x2000000000000000000000000000000000000000;

ISchemaRegistry private schemaRegistry;
IEAS private eas;
ScrollBadgeResolver private resolver;
SCRHoldingBadge private badge;
Token private token;

Profile private profileImpl;
ProfileRegistry private profileRegistry;
Profile private profile;

receive() external payable {}

function setUp() public {
schemaRegistry = new SchemaRegistry();
eas = new EAS(schemaRegistry);
address profileRegistryProxy = address(
new TransparentUpgradeableProxy(address(new EmptyContract()), PROXY_ADMIN_ADDRESS, "")
);

address resolverImpl = address(new ScrollBadgeResolver(address(eas), profileRegistryProxy));
address resolverProxy = address(new TransparentUpgradeableProxy(resolverImpl, PROXY_ADMIN_ADDRESS, ""));
resolver = ScrollBadgeResolver(payable(resolverProxy));
resolver.initialize();

token = new Token();
badge = new SCRHoldingBadge(address(resolver), "xx", address(token));
resolver.updateSelfAttestedBadge(0, address(badge));

profileImpl = new Profile(address(resolver));
ProfileRegistry profileRegistryImpl = new ProfileRegistry();
vm.prank(PROXY_ADMIN_ADDRESS);
ITransparentUpgradeableProxy(profileRegistryProxy).upgradeTo(address(profileRegistryImpl));
profileRegistry = ProfileRegistry(profileRegistryProxy);
profileRegistry.initialize(TREASURY_ADDRESS, TREASURY_ADDRESS, address(profileImpl));
profile = Profile(profileRegistry.mint{value: 0.001 ether}("xxxxx", new bytes(0)));
}

function testInitialize() external view {
// from ScrollBadge
assertEq(badge.resolver(), address(resolver));

// from ScrollBadgeCustomPayload
assertEq(badge.getSchema(), "uint256 level");

// from ScrollBadgeDefaultURI
assertEq(badge.defaultBadgeURI(), "xx");
assertEq(badge.badgeTokenURI(0), "xx");

// from SCRHoldingBadge
assertEq(badge.scr(), address(token));
assertEq(badge.getBadgeId(), 0);

// in ScrollBadgeResolver
assertEq(resolver.selfAttestedBadges(0), address(badge));
}

function testIssueBadge(Attestation calldata attestation) external {
vm.prank(address(resolver));
assertEq(false, badge.issueBadge(attestation));
}

function testRevokeBadge(Attestation calldata attestation) external {
vm.prank(address(resolver));
assertEq(false, badge.revokeBadge(attestation));
}

function testGetAndValidateBadge() external {
bytes32 uid;
// badge id nonzero
assembly {
uid := 0
uid := or(uid, shl(1, 160))
}
vm.expectRevert(abi.encodePacked(AttestationNotFound.selector, uid));
badge.getAndValidateBadge(uid);

// customized data nonzero
assembly {
uid := 0
uid := or(uid, shl(1, 192))
}
vm.expectRevert(abi.encodePacked(AttestationNotFound.selector, uid));
badge.getAndValidateBadge(uid);

// no scr
assembly {
uid := address()
}
token.mint(address(this), 1 ether - 1);
vm.expectRevert(abi.encodePacked(AttestationNotFound.selector, uid));
badge.getAndValidateBadge(uid);

// succeed
assembly {
uid := address()
}
token.mint(address(this), 1 ether);
Attestation memory attestation = badge.getAndValidateBadge(uid);
assertEq(attestation.uid, uid);
assertEq(attestation.schema, resolver.schema());
assertEq(attestation.time, block.timestamp);
assertEq(attestation.expirationTime, 0);
assertEq(attestation.refUID, bytes32(0));
assertEq(attestation.recipient, address(this));
assertEq(attestation.attester, address(badge));
assertEq(attestation.revocable, false);
assertEq(attestation.data, encodeBadgeData(address(badge), abi.encode(uint256(1))));
}

function testBadgeTokenURI(address user, uint256 amount) external {
vm.assume(amount >= 1 ether);
vm.assume(user != address(0));

uint256 level;
if (amount >= 1 ether) level = 1;
if (amount >= 10 ether) level = 2;
if (amount >= 100 ether) level = 3;
if (amount >= 1000 ether) level = 4;
if (amount >= 10000 ether) level = 5;
if (amount >= 100000 ether) level = 6;

token.mint(user, amount);
bytes32 uid;
assembly {
uid := user
}
assertEq(badge.badgeTokenURI(uid), string(abi.encodePacked("xx", Strings.toString(level), ".json")));
}

function testHasBadge(address user, uint256 amount) external {
vm.assume(user != address(0));

token.mint(user, amount);
assertEq(badge.hasBadge(user), amount >= 1 ether);
}

function testGetAttestationInvalidUID(address user, uint96 base) external view {
vm.assume(base > 0);
bytes32 uid;
assembly {
uid := or(user, shl(160, base))
}
Attestation memory attestation = badge.getAttestation(uid);
assertEq(attestation.uid, bytes32(0));
assertEq(attestation.schema, "");
assertEq(attestation.time, 0);
assertEq(attestation.expirationTime, 0);
assertEq(attestation.refUID, bytes32(0));
assertEq(attestation.recipient, address(0));
assertEq(attestation.attester, address(0));
assertEq(attestation.revocable, false);
assertEq(attestation.data, "");
}

function testGetAttestationNoSCR(address user, uint256 amount) external {
amount = bound(amount, 0, 1 ether - 1);
token.mint(user, amount);
bytes32 uid;
assembly {
uid := user
}
Attestation memory attestation = badge.getAttestation(uid);
_validateAttestation(attestation, user);
}

function testGetAttestation(address user, uint256 amount, uint256 amount2) external {
vm.assume(amount >= 1 ether);
vm.assume(user != address(0));
amount2 = bound(amount2, 0, amount);

uint256 level;
if (amount >= 1 ether) level = 1;
if (amount >= 10 ether) level = 2;
if (amount >= 100 ether) level = 3;
if (amount >= 1000 ether) level = 4;
if (amount >= 10000 ether) level = 5;
if (amount >= 100000 ether) level = 6;

token.mint(user, amount);
bytes32 uid;
assembly {
uid := user
}
Attestation memory attestation = badge.getAttestation(uid);
_validateAttestation(attestation, user);

// transfer
vm.prank(user);
token.transfer(address(this), amount2);
attestation = badge.getAttestation(uid);
_validateAttestation(attestation, user);
}

function _validateAttestation(Attestation memory attestation, address user) internal view {
uint256 amount = token.balanceOf(user);
if (amount < 1 ether) {
assertEq(attestation.uid, bytes32(0));
assertEq(attestation.schema, "");
assertEq(attestation.time, 0);
assertEq(attestation.expirationTime, 0);
assertEq(attestation.refUID, bytes32(0));
assertEq(attestation.recipient, address(0));
assertEq(attestation.attester, address(0));
assertEq(attestation.revocable, false);
assertEq(attestation.data, "");
} else {
bytes32 uid;
assembly {
uid := user
}
uint256 level;
if (amount >= 1 ether) level = 1;
if (amount >= 10 ether) level = 2;
if (amount >= 100 ether) level = 3;
if (amount >= 1000 ether) level = 4;
if (amount >= 10000 ether) level = 5;
if (amount >= 100000 ether) level = 6;
assertEq(attestation.uid, uid);
assertEq(attestation.schema, resolver.schema());
assertEq(attestation.time, block.timestamp);
assertEq(attestation.expirationTime, 0);
assertEq(attestation.refUID, bytes32(0));
assertEq(attestation.recipient, user);
assertEq(attestation.attester, address(badge));
assertEq(attestation.revocable, false);
assertEq(attestation.data, encodeBadgeData(address(badge), abi.encode(level)));
}
}
}

0 comments on commit 5412aea

Please sign in to comment.