Skip to content

Commit 5412aea

Browse files
authored
feat: add SCRHoldingBadge (#57)
* feat: add SCRHoldingBadge * naming and missing override * feat: unit tests for SCRHoldingBadge
1 parent 92e08a8 commit 5412aea

File tree

7 files changed

+477
-6
lines changed

7 files changed

+477
-6
lines changed

src/Common.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ string constant SCROLL_BADGE_SCHEMA = "address badge, bytes payload";
99
function decodeBadgeData(bytes memory data) pure returns (address, bytes memory) {
1010
return abi.decode(data, (address, bytes));
1111
}
12+
13+
function encodeBadgeData(address badge, bytes memory payload) pure returns (bytes memory) {
14+
return abi.encode(badge, payload);
15+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.19;
4+
5+
import {Attestation} from "@eas/contracts/IEAS.sol";
6+
import {NO_EXPIRATION_TIME} from "@eas/contracts/Common.sol";
7+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
8+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
9+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
10+
11+
import {IScrollBadgeResolver} from "../../interfaces/IScrollBadgeResolver.sol";
12+
import {IScrollBadge, IScrollSelfAttestationBadge} from "../../interfaces/IScrollSelfAttestationBadge.sol";
13+
import {encodeBadgeData} from "../../Common.sol";
14+
import {ScrollBadge} from "../ScrollBadge.sol";
15+
import {ScrollBadgeCustomPayload} from "../extensions/ScrollBadgeCustomPayload.sol";
16+
import {ScrollBadgeDefaultURI} from "../extensions/ScrollBadgeDefaultURI.sol";
17+
18+
string constant SCR_HOLDING_BADGE_SCHEMA = "uint256 level";
19+
20+
function decodePayloadData(bytes memory data) pure returns (uint256) {
21+
return abi.decode(data, (uint256));
22+
}
23+
24+
/// @title SCRHoldingBadge
25+
/// @notice A badge that represents user's SCR holding amount.
26+
contract SCRHoldingBadge is ScrollBadgeCustomPayload, ScrollBadgeDefaultURI, Ownable, IScrollSelfAttestationBadge {
27+
uint256 private constant LEVEL_ONE_SCR_AMOUNT = 1 ether;
28+
uint256 private constant LEVEL_TWO_SCR_AMOUNT = 10 ether;
29+
uint256 private constant LEVEL_THREE_SCR_AMOUNT = 100 ether;
30+
uint256 private constant LEVEL_FOUR_SCR_AMOUNT = 1000 ether;
31+
uint256 private constant LEVEL_FIVE_SCR_AMOUNT = 10_000 ether;
32+
uint256 private constant LEVEL_SIX_SCR_AMOUNT = 100_000 ether;
33+
34+
/// @notice The address of SCR token.
35+
address public immutable scr;
36+
37+
constructor(address resolver_, string memory baseTokenURI_, address scr_)
38+
ScrollBadge(resolver_)
39+
ScrollBadgeDefaultURI(baseTokenURI_)
40+
{
41+
scr = scr_;
42+
}
43+
44+
/// @notice Update the base token URI.
45+
/// @param baseTokenURI_ The new base token URI.
46+
function updateBaseTokenURI(string memory baseTokenURI_) external onlyOwner {
47+
defaultBadgeURI = baseTokenURI_;
48+
}
49+
50+
/// @inheritdoc ScrollBadge
51+
function onIssueBadge(Attestation calldata)
52+
internal
53+
virtual
54+
override (ScrollBadge, ScrollBadgeCustomPayload)
55+
returns (bool)
56+
{
57+
return false;
58+
}
59+
60+
/// @inheritdoc ScrollBadge
61+
function onRevokeBadge(Attestation calldata)
62+
internal
63+
virtual
64+
override (ScrollBadge, ScrollBadgeCustomPayload)
65+
returns (bool)
66+
{
67+
return false;
68+
}
69+
70+
/// @inheritdoc ScrollBadge
71+
function badgeTokenURI(bytes32 uid)
72+
public
73+
view
74+
override (IScrollBadge, ScrollBadge, ScrollBadgeDefaultURI)
75+
returns (string memory)
76+
{
77+
return ScrollBadgeDefaultURI.badgeTokenURI(uid);
78+
}
79+
80+
/// @inheritdoc IScrollBadge
81+
function hasBadge(address user) public view virtual override (IScrollBadge, ScrollBadge) returns (bool) {
82+
uint256 balance = IERC20(scr).balanceOf(user);
83+
return balance >= LEVEL_ONE_SCR_AMOUNT;
84+
}
85+
86+
/// @inheritdoc ScrollBadgeDefaultURI
87+
function getBadgeTokenURI(bytes32 uid) internal view override returns (string memory) {
88+
Attestation memory attestation = getAndValidateBadge(uid);
89+
bytes memory payload = getPayload(attestation);
90+
uint256 level = decodePayloadData(payload);
91+
92+
return string(abi.encodePacked(defaultBadgeURI, Strings.toString(level), ".json"));
93+
}
94+
95+
/// @inheritdoc ScrollBadgeCustomPayload
96+
function getSchema() public pure override returns (string memory) {
97+
return SCR_HOLDING_BADGE_SCHEMA;
98+
}
99+
100+
/// @inheritdoc IScrollSelfAttestationBadge
101+
function getBadgeId() external pure returns (uint256) {
102+
return 0;
103+
}
104+
105+
/// @inheritdoc IScrollSelfAttestationBadge
106+
///
107+
/// @dev The uid encoding should be
108+
/// ```text
109+
/// [ address | badge id | customized data ]
110+
/// [ 160 bits | 32 bits | 64 bits ]
111+
/// [LSB MSB]
112+
/// ```
113+
/// The *badge id* and the *customized data* should both be zero.
114+
function getAttestation(bytes32 uid) external view override returns (Attestation memory attestation) {
115+
// invalid uid, return empty badge
116+
if ((uint256(uid) >> 160) > 0) return attestation;
117+
118+
// extract badge recipient from uid
119+
address recipient;
120+
assembly {
121+
recipient := and(uid, 0xffffffffffffffffffffffffffffffffffffffff)
122+
}
123+
124+
// compute payload
125+
uint256 level;
126+
uint256 balance = IERC20(scr).balanceOf(recipient);
127+
// not hold enough SCR, return empty badge
128+
if (balance < LEVEL_ONE_SCR_AMOUNT) return attestation;
129+
else if (balance < LEVEL_TWO_SCR_AMOUNT) level = 1;
130+
else if (balance < LEVEL_THREE_SCR_AMOUNT) level = 2;
131+
else if (balance < LEVEL_FOUR_SCR_AMOUNT) level = 3;
132+
else if (balance < LEVEL_FIVE_SCR_AMOUNT) level = 4;
133+
else if (balance < LEVEL_SIX_SCR_AMOUNT) level = 5;
134+
else level = 6;
135+
bytes memory payload = abi.encode(level);
136+
137+
// fill data in Attestation
138+
attestation.uid = uid;
139+
attestation.schema = IScrollBadgeResolver(resolver).schema();
140+
attestation.time = uint64(block.timestamp);
141+
attestation.expirationTime = NO_EXPIRATION_TIME;
142+
attestation.refUID = bytes32(0);
143+
attestation.recipient = recipient;
144+
attestation.attester = address(this);
145+
attestation.revocable = false;
146+
attestation.data = encodeBadgeData(address(this), payload);
147+
148+
return attestation;
149+
}
150+
}

src/badge/extensions/ScrollBadgeDefaultURI.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ abstract contract ScrollBadgeDefaultURI is ScrollBadge {
1414
}
1515

1616
/// @inheritdoc ScrollBadge
17-
function badgeTokenURI(bytes32 uid) public view override returns (string memory) {
17+
function badgeTokenURI(bytes32 uid) public view virtual override returns (string memory) {
1818
if (uid == bytes32(0)) {
1919
return defaultBadgeURI;
2020
}

src/interfaces/IScrollBadgeResolver.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ interface IScrollBadgeResolver {
3232

3333
/// @notice Return the Scroll badge attestation schema.
3434
/// @return The GUID of the Scroll badge attestation schema.
35-
function schema() external returns (bytes32);
35+
function schema() external view returns (bytes32);
3636

3737
/// @notice The profile registry contract.
3838
/// @return The address of the profile registry.
39-
function registry() external returns (address);
39+
function registry() external view returns (address);
4040

4141
/// @notice The global EAS contract.
4242
/// @return The address of the global EAS contract.
43-
function eas() external returns (address);
43+
function eas() external view returns (address);
4444

4545
/// @notice Validate and return a Scroll badge attestation.
4646
/// @param uid The attestation UID.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.19;
4+
5+
import {Attestation} from "@eas/contracts/IEAS.sol";
6+
7+
import {IScrollBadge} from "./IScrollBadge.sol";
8+
9+
interface IScrollSelfAttestationBadge is IScrollBadge {
10+
/// @notice Return the unique id of this badge.
11+
function getBadgeId() external view returns (uint256);
12+
13+
/// @notice Returns an existing attestation by UID.
14+
/// @param uid The UID of the attestation to retrieve.
15+
/// @return The attestation data members.
16+
function getAttestation(bytes32 uid) external view returns (Attestation memory);
17+
}

src/resolver/ScrollBadgeResolver.sol

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {IProfile} from "../interfaces/IProfile.sol";
1212
import {IProfileRegistry} from "../interfaces/IProfileRegistry.sol";
1313
import {IScrollBadge} from "../interfaces/IScrollBadge.sol";
1414
import {IScrollBadgeResolver} from "../interfaces/IScrollBadgeResolver.sol";
15+
import {IScrollSelfAttestationBadge} from "../interfaces/IScrollSelfAttestationBadge.sol";
1516
import {SCROLL_BADGE_SCHEMA, decodeBadgeData} from "../Common.sol";
1617
import {ScrollBadgeResolverWhitelist} from "./ScrollBadgeResolverWhitelist.sol";
1718

@@ -49,8 +50,19 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg
4950
/// @inheritdoc IScrollBadgeResolver
5051
bytes32 public schema;
5152

53+
/// @notice The list of self attested badges, mapping from badge id to badge address.
54+
/// @dev This is a list of badges with special needs which EAS cannot satisfy, such as
55+
/// auto attest/revoke badge based on certain token holding amount.
56+
/// The uid for the badge is customized in the following way:
57+
/// ```text
58+
/// [ address | badge id | customized data ]
59+
/// [ 160 bits | 32 bits | 64 bits ]
60+
/// [LSB MSB]
61+
/// ```
62+
mapping(uint256 => address) public selfAttestedBadges;
63+
5264
// Storage slots reserved for future upgrades.
53-
uint256[49] private __gap;
65+
uint256[48] private __gap;
5466

5567
/**
5668
*
@@ -165,8 +177,19 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg
165177
function getAndValidateBadge(bytes32 uid) external view returns (Attestation memory) {
166178
Attestation memory attestation = _eas.getAttestation(uid);
167179

180+
// if we cannot find the badge in EAS, try self attestation
168181
if (attestation.uid == EMPTY_UID) {
169-
revert AttestationNotFound(uid);
182+
// extract badge address from uid and do self attestation
183+
uint256 badgeId = uint256(uid) >> 160 & 0xffffffff;
184+
address badgeAddr = selfAttestedBadges[badgeId];
185+
if (badgeAddr != address(0)) {
186+
attestation = IScrollSelfAttestationBadge(badgeAddr).getAttestation(uid);
187+
}
188+
if (attestation.uid == EMPTY_UID) {
189+
revert AttestationNotFound(uid);
190+
} else {
191+
return attestation;
192+
}
170193
}
171194

172195
if (attestation.schema != schema) {
@@ -184,6 +207,17 @@ contract ScrollBadgeResolver is IScrollBadgeResolver, SchemaResolver, ScrollBadg
184207
return attestation;
185208
}
186209

210+
/**
211+
*
212+
* Restricted Functions *
213+
*
214+
*/
215+
216+
/// @notice Update the address of a self attested badge.
217+
function updateSelfAttestedBadge(uint256 badgeId, address badgeAddress) external onlyOwner {
218+
selfAttestedBadges[badgeId] = badgeAddress;
219+
}
220+
187221
/**
188222
*
189223
* Internal Functions *

0 commit comments

Comments
 (0)