diff --git a/contracts/GitcoinResolver.sol b/contracts/GitcoinResolver.sol index 8acee8d..c49db27 100644 --- a/contracts/GitcoinResolver.sol +++ b/contracts/GitcoinResolver.sol @@ -13,6 +13,8 @@ import { GitcoinAttester } from "./GitcoinAttester.sol"; import { IGitcoinResolver } from "./IGitcoinResolver.sol"; +import "hardhat/console.sol"; + /** * @title GitcoinResolver * @notice This contract is used to as a resolver contract for EAS schemas, and it will track the last attestation issued for a given recipient. @@ -43,6 +45,30 @@ contract GitcoinResolver is // List of addresses allowed to write to this contract mapping(address => bool) public allowlist; + // Score data for querying + struct Score { + uint32 score; // compacted uint value 4 decimal places + uint64 issuanceDate; // For checking the age of the stamp, without loading the attestation + uint64 expirationDate; // This makes sense because we want to make sure the stamp is not expired, and also do not want to load the attestation + uint32 scorerId; // would we need this ??? + } + + /** + * @dev A struct representing the passport score for an ETH address. + */ + // TODO: use same type from IGitcoinPassportDecoder ? + struct ScoreAttestation { + uint256 score; + uint32 scorerID; + uint8 decimals; + } + + // Mapping of addresses to scores + mapping(address => Score) public scores; + + // Mapping of active passport score schemas - used when storing scores to state + mapping(bytes32 => bool) public scoreSchemas; + /** * @dev Creates a new resolver. * @notice Initializer function responsible for setting up the contract's initial state. @@ -88,6 +114,22 @@ contract GitcoinResolver is _unpause(); } + /** + * @dev Set supported score schemas. + * @param schema The score schema uid + */ + function setScoreSchema(bytes32 schema) external onlyOwner { + scoreSchemas[schema] = true; + } + + /** + * @dev Unset supported score schemas. + * @param schema The score schema uid + */ + function unsetScoreSchema(bytes32 schema) external onlyOwner { + scoreSchemas[schema] = false; + } + // solhint-disable-next-line no-empty-blocks function _authorizeUpgrade(address) internal override onlyOwner {} @@ -108,6 +150,10 @@ contract GitcoinResolver is function attest( Attestation calldata attestation ) external payable whenNotPaused onlyAllowlisted returns (bool) { + if (scoreSchemas[attestation.schema]) { + _setScore(attestation); + } + return _attest(attestation); } @@ -122,6 +168,27 @@ contract GitcoinResolver is return true; } + /** + * @dev Stores Score data in state. + * @param attestation The new attestation. + */ + function _setScore(Attestation calldata attestation) private { + // Decode the score attestion output + (uint256 score, uint32 scorerId, ) = abi.decode( + attestation.data, + (uint256, uint32, uint8) + ); + + // TODO: save based on schema scores[schema][attestation.recipient] + scores[attestation.recipient] = Score( + // TODO: correct conversion to uint32 + uint32(score), + attestation.time, + attestation.expirationTime, + scorerId + ); + } + /** * @dev Processes multiple attestations and verifies whether they are valid. * @param attestations The new attestations. diff --git a/contracts/IGitcoinPassportDecoder.sol b/contracts/IGitcoinPassportDecoder.sol index 6e8a6e3..588d352 100644 --- a/contracts/IGitcoinPassportDecoder.sol +++ b/contracts/IGitcoinPassportDecoder.sol @@ -17,8 +17,8 @@ struct Credential { */ struct Score { uint256 score; - uint256 scorerID; - uint256 decimals; + uint32 scorerID; + uint8 decimals; } /** diff --git a/test/GitcoinResolver.ts b/test/GitcoinResolver.ts index edb10d3..68b8b42 100644 --- a/test/GitcoinResolver.ts +++ b/test/GitcoinResolver.ts @@ -5,14 +5,43 @@ import { NO_EXPIRATION, } from "@ethereum-attestation-service/eas-sdk"; import { GitcoinAttester, GitcoinResolver } from "../typechain-types"; -import { encodedData } from "./helpers/mockAttestations"; +import { easEncodeScore, encodedData } from "./helpers/mockAttestations"; import { SCHEMA_REGISTRY_ABI } from "./abi/SCHEMA_REGISTRY_ABI"; +import { BigNumber } from "@ethersproject/bignumber"; export const schemaRegistryContractAddress = process.env.SEPOLIA_SCHEMA_REGISTRY_ADDRESS || "0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0"; -describe("GitcoinResolver", function () { +async function registerSchema( + owner: any, + gitcoinResolverAddress: string, + schema: string +): Promise { + const schemaRegistry = new ethers.Contract( + schemaRegistryContractAddress, + SCHEMA_REGISTRY_ABI, + owner + ); + + const revocable = true; + + const transaction = await schemaRegistry.register( + schema, + gitcoinResolverAddress, + revocable + ); + + const registerTransactionReceipt = await transaction.wait(); + + const registerEvent = registerTransactionReceipt.logs.filter((log: any) => { + return log.fragment.name == "Registered"; + }); + + return registerEvent[0].args[0]; +} + +describe.only("GitcoinResolver", function () { let owner: any, iamAccount: any, recipient: any, @@ -42,7 +71,7 @@ describe("GitcoinResolver", function () { ); gitcoinAttester = await GitcoinAttester.deploy(); await gitcoinAttester.connect(owner).initialize(); - const gitcoinAttesterAddress = await gitcoinAttester.getAddress(); + this.gitcoinAttesterAddress = await gitcoinAttester.getAddress(); // Initialize the sdk with the address of the EAS Schema contract address await gitcoinAttester.setEASAddress(mockEas); @@ -54,45 +83,34 @@ describe("GitcoinResolver", function () { gitcoinResolver = await GitcoinResolver.deploy(); await gitcoinResolver .connect(owner) - .initialize(mockEas.address, gitcoinAttesterAddress); + .initialize(mockEas.address, this.gitcoinAttesterAddress); const gitcoinResolverAddress = await gitcoinResolver.getAddress(); this.uid = ethers.keccak256(ethers.toUtf8Bytes("test")); - const schemaRegistry = new ethers.Contract( - schemaRegistryContractAddress, - SCHEMA_REGISTRY_ABI, - owner + this.testSchemaUID = await registerSchema( + owner, + gitcoinResolverAddress, + "uint256 eventId, uint8 voteIndex" ); - - const schema = "uint256 eventId, uint8 voteIndex"; - const resolverAddress = gitcoinResolverAddress; - const revocable = true; - - const transaction = await schemaRegistry.register( - schema, - resolverAddress, - revocable + this.scoreSchemaId = await registerSchema( + owner, + gitcoinResolverAddress, + "uint256 score, uint8 scorer_id, uint8 score_decimals" ); - const registerTransactionReceipt = await transaction.wait(); - - const registerEvent = registerTransactionReceipt.logs.filter((log: any) => { - return log.fragment.name == "Registered"; - }); - - this.schemaUID = registerEvent[0].args[0]; + await gitcoinResolver.connect(owner).setScoreSchema(this.scoreSchemaId); this.validAttestation = { uid: this.uid, - schema: this.schemaUID, + schema: this.testSchemaUID, time: NO_EXPIRATION, expirationTime: NO_EXPIRATION, revocationTime: NO_EXPIRATION, refUID: ZERO_BYTES32, recipient: recipient.address, - attester: gitcoinAttesterAddress, + attester: this.gitcoinAttesterAddress, revocable: true, data: encodedData, }; @@ -104,7 +122,7 @@ describe("GitcoinResolver", function () { const attestationUID = await gitcoinResolver.userAttestations( recipient.address, - this.schemaUID + this.testSchemaUID ); expect(attestationUID).to.equal(this.uid); @@ -120,7 +138,7 @@ describe("GitcoinResolver", function () { const attestationUID = await gitcoinResolver.userAttestations( recipient.address, - this.schemaUID + this.testSchemaUID ); expect(attestationUID).to.equal(this.uid); @@ -136,7 +154,7 @@ describe("GitcoinResolver", function () { const uid = ethers.keccak256(ethers.toUtf8Bytes("test")); const attestation = { uid, - schema: this.schemaUID, + schema: this.testSchemaUID, time: NO_EXPIRATION, expirationTime: NO_EXPIRATION, revocationTime: NO_EXPIRATION, @@ -153,6 +171,35 @@ describe("GitcoinResolver", function () { }); }); + describe.only("Saving Scores", function () { + it("should save a score", async function () { + const bnScore = BigNumber.from("28287000000000000000"); + + const scoreAttestation = easEncodeScore({ + score: bnScore, + scorer_id: 1, + score_decimals: 0, + }); + const uid = ethers.keccak256(ethers.toUtf8Bytes("test")); + const attestation = { + uid, + schema: this.scoreSchemaId, + time: NO_EXPIRATION, + expirationTime: NO_EXPIRATION, + revocationTime: NO_EXPIRATION, + refUID: ZERO_BYTES32, + recipient: recipient.address, + attester: this.gitcoinAttesterAddress, + revocable: true, + data: scoreAttestation, + }; + + await gitcoinResolver.connect(mockEas).attest(attestation); + + const score = await gitcoinResolver.scores(recipient.address); + }); + }); + describe("Revocations", function () { it("should make 1 revocation", async function () { // Make an attestation @@ -176,7 +223,7 @@ describe("GitcoinResolver", function () { let attestationUID = await gitcoinResolver.userAttestations( recipient.address, - this.schemaUID + this.testSchemaUID ); expect(attestationUID).to.equal(ethers.ZeroHash); @@ -185,7 +232,7 @@ describe("GitcoinResolver", function () { it("should allow a eas to revoke a user's attestation", async function () { const validAttestation = { uid: this.uid, - schema: this.schemaUID, + schema: this.testSchemaUID, time: NO_EXPIRATION, expirationTime: NO_EXPIRATION, revocationTime: NO_EXPIRATION, @@ -202,7 +249,7 @@ describe("GitcoinResolver", function () { let attestationUID = await gitcoinResolver.userAttestations( recipient.address, - this.schemaUID + this.testSchemaUID ); expect(attestationUID).to.equal(ethers.ZeroHash); }); @@ -210,7 +257,7 @@ describe("GitcoinResolver", function () { it("should not allow non-EAS to revoke a user's attestations", async function () { const validAttestation = { uid: this.uid, - schema: this.schemaUID, + schema: this.testSchemaUID, time: NO_EXPIRATION, expirationTime: NO_EXPIRATION, revocationTime: NO_EXPIRATION, diff --git a/test/helpers/mockAttestations.ts b/test/helpers/mockAttestations.ts index e89ce9c..e9e7e63 100644 --- a/test/helpers/mockAttestations.ts +++ b/test/helpers/mockAttestations.ts @@ -3,6 +3,7 @@ import { SchemaEncoder, ZERO_BYTES32, NO_EXPIRATION, + SchemaValue, } from "@ethereum-attestation-service/eas-sdk"; export type Stamp = { @@ -11,7 +12,7 @@ export type Stamp = { }; export type Score = { - score: number; + score: SchemaValue; scorer_id: number; score_decimals: number; };