diff --git a/.openzeppelin/unknown-360.json b/.openzeppelin/unknown-360.json index 9b987dd..14935e6 100644 --- a/.openzeppelin/unknown-360.json +++ b/.openzeppelin/unknown-360.json @@ -698,6 +698,436 @@ }, "namespaces": {} } + }, + "08ff19fdfd68b5802bef4603eefb8ead969a0cd43bafc125a82e715b5edeab79": { + "address": "0xCe4Bf32909EF4f755cbbc48b45EDB9Fb948a028D", + "txHash": "0x70c5fdf6350e7e1b59cfc099a31d8b7dd0e9126621f37eef2ec0f2cde239bbcf", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "userAttestations", + "offset": 0, + "slot": "151", + "type": "t_mapping(t_address,t_mapping(t_bytes32,t_bytes32))", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:37" + }, + { + "label": "_eas", + "offset": 0, + "slot": "152", + "type": "t_contract(IEAS)2310", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:40" + }, + { + "label": "_gitcoinAttester", + "offset": 0, + "slot": "153", + "type": "t_contract(GitcoinAttester)6844", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:43" + }, + { + "label": "allowlist", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_address,t_bool)", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:46" + }, + { + "label": "scores", + "offset": 0, + "slot": "155", + "type": "t_mapping(t_address,t_struct(CachedScore)7514_storage)", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:49" + }, + { + "label": "scoreSchema", + "offset": 0, + "slot": "156", + "type": "t_bytes32", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:52" + }, + { + "label": "defaultCommunityId", + "offset": 0, + "slot": "157", + "type": "t_uint32", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:54" + }, + { + "label": "communityScores", + "offset": 0, + "slot": "158", + "type": "t_mapping(t_uint32,t_mapping(t_address,t_struct(CachedScore)7514_storage))", + "contract": "GitcoinResolver", + "src": "contracts/GitcoinResolver.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(GitcoinAttester)6844": { + "label": "contract GitcoinAttester", + "numberOfBytes": "20" + }, + "t_contract(IEAS)2310": { + "label": "contract IEAS", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_bytes32,t_bytes32))": { + "label": "mapping(address => mapping(bytes32 => bytes32))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(CachedScore)7514_storage)": { + "label": "mapping(address => struct IGitcoinResolver.CachedScore)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bytes32)": { + "label": "mapping(bytes32 => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint32,t_mapping(t_address,t_struct(CachedScore)7514_storage))": { + "label": "mapping(uint32 => mapping(address => struct IGitcoinResolver.CachedScore))", + "numberOfBytes": "32" + }, + "t_struct(CachedScore)7514_storage": { + "label": "struct IGitcoinResolver.CachedScore", + "members": [ + { + "label": "score", + "type": "t_uint32", + "offset": 0, + "slot": "0" + }, + { + "label": "time", + "type": "t_uint64", + "offset": 4, + "slot": "0" + }, + { + "label": "expirationTime", + "type": "t_uint64", + "offset": 12, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "3a2253feeff1c367227e1923a10ee256b87d8582e762953fbd6dda101eee9feb": { + "address": "0xc4858e4D177Bf0d14571F91401492d62aa608047", + "txHash": "0x0641d4bc106a897d1c6f687ee6aa094d54634ee3b97907e8c934ca89194c172e", + "layout": { + "solcVersion": "0.8.20", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "eas", + "offset": 0, + "slot": "151", + "type": "t_contract(IEAS)368", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:25" + }, + { + "label": "providerVersions", + "offset": 0, + "slot": "152", + "type": "t_mapping(t_uint32,t_array(t_string_storage)dyn_storage)", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:28" + }, + { + "label": "reversedMappingVersions", + "offset": 0, + "slot": "153", + "type": "t_mapping(t_uint32,t_mapping(t_string_memory_ptr,t_uint8))", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:31" + }, + { + "label": "currentVersion", + "offset": 0, + "slot": "154", + "type": "t_uint32", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:34" + }, + { + "label": "gitcoinResolver", + "offset": 4, + "slot": "154", + "type": "t_contract(IGitcoinResolver)3301", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:37" + }, + { + "label": "passportSchemaUID", + "offset": 0, + "slot": "155", + "type": "t_bytes32", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:40" + }, + { + "label": "scoreSchemaUID", + "offset": 0, + "slot": "156", + "type": "t_bytes32", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:43" + }, + { + "label": "maxScoreAge", + "offset": 0, + "slot": "157", + "type": "t_uint64", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:46" + }, + { + "label": "threshold", + "offset": 0, + "slot": "158", + "type": "t_uint256", + "contract": "GitcoinPassportDecoder", + "src": "contracts/GitcoinPassportDecoder.sol:49" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_string_storage)dyn_storage": { + "label": "string[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IEAS)368": { + "label": "contract IEAS", + "numberOfBytes": "20" + }, + "t_contract(IGitcoinResolver)3301": { + "label": "contract IGitcoinResolver", + "numberOfBytes": "20" + }, + "t_mapping(t_string_memory_ptr,t_uint8)": { + "label": "mapping(string => uint8)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint32,t_array(t_string_storage)dyn_storage)": { + "label": "mapping(uint32 => string[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint32,t_mapping(t_string_memory_ptr,t_uint8))": { + "label": "mapping(uint32 => mapping(string => uint8))", + "numberOfBytes": "32" + }, + "t_string_memory_ptr": { + "label": "string", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/contracts/GitcoinPassportDecoder.sol b/contracts/GitcoinPassportDecoder.sol index 27e20d4..1e8cec7 100644 --- a/contracts/GitcoinPassportDecoder.sol +++ b/contracts/GitcoinPassportDecoder.sol @@ -456,6 +456,30 @@ contract GitcoinPassportDecoder is return score; } + /** + * @dev Retrieves the user's Score attestation for a given community via the GitcoinResolver and returns it as a 4 digit number + * @param user The ETH address of the recipient + */ + function getScore( + uint32 communityId, + address user + ) public view returns (uint256) { + IGitcoinResolver.CachedScore memory cachedScore = gitcoinResolver + .getCachedScore(communityId, user); + + if (cachedScore.time != 0) { + // Check for expiration time + if (_isCachedScoreExpired(cachedScore)) { + revert AttestationExpired(cachedScore.time); + } + + // Return the score value + return cachedScore.score; + } else { + revert AttestationNotFound(); + } + } + /** * @dev Determines if a user is a human based on their score being above a certain threshold and valid within the max score age * @param user The ETH address of the recipient diff --git a/contracts/GitcoinResolver.sol b/contracts/GitcoinResolver.sol index cff378d..1d08b08 100644 --- a/contracts/GitcoinResolver.sol +++ b/contracts/GitcoinResolver.sol @@ -51,6 +51,11 @@ contract GitcoinResolver is // Mapping of active passport score schemas - used when storing scores to state bytes32 public scoreSchema; + uint32 public defaultCommunityId; + + // Mapping of communityId => address => score + mapping(uint32 => mapping(address => CachedScore)) public communityScores; + /** * @dev Creates a new resolver. * @notice Initializer function responsible for setting up the contract's initial state. @@ -96,6 +101,10 @@ contract GitcoinResolver is _unpause(); } + function setDefaultCommunityId(uint32 communityId) external onlyOwner { + defaultCommunityId = communityId; + } + /** * @dev Set supported score schemas. * @param _schema The score schema uid @@ -133,11 +142,11 @@ contract GitcoinResolver is revert InvalidAttester(); } - userAttestations[attestation.recipient][attestation.schema] = attestation - .uid; - if (scoreSchema == attestation.schema) { _setScore(attestation); + } else { + userAttestations[attestation.recipient][attestation.schema] = attestation + .uid; } return true; } @@ -148,7 +157,7 @@ contract GitcoinResolver is */ function _setScore(Attestation calldata attestation) private { // Decode the score attestion output - (uint256 score, , uint8 digits) = abi.decode( + (uint256 score, uint32 communityId, uint8 digits) = abi.decode( attestation.data, (uint256, uint32, uint8) ); @@ -159,33 +168,59 @@ contract GitcoinResolver is score *= 10 ** (4 - digits); } - scores[attestation.recipient] = CachedScore( + CachedScore memory cachedScore = CachedScore( uint32(score), attestation.time, attestation.expirationTime ); + + if (communityId == defaultCommunityId || defaultCommunityId == 0) { + scores[attestation.recipient] = cachedScore; + userAttestations[attestation.recipient][attestation.schema] = attestation + .uid; + } else { + communityScores[communityId][attestation.recipient] = cachedScore; + } } /** * @dev Removes the score data from the state for the specified recipient. - * @param recipient The recipient of the score which needs to be removed. + * @param attestation The attestation to be removed. */ - function _removeScore(address recipient) private { - delete scores[recipient]; + function _removeScore(Attestation calldata attestation) private { + // Decode the score attestion output + (, uint32 communityId, ) = abi.decode( + attestation.data, + (uint256, uint32, uint8) + ); + + if (communityId == defaultCommunityId || defaultCommunityId == 0) { + delete scores[attestation.recipient]; + delete userAttestations[attestation.recipient][attestation.schema]; + } else { + delete communityScores[communityId][attestation.recipient]; + } } - /** - * - * @param user The ETH address of the recipient - * @return The `CachedScore` for the given ETH address. - * A non-zero value in the `time` (issuance time) indicates that a valid score has been retreived. - */ + /// @inheritdoc IGitcoinResolver function getCachedScore( address user ) external view returns (CachedScore memory) { return scores[user]; } + /// @inheritdoc IGitcoinResolver + function getCachedScore( + uint32 communityId, + address user + ) external view returns (CachedScore memory) { + if (communityId == defaultCommunityId || defaultCommunityId == 0) { + return scores[user]; + } else { + return communityScores[communityId][user]; + } + } + /** * @dev Processes multiple attestations and verifies whether they are valid. * @param attestations The new attestations. @@ -244,18 +279,16 @@ contract GitcoinResolver is * @return true indicating if the pre-revocation have been performed and the revocation process should continue */ function _revoke(Attestation calldata attestation) internal returns (bool) { - userAttestations[attestation.recipient][attestation.schema] = 0; - _removeScore(attestation.recipient); + if (attestation.schema == scoreSchema) { + _removeScore(attestation); + } else { + userAttestations[attestation.recipient][attestation.schema] = 0; + } return true; } - /** - * - * @param user The ETH address of the recipient - * @param schema THE UID of the chema - * @return The attestation UID or 0x0 if not found - */ + /// @inheritdoc IGitcoinResolver function getUserAttestation( address user, bytes32 schema diff --git a/contracts/IGitcoinPassportDecoder.sol b/contracts/IGitcoinPassportDecoder.sol index 553e1ce..47f6b06 100644 --- a/contracts/IGitcoinPassportDecoder.sol +++ b/contracts/IGitcoinPassportDecoder.sol @@ -23,5 +23,7 @@ interface IGitcoinPassportDecoder { function getScore(address user) external view returns (uint256); + function getScore(uint32 communityId, address user) external view returns (uint256); + function isHuman(address user) external view returns (bool); } diff --git a/contracts/IGitcoinResolver.sol b/contracts/IGitcoinResolver.sol index 3ac4d92..6a69789 100644 --- a/contracts/IGitcoinResolver.sol +++ b/contracts/IGitcoinResolver.sol @@ -14,25 +14,31 @@ interface IGitcoinResolver { uint64 expirationTime; // This makes sense because we want to make sure the stamp is not expired, and also do not want to load the attestation } - /** - * - * @param user The ETH address of the recipient - * @param schema THE UID of the chema - * @return The attestation UID or 0x0 if not found - */ + /// @param user The ETH address of the recipient + /// @param schema THE UID of the chema + /// @return The attestation UID or 0x0 if not found + /// @dev Returns the latest user attestation for a given schema + /// @dev Not supported for community-specific attestations function getUserAttestation( address user, bytes32 schema ) external view returns (bytes32); - /** - * - * @param user The ETH address of the recipient - * @return The `CachedScore` for the given ETH address. - * A non-zero value in the `issuanceDate` indicates that a valid score has been retreived. - */ + /// @notice Get the cached score for a user in a the default community + /// @param user The ETH address of the recipient + /// @return The `CachedScore` for the given ETH address. + /// @dev A non-zero value in the `issuanceDate` indicates that a valid score has been retreived. + function getCachedScore( + address user + ) external view returns (CachedScore memory); + /// @notice Get the cached score for a user in a specific community + /// @param communityId The ID of the community + /// @param user The ETH address of the recipient + /// @return The `CachedScore` for the given ETH address. + /// @dev A non-zero value in the `issuanceDate` indicates that a valid score has been retreived. function getCachedScore( + uint32 communityId, address user ) external view returns (CachedScore memory); } diff --git a/contracts/mocks/GitcoinResolverUpdate.sol b/contracts/mocks/GitcoinResolverUpdate.sol index c5003e3..c7111e4 100644 --- a/contracts/mocks/GitcoinResolverUpdate.sol +++ b/contracts/mocks/GitcoinResolverUpdate.sol @@ -1,3 +1,4 @@ + // SPDX-License-Identifier: GPL pragma solidity ^0.8.9; @@ -31,6 +32,8 @@ contract GitcoinResolverUpdate is error NotAllowlisted(); error InvalidAttester(); + event ScoreSchemaSet(bytes32 schema); + // Mapping of addresses to schemas to an attestation UID mapping(address => mapping(bytes32 => bytes32)) public userAttestations; @@ -49,7 +52,12 @@ contract GitcoinResolverUpdate is // Mapping of active passport score schemas - used when storing scores to state bytes32 public scoreSchema; - uint256 public aNewPublicVariable; + uint32 public defaultCommunityId; + + // Mapping of communityId => address => score + mapping(uint32 => mapping(address => CachedScore)) public communityScores; + + uint256 aNewVariable; /** * @dev Creates a new resolver. @@ -96,6 +104,19 @@ contract GitcoinResolverUpdate is _unpause(); } + function setDefaultCommunityId(uint32 communityId) external onlyOwner { + defaultCommunityId = communityId; + } + + /** + * @dev Set supported score schemas. + * @param _schema The score schema uid + */ + function setScoreSchema(bytes32 _schema) external onlyOwner { + scoreSchema = _schema; + emit ScoreSchemaSet(_schema); + } + // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address) internal override onlyOwner {} @@ -124,9 +145,12 @@ contract GitcoinResolverUpdate is revert InvalidAttester(); } - userAttestations[attestation.recipient][attestation.schema] = attestation - .uid; - + if (scoreSchema == attestation.schema) { + _setScore(attestation); + } else { + userAttestations[attestation.recipient][attestation.schema] = attestation + .uid; + } return true; } @@ -136,7 +160,7 @@ contract GitcoinResolverUpdate is */ function _setScore(Attestation calldata attestation) private { // Decode the score attestion output - (uint256 score, , uint8 digits) = abi.decode( + (uint256 score, uint32 communityId, uint8 digits) = abi.decode( attestation.data, (uint256, uint32, uint8) ); @@ -147,22 +171,59 @@ contract GitcoinResolverUpdate is score *= 10 ** (4 - digits); } - scores[attestation.recipient] = CachedScore( + CachedScore memory cachedScore = CachedScore( uint32(score), attestation.time, attestation.expirationTime ); + + if (communityId == defaultCommunityId) { + scores[attestation.recipient] = cachedScore; + userAttestations[attestation.recipient][attestation.schema] = attestation + .uid; + } else { + communityScores[communityId][attestation.recipient] = cachedScore; + } } /** - * @dev Returns the cached score for a given address. + * @dev Removes the score data from the state for the specified recipient. + * @param attestation The attestation to be removed. */ + function _removeScore(Attestation calldata attestation) private { + // Decode the score attestion output + (, uint32 communityId, ) = abi.decode( + attestation.data, + (uint256, uint32, uint8) + ); + + if (communityId == defaultCommunityId) { + delete scores[attestation.recipient]; + delete userAttestations[attestation.recipient][attestation.schema]; + } else { + delete communityScores[communityId][attestation.recipient]; + } + } + + /// @inheritdoc IGitcoinResolver function getCachedScore( address user ) external view returns (CachedScore memory) { return scores[user]; } + /// @inheritdoc IGitcoinResolver + function getCachedScore( + uint32 communityId, + address user + ) external view returns (CachedScore memory) { + if (communityId == defaultCommunityId) { + return scores[user]; + } else { + return communityScores[communityId][user]; + } + } + /** * @dev Processes multiple attestations and verifies whether they are valid. * @param attestations The new attestations. @@ -215,12 +276,22 @@ contract GitcoinResolverUpdate is return true; } + /** + * @dev Processes an revocation request + * @param attestation The new attestation request. + * @return true indicating if the pre-revocation have been performed and the revocation process should continue + */ function _revoke(Attestation calldata attestation) internal returns (bool) { - userAttestations[attestation.recipient][attestation.schema] = 0; + if (attestation.schema == scoreSchema) { + _removeScore(attestation); + } else { + userAttestations[attestation.recipient][attestation.schema] = 0; + } return true; } + /// @inheritdoc IGitcoinResolver function getUserAttestation( address user, bytes32 schema diff --git a/contracts/mocks/MockResolver.sol b/contracts/mocks/MockResolver.sol index f7a963a..f1af9b3 100644 --- a/contracts/mocks/MockResolver.sol +++ b/contracts/mocks/MockResolver.sol @@ -63,6 +63,13 @@ contract MockResolver is IGitcoinResolver, ISchemaResolver { return scores[user]; } + function getCachedScore( + uint32 communityId, + address user + ) external view returns (CachedScore memory) { + return scores[user]; + } + /** * @dev Processes multiple attestations and verifies whether they are valid. * @param attestations The new attestations. diff --git a/deployments/abi/GitcoinPassportDecoder.json b/deployments/abi/GitcoinPassportDecoder.json index 3a32d31..687c13a 100644 --- a/deployments/abi/GitcoinPassportDecoder.json +++ b/deployments/abi/GitcoinPassportDecoder.json @@ -680,6 +680,7 @@ "function getPassport(address user) view returns ((string provider, bytes32 hash, uint64 time, uint64 expirationTime)[])", "function getProviders(uint32 version) view returns (string[])", "function getScore(address user) view returns (uint256)", + "function getScore(uint32 communityId, address user) view returns (uint256)", "function gitcoinResolver() view returns (address)", "function initialize()", "function isHuman(address user) view returns (bool)", diff --git a/deployments/abi/GitcoinResolver.json b/deployments/abi/GitcoinResolver.json index 73650c4..fda09de 100644 --- a/deployments/abi/GitcoinResolver.json +++ b/deployments/abi/GitcoinResolver.json @@ -550,7 +550,10 @@ "function addToAllowlist(address addr)", "function allowlist(address) view returns (bool)", "function attest((bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data) attestation) payable returns (bool)", + "function communityScores(uint32, address) view returns (uint32 score, uint64 time, uint64 expirationTime)", + "function defaultCommunityId() view returns (uint32)", "function getCachedScore(address user) view returns ((uint32 score, uint64 time, uint64 expirationTime))", + "function getCachedScore(uint32 communityId, address user) view returns ((uint32 score, uint64 time, uint64 expirationTime))", "function getUserAttestation(address user, bytes32 schema) view returns (bytes32)", "function initialize(address eas, address gitcoinAttester)", "function isPayable() pure returns (bool)", @@ -565,6 +568,7 @@ "function revoke((bytes32 uid, bytes32 schema, uint64 time, uint64 expirationTime, uint64 revocationTime, bytes32 refUID, address recipient, address attester, bool revocable, bytes data) attestation) payable returns (bool)", "function scoreSchema() view returns (bytes32)", "function scores(address) view returns (uint32 score, uint64 time, uint64 expirationTime)", + "function setDefaultCommunityId(uint32 communityId)", "function setScoreSchema(bytes32 _schema)", "function transferOwnership(address newOwner)", "function unpause()", diff --git a/test/GitcoinResolver.ts b/test/GitcoinResolver.ts index 212c14f..23c5936 100644 --- a/test/GitcoinResolver.ts +++ b/test/GitcoinResolver.ts @@ -502,4 +502,208 @@ describe("GitcoinResolver", function () { ).to.be.revertedWith("Ownable: caller is not the owner"); }); }); + + describe("community scores", function () { + const defaultCommunityId = 1; + const testCommunityId = 2; + + it("should set the default community ID", async function () { + await gitcoinResolver.setDefaultCommunityId(defaultCommunityId); + expect(await gitcoinResolver.defaultCommunityId()).to.equal( + defaultCommunityId + ); + }); + + it("should not allow non-owner to set default community ID", async function () { + await expect( + gitcoinResolver.connect(recipient).setDefaultCommunityId(1) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + describe("With default community ID set", function () { + before(async function () { + await gitcoinResolver.setDefaultCommunityId(defaultCommunityId); + }); + + it("should cache default and community specific scores", async function () { + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + defaultCommunityId, + recipient.address + ) + )[0] + ).to.equal(0); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + testCommunityId, + recipient.address + ) + )[0] + ).to.equal(0); + + const attestation = getScoreAttestation( + { + schema: this.scoreSchemaId, + recipient: recipient.address, + attester: this.gitcoinAttesterAddress + }, + { + score: "12345", + scorer_id: defaultCommunityId, + score_decimals: 4 + } + ) as AttestationStruct; + + await gitcoinResolver.connect(mockEas).attest(attestation); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + defaultCommunityId, + recipient.address + ) + )[0] + ).to.equal(12345); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + testCommunityId, + recipient.address + ) + )[0] + ).to.equal(0); + + const communityAttestation = getScoreAttestation( + { + schema: this.scoreSchemaId, + recipient: recipient.address, + attester: this.gitcoinAttesterAddress + }, + { + score: "67890", + scorer_id: testCommunityId, + score_decimals: 4 + } + ) as AttestationStruct; + + await gitcoinResolver.connect(mockEas).attest(communityAttestation); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + defaultCommunityId, + recipient.address + ) + )[0] + ).to.equal(12345); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + testCommunityId, + recipient.address + ) + )[0] + ).to.equal(67890); + }); + + it("should revoke default and custom community scores correctly", async function () { + const scoreAttestation = getScoreAttestation( + { + schema: this.scoreSchemaId, + recipient: recipient.address, + attester: this.gitcoinAttesterAddress + }, + { + score: "12345", + scorer_id: defaultCommunityId, + score_decimals: 4 + } + ) as AttestationStruct; + + // Attest + await gitcoinResolver.connect(mockEas).attest(scoreAttestation); + + const communityAttestation = getScoreAttestation( + { + schema: this.scoreSchemaId, + recipient: recipient.address, + attester: this.gitcoinAttesterAddress + }, + { + score: "67890", + scorer_id: testCommunityId, + score_decimals: 4 + } + ) as AttestationStruct; + + // Attest + await gitcoinResolver.connect(mockEas).attest(communityAttestation); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + defaultCommunityId, + recipient.address + ) + )[0] + ).to.equal(12345); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + testCommunityId, + recipient.address + ) + )[0] + ).to.equal(67890); + + // Revoke + await gitcoinResolver.connect(mockEas).revoke(scoreAttestation); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + defaultCommunityId, + recipient.address + ) + )[0] + ).to.equal(0); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + testCommunityId, + recipient.address + ) + )[0] + ).to.equal(67890); + + // Revoke + await gitcoinResolver.connect(mockEas).revoke(communityAttestation); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + defaultCommunityId, + recipient.address + ) + )[0] + ).to.equal(0); + + expect( + ( + await gitcoinResolver["getCachedScore(uint32,address)"]( + testCommunityId, + recipient.address + ) + )[0] + ).to.equal(0); + }); + }); + }); });