diff --git a/packages/contracts/.solcover.js b/packages/contracts/.solcover.js index 2885b4d1..f7a22964 100644 --- a/packages/contracts/.solcover.js +++ b/packages/contracts/.solcover.js @@ -1,4 +1,5 @@ -const { buildPoseidonT3, buildPoseidonT4, buildPoseidonT5, buildPoseidonT6 } = require("maci-contracts"); +const { poseidonContract } = require("circomlibjs"); +const hre = require("hardhat"); const fs = require("fs"); const path = require("path"); @@ -8,6 +9,15 @@ const PATHS = [ path.resolve(__dirname, "..", "typechain-types"), ]; +const buildPoseidon = async (numInputs) => { + await hre.overwriteArtifact(`PoseidonT${numInputs + 1}`, poseidonContract.createCode(numInputs)); +}; + +const buildPoseidonT3 = () => buildPoseidon(2); +const buildPoseidonT4 = () => buildPoseidon(3); +const buildPoseidonT5 = () => buildPoseidon(4); +const buildPoseidonT6 = () => buildPoseidon(5); + module.exports = { onPreCompile: async () => { await Promise.all( diff --git a/packages/contracts/contracts/common/Common.sol b/packages/contracts/contracts/common/Common.sol new file mode 100644 index 00000000..fa52d90a --- /dev/null +++ b/packages/contracts/contracts/common/Common.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @title Common +/// @notice Contract that contains common things for all the contracts +contract Common { + /// @notice custom errors + error InvalidAddress(); + error InvalidInput(); + error InvalidIndex(); + error ValidationError(); +} diff --git a/packages/contracts/contracts/interfaces/IRecipientRegistry.sol b/packages/contracts/contracts/interfaces/IRecipientRegistry.sol index 4b87b5d1..c6b74988 100644 --- a/packages/contracts/contracts/interfaces/IRecipientRegistry.sol +++ b/packages/contracts/contracts/interfaces/IRecipientRegistry.sol @@ -21,8 +21,6 @@ interface IRecipientRegistry { /// @notice Custom errors error MaxRecipientsReached(); - error InvalidIndex(); - error InvalidInput(); /// @notice Get a registry metadata url /// @return The metadata url in bytes32 format diff --git a/packages/contracts/contracts/interfaces/IRegistryManager.sol b/packages/contracts/contracts/interfaces/IRegistryManager.sol new file mode 100644 index 00000000..d3990f85 --- /dev/null +++ b/packages/contracts/contracts/interfaces/IRegistryManager.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IRecipientRegistry } from "./IRecipientRegistry.sol"; + +/// @title IRegistryManager +/// @notice An interface for a registry manager. Allows to manage requests for Registry. +interface IRegistryManager { + /// @notice Enum representing request type + enum RequestType { + Add, + Change + } + + /// @notice Enum representing request status + enum Status { + Pending, + Approved, + Rejected + } + + /// @notice Request data + struct Request { + /// @notice index (optional) + uint256 index; + /// @notice registry adderss + address registry; + /// @notice request type + RequestType requestType; + /// @notice recipient data + IRecipientRegistry.Recipient recipient; + } + + /// @notice Events + event RequestSent( + address indexed registry, + RequestType indexed requestType, + address indexed recipient, + uint256 index, + bytes32 id, + bytes32 metadataUrl + ); + event RequestApproved( + address indexed registry, + RequestType indexed requestType, + address indexed recipient, + uint256 index, + bytes32 id, + bytes32 metadataUrl + ); + event RequestRejected( + address indexed registry, + RequestType indexed requestType, + address indexed recipient, + uint256 index, + bytes32 id, + bytes32 metadataUrl + ); + + /// @notice Custom errors + error OperationError(); + + /// @notice Send the request to the Registry + /// @param request user request + function send(Request calldata request) external; + + /// @notice Approve the request and call registry function + /// @param index The index of the request + function approve(uint256 index) external; + + /// @notice Reject the request + /// @param index The index of the request + function reject(uint256 index) external; + + /// @notice Get a request + /// @param index The index of the request + /// @return request The request with index and data + /// @return status The request status + function getRequest(uint256 index) external view returns (Request memory request, Status status); + + /// @notice Get the number of requests + /// @return The number of requests + function requestCount() external view returns (uint256); +} diff --git a/packages/contracts/contracts/mocks/MockRegistry.sol b/packages/contracts/contracts/mocks/MockRegistry.sol new file mode 100644 index 00000000..3a519348 --- /dev/null +++ b/packages/contracts/contracts/mocks/MockRegistry.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { BaseRegistry } from "../registry/BaseRegistry.sol"; + +/// @title MockRegistry +/// @notice Mock registry contract +contract MockRegistry is BaseRegistry { + /// @notice Create a new instance of the registry contract + /// @param maxRecipients The maximum number of projects that can be registered + /// @param metadataUrl The metadata url + constructor(uint256 maxRecipients, bytes32 metadataUrl) payable BaseRegistry(maxRecipients, metadataUrl) {} +} diff --git a/packages/contracts/contracts/registry/BaseRegistry.sol b/packages/contracts/contracts/registry/BaseRegistry.sol index 8c98aa20..051f7a96 100644 --- a/packages/contracts/contracts/registry/BaseRegistry.sol +++ b/packages/contracts/contracts/registry/BaseRegistry.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import { Common } from "../common/Common.sol"; import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; /// @title BaseRegistry /// @notice Base contract for a registry -abstract contract BaseRegistry is IRecipientRegistry { +abstract contract BaseRegistry is IRecipientRegistry, Common { /// @notice The storage of recipients mapping(uint256 => Recipient) internal recipients; diff --git a/packages/contracts/contracts/registry/EASRegistry.sol b/packages/contracts/contracts/registry/EASRegistry.sol index eee927fd..c8c838a4 100644 --- a/packages/contracts/contracts/registry/EASRegistry.sol +++ b/packages/contracts/contracts/registry/EASRegistry.sol @@ -6,20 +6,26 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IEAS } from "../interfaces/IEAS.sol"; import { BaseRegistry } from "./BaseRegistry.sol"; +/// @title EASRegistry +/// @notice EAS registry contract contract EASRegistry is Ownable, BaseRegistry, IEAS { /// @notice The EAS contract IEAS public immutable eas; /// @notice Create a new instance of the registry contract - /// @param _maxRecipients The maximum number of projects that can be registered - /// @param _metadataUrl The metadata url - /// @param _eas The EAS address + /// @param maxRecipients The maximum number of projects that can be registered + /// @param metadataUrl The metadata url + /// @param easAddess The EAS address constructor( - uint256 _maxRecipients, - bytes32 _metadataUrl, - address _eas - ) payable Ownable(msg.sender) BaseRegistry(_maxRecipients, _metadataUrl) { - eas = IEAS(_eas); + uint256 maxRecipients, + bytes32 metadataUrl, + address easAddess + ) payable Ownable(msg.sender) BaseRegistry(maxRecipients, metadataUrl) { + if (easAddess == address(0)) { + revert InvalidAddress(); + } + + eas = IEAS(easAddess); } /// @notice Add multiple recipients to the registry diff --git a/packages/contracts/contracts/registryManager/EASRegistryManager.sol b/packages/contracts/contracts/registryManager/EASRegistryManager.sol new file mode 100644 index 00000000..e7ed7ce5 --- /dev/null +++ b/packages/contracts/contracts/registryManager/EASRegistryManager.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IEAS } from "../interfaces/IEAS.sol"; +import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; +import { RegistryManager } from "./RegistryManager.sol"; + +/// @title EASRegistryManager +/// @notice Contract that allows to use send, approve, reject requests to EASRegistry. +contract EASRegistryManager is RegistryManager { + /// @notice custom errors + error NotYourAttestation(); + + /// @notice EAS + IEAS public eas; + + /// @notice Initialize EASRegistryManager + /// @param easAddress EAS contract address + constructor(address easAddress) payable { + if (easAddress == address(0)) { + revert InvalidAddress(); + } + + eas = IEAS(easAddress); + } + + /// @notice Check recipient has an EAS attestation + /// @param request request to the registry + modifier onlyWithAttestation(Request memory request) { + if (request.requestType != RequestType.Change) { + _; + return; + } + + IEAS.Attestation memory attestation = eas.getAttestation(request.recipient.id); + + if (attestation.recipient != request.recipient.recipient) { + revert NotYourAttestation(); + } + + _; + } + + /// @inheritdoc RegistryManager + function send( + Request calldata request + ) public virtual override isValidRequest(request) onlyWithAttestation(request) { + super.send(request); + } +} diff --git a/packages/contracts/contracts/registryManager/RegistryManager.sol b/packages/contracts/contracts/registryManager/RegistryManager.sol new file mode 100644 index 00000000..101c89b2 --- /dev/null +++ b/packages/contracts/contracts/registryManager/RegistryManager.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { Common } from "../common/Common.sol"; +import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; +import { IRegistryManager } from "../interfaces/IRegistryManager.sol"; + +/// @title RegistryManager +/// @notice Contract that allows to use send, approve, reject requests to RecipientRegistry. +contract RegistryManager is Ownable, IRegistryManager, Common { + /// @notice requests + mapping(uint256 => Request) internal requests; + + /// @notice request statuses + mapping(uint256 => Status) internal statuses; + + /// @inheritdoc IRegistryManager + uint256 public requestCount; + + /// @notice Initialize registry manager + constructor() payable Ownable(msg.sender) {} + + /// @notice Check if request is valid + modifier isValidRequest(Request calldata request) { + if (request.registry == address(0)) { + revert ValidationError(); + } + + if (request.recipient.recipient == address(0)) { + revert ValidationError(); + } + + uint256 count = IRecipientRegistry(request.registry).recipientCount(); + + if (request.index >= count && request.requestType == RequestType.Change) { + revert ValidationError(); + } + + _; + } + + /// @notice Check if request is pending and exists + /// @param index Request index + modifier isPending(uint256 index) { + if (index >= requestCount || statuses[index] != Status.Pending) { + revert OperationError(); + } + _; + } + + /// @inheritdoc IRegistryManager + function send(Request calldata request) public virtual override isValidRequest(request) { + requests[requestCount] = request; + statuses[requestCount] = Status.Pending; + + unchecked { + requestCount++; + } + + emit RequestSent( + request.registry, + request.requestType, + request.recipient.recipient, + request.index, + request.recipient.id, + request.recipient.metadataUrl + ); + } + + /// @inheritdoc IRegistryManager + function approve(uint256 index) public virtual override onlyOwner isPending(index) { + Request memory request = requests[index]; + IRecipientRegistry registry = IRecipientRegistry(request.registry); + + if (request.requestType == RequestType.Change) { + approveRequest(index, request); + registry.changeRecipient(request.index, request.recipient); + } else { + approveRequest(index, request); + registry.addRecipient(request.recipient); + } + } + + /// @notice Internal approve request with state change + /// @param index Index of the request (optional) + /// @param request Request to the registry + function approveRequest(uint256 index, Request memory request) internal { + statuses[index] = Status.Approved; + + emit RequestApproved( + request.registry, + request.requestType, + request.recipient.recipient, + request.index, + request.recipient.id, + request.recipient.metadataUrl + ); + } + + /// @inheritdoc IRegistryManager + function reject(uint256 index) public virtual override onlyOwner isPending(index) { + Request memory request = requests[index]; + statuses[index] = Status.Rejected; + + emit RequestRejected( + request.registry, + request.requestType, + request.recipient.recipient, + request.index, + request.recipient.id, + request.recipient.metadataUrl + ); + } + + /// @inheritdoc IRegistryManager + function getRequest(uint256 index) public view virtual override returns (Request memory request, Status status) { + request = requests[index]; + status = statuses[index]; + } +} diff --git a/packages/contracts/tests/EASRegistry.test.ts b/packages/contracts/tests/EASRegistry.test.ts index cb395a7f..a2b5c4d5 100644 --- a/packages/contracts/tests/EASRegistry.test.ts +++ b/packages/contracts/tests/EASRegistry.test.ts @@ -25,6 +25,11 @@ describe("EASRegistry", () => { [ownerAddress, userAddress] = await Promise.all([owner.getAddress(), user.getAddress()]); mockEAS = await deployContract("MockEAS", owner, true, ownerAddress, schema, userAddress); + const common = await deployContract("Common", owner, true); + + await expect( + deployContract("EASRegistry", owner, true, maxRecipients, metadataUrl, ZeroAddress), + ).to.be.revertedWithCustomError(common, "InvalidAddress"); registry = await deployContract("EASRegistry", owner, true, maxRecipients, metadataUrl, await mockEAS.getAddress()); }); diff --git a/packages/contracts/tests/EASRegistryManager.test.ts b/packages/contracts/tests/EASRegistryManager.test.ts new file mode 100644 index 00000000..ee7054dc --- /dev/null +++ b/packages/contracts/tests/EASRegistryManager.test.ts @@ -0,0 +1,152 @@ +import { expect } from "chai"; +import { encodeBytes32String, Signer, ZeroAddress } from "ethers"; +import { getSigners, deployContract } from "maci-contracts"; + +import { ERegistryManagerRequestStatus, ERegistryManagerRequestType } from "../ts"; +import { MockRegistry, EASRegistryManager, MockEAS } from "../typechain-types"; + +describe("EASRegistryManager", () => { + let registryManager: EASRegistryManager; + let mockEAS: MockEAS; + let mockRegistry: MockRegistry; + let owner: Signer; + let user: Signer; + + let ownerAddress: string; + let userAddress: string; + + const schema = "0xfdcfdad2dbe7489e0ce56b260348b7f14e8365a8a325aef9834818c00d46b31b"; + const attestation = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const newAttestation = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const metadataUrl = encodeBytes32String("url"); + const maxRecipients = 5; + + before(async () => { + [owner, user] = await getSigners(); + [ownerAddress, userAddress] = await Promise.all([owner.getAddress(), user.getAddress()]); + + mockEAS = await deployContract("MockEAS", owner, true, ownerAddress, schema, userAddress); + mockRegistry = await deployContract("MockRegistry", owner, true, maxRecipients, metadataUrl); + + const common = await deployContract("Common", owner, true); + + await expect(deployContract("EASRegistryManager", owner, true, ZeroAddress)).to.be.revertedWithCustomError( + common, + "InvalidAddress", + ); + + registryManager = await deployContract("EASRegistryManager", owner, true, await mockEAS.getAddress()); + + await registryManager.connect(user).send({ + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Add, + recipient: { + id: attestation, + recipient: ownerAddress, + metadataUrl, + }, + }); + }); + + it("should not allow non-owner to approve requests to the registry", async () => { + await expect(registryManager.connect(user).approve(0)).to.be.revertedWithCustomError( + registryManager, + "OwnableUnauthorizedAccount", + ); + }); + + it("should allow owner to approve requests to the registry", async () => { + const [addRequest, addRequestStatus] = await registryManager.getRequest(0); + + expect(addRequestStatus).to.equal(ERegistryManagerRequestStatus.Pending); + + await expect(registryManager.connect(owner).approve(0)) + .to.emit(registryManager, "RequestApproved") + .withArgs( + addRequest.registry, + addRequest.requestType, + addRequest.recipient.recipient, + addRequest.index, + addRequest.recipient.id, + addRequest.recipient.metadataUrl, + ); + + const changeRequest = { + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Change, + recipient: { + id: attestation, + recipient: userAddress, + metadataUrl, + }, + }; + + await expect(registryManager.connect(user).send(changeRequest)) + .to.emit(registryManager, "RequestSent") + .withArgs( + changeRequest.registry, + changeRequest.requestType, + changeRequest.recipient.recipient, + changeRequest.index, + changeRequest.recipient.id, + changeRequest.recipient.metadataUrl, + ); + + await expect(registryManager.connect(owner).approve(1)) + .to.emit(registryManager, "RequestApproved") + .withArgs( + changeRequest.registry, + changeRequest.requestType, + changeRequest.recipient.recipient, + changeRequest.index, + changeRequest.recipient.id, + changeRequest.recipient.metadataUrl, + ); + + const [[, updatedAddRequestStatus], [, updatedChangeRequestStatus], recipient, recipientCount] = await Promise.all([ + registryManager.getRequest(0), + registryManager.getRequest(1), + mockRegistry.getRecipient(0), + mockRegistry.recipientCount(), + ]); + + expect(updatedAddRequestStatus).to.equal(ERegistryManagerRequestStatus.Approved); + expect(updatedChangeRequestStatus).to.equal(ERegistryManagerRequestStatus.Approved); + expect(recipient.id).to.equal(changeRequest.recipient.id); + expect(recipient.recipient).to.equal(changeRequest.recipient.recipient); + expect(recipient.metadataUrl).to.equal(changeRequest.recipient.metadataUrl); + expect(recipientCount).to.equal(1); + }); + + it("should not allow to send requests to the registry with invalid request", async () => { + await expect( + registryManager.connect(owner).send({ + index: 1, + registry: ZeroAddress, + requestType: ERegistryManagerRequestType.Change, + recipient: { + id: newAttestation, + recipient: ZeroAddress, + metadataUrl, + }, + }), + ).to.be.revertedWithCustomError(registryManager, "ValidationError"); + }); + + it("should not allow to send requests to the registry with invalid attestation", async () => { + await expect( + registryManager.connect(owner).send({ + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Change, + recipient: { + id: newAttestation, + recipient: ownerAddress, + metadataUrl, + }, + }), + ).to.be.revertedWithCustomError(registryManager, "NotYourAttestation"); + }); +}); diff --git a/packages/contracts/tests/RegistryManager.test.ts b/packages/contracts/tests/RegistryManager.test.ts new file mode 100644 index 00000000..47295f3f --- /dev/null +++ b/packages/contracts/tests/RegistryManager.test.ts @@ -0,0 +1,290 @@ +import { expect } from "chai"; +import { encodeBytes32String, Signer, ZeroAddress } from "ethers"; +import { getSigners, deployContract } from "maci-contracts"; + +import { ERegistryManagerRequestStatus, ERegistryManagerRequestType } from "../ts"; +import { RegistryManager, MockRegistry } from "../typechain-types"; + +describe("RegistryManager", () => { + let registryManager: RegistryManager; + let mockRegistry: MockRegistry; + let owner: Signer; + let user: Signer; + + let ownerAddress: string; + let userAddress: string; + + const attestation = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const metadataUrl = encodeBytes32String("url"); + const maxRecipients = 5; + + before(async () => { + [owner, user] = await getSigners(); + [ownerAddress, userAddress] = await Promise.all([owner.getAddress(), user.getAddress()]); + + mockRegistry = await deployContract("MockRegistry", owner, true, maxRecipients, metadataUrl); + + registryManager = await deployContract("RegistryManager", owner, true); + }); + + it("should not allow user to send invalid requests to the registry", async () => { + await expect( + registryManager.connect(user).send({ + index: 0, + registry: ZeroAddress, + requestType: ERegistryManagerRequestType.Add, + recipient: { + id: attestation, + recipient: ownerAddress, + metadataUrl, + }, + }), + ).to.be.revertedWithCustomError(registryManager, "ValidationError"); + + await expect( + registryManager.connect(user).send({ + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Add, + recipient: { + id: attestation, + recipient: ZeroAddress, + metadataUrl, + }, + }), + ).to.be.revertedWithCustomError(registryManager, "ValidationError"); + + await expect( + registryManager.connect(user).send({ + index: 1, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Change, + recipient: { + id: attestation, + recipient: userAddress, + metadataUrl, + }, + }), + ).to.be.revertedWithCustomError(registryManager, "ValidationError"); + + expect(await registryManager.requestCount()).to.equal(0); + }); + + it("should allow user to send requests to the registry", async () => { + const addRequest = { + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Add, + recipient: { + id: attestation, + recipient: ownerAddress, + metadataUrl, + }, + }; + + await expect(registryManager.connect(user).send(addRequest)) + .to.emit(registryManager, "RequestSent") + .withArgs( + addRequest.registry, + addRequest.requestType, + addRequest.recipient.recipient, + addRequest.index, + addRequest.recipient.id, + addRequest.recipient.metadataUrl, + ); + + expect(await registryManager.requestCount()).to.equal(1); + }); + + it("should not allow non-owner to approve requests to the registry", async () => { + await expect(registryManager.connect(user).approve(0)).to.be.revertedWithCustomError( + registryManager, + "OwnableUnauthorizedAccount", + ); + }); + + it("should not allow non-owner to reject requests to the registry", async () => { + await expect(registryManager.connect(user).reject(0)).to.be.revertedWithCustomError( + registryManager, + "OwnableUnauthorizedAccount", + ); + }); + + it("should not allow to approve requests to the registry with invalid index", async () => { + await expect(registryManager.connect(owner).approve(9000)).to.be.revertedWithCustomError( + registryManager, + "OperationError", + ); + }); + + it("should not allow to reject requests to the registry with invalid index", async () => { + await expect(registryManager.connect(owner).reject(9000)).to.be.revertedWithCustomError( + registryManager, + "OperationError", + ); + }); + + it("should allow owner to approve requests to the registry", async () => { + const [addRequest, addRequestStatus] = await registryManager.getRequest(0); + + expect(addRequestStatus).to.equal(ERegistryManagerRequestStatus.Pending); + + await expect(registryManager.connect(owner).approve(0)) + .to.emit(registryManager, "RequestApproved") + .withArgs( + addRequest.registry, + addRequest.requestType, + addRequest.recipient.recipient, + addRequest.index, + addRequest.recipient.id, + addRequest.recipient.metadataUrl, + ); + + const changeRequest = { + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Change, + recipient: { + id: attestation, + recipient: userAddress, + metadataUrl, + }, + }; + + await expect(registryManager.connect(user).send(changeRequest)) + .to.emit(registryManager, "RequestSent") + .withArgs( + changeRequest.registry, + changeRequest.requestType, + changeRequest.recipient.recipient, + changeRequest.index, + changeRequest.recipient.id, + changeRequest.recipient.metadataUrl, + ); + + await expect(registryManager.connect(owner).approve(1)) + .to.emit(registryManager, "RequestApproved") + .withArgs( + changeRequest.registry, + changeRequest.requestType, + changeRequest.recipient.recipient, + changeRequest.index, + changeRequest.recipient.id, + changeRequest.recipient.metadataUrl, + ); + + const [[, updatedAddRequestStatus], [, updatedChangeRequestStatus], recipient, recipientCount] = await Promise.all([ + registryManager.getRequest(0), + registryManager.getRequest(1), + mockRegistry.getRecipient(0), + mockRegistry.recipientCount(), + ]); + + expect(updatedAddRequestStatus).to.equal(ERegistryManagerRequestStatus.Approved); + expect(updatedChangeRequestStatus).to.equal(ERegistryManagerRequestStatus.Approved); + expect(recipient.id).to.equal(changeRequest.recipient.id); + expect(recipient.recipient).to.equal(changeRequest.recipient.recipient); + expect(recipient.metadataUrl).to.equal(changeRequest.recipient.metadataUrl); + expect(recipientCount).to.equal(1); + }); + + it("should not allow to approve requests to the registry twice", async () => { + await expect(registryManager.connect(owner).approve(0)).to.be.revertedWithCustomError( + registryManager, + "OperationError", + ); + + await expect(registryManager.connect(owner).approve(1)).to.be.revertedWithCustomError( + registryManager, + "OperationError", + ); + }); + + it("should allow owner to reject requests to the registry", async () => { + const addRequest = { + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Add, + recipient: { + id: attestation, + recipient: ownerAddress, + metadataUrl, + }, + }; + + const changeRequest = { + index: 0, + registry: await mockRegistry.getAddress(), + requestType: ERegistryManagerRequestType.Change, + recipient: { + id: attestation, + recipient: userAddress, + metadataUrl, + }, + }; + + await expect(registryManager.connect(user).send(addRequest)) + .to.emit(registryManager, "RequestSent") + .withArgs( + addRequest.registry, + addRequest.requestType, + addRequest.recipient.recipient, + addRequest.index, + addRequest.recipient.id, + addRequest.recipient.metadataUrl, + ); + + await expect(registryManager.connect(user).send(changeRequest)) + .to.emit(registryManager, "RequestSent") + .withArgs( + changeRequest.registry, + changeRequest.requestType, + changeRequest.recipient.recipient, + changeRequest.index, + changeRequest.recipient.id, + changeRequest.recipient.metadataUrl, + ); + + await expect(registryManager.connect(owner).reject(2)) + .to.emit(registryManager, "RequestRejected") + .withArgs( + addRequest.registry, + addRequest.requestType, + addRequest.recipient.recipient, + addRequest.index, + addRequest.recipient.id, + addRequest.recipient.metadataUrl, + ); + + await expect(registryManager.connect(owner).reject(3)) + .to.emit(registryManager, "RequestRejected") + .withArgs( + changeRequest.registry, + changeRequest.requestType, + changeRequest.recipient.recipient, + changeRequest.index, + changeRequest.recipient.id, + changeRequest.recipient.metadataUrl, + ); + + const [[, addRequestStatus], [, changeRequestStatus]] = await Promise.all([ + registryManager.getRequest(2), + registryManager.getRequest(3), + ]); + + expect(addRequestStatus).to.equal(ERegistryManagerRequestStatus.Rejected); + expect(changeRequestStatus).to.equal(ERegistryManagerRequestStatus.Rejected); + }); + + it("should not allow to reject requests to the registry twice", async () => { + await expect(registryManager.connect(owner).reject(2)).to.be.revertedWithCustomError( + registryManager, + "OperationError", + ); + + await expect(registryManager.connect(owner).reject(3)).to.be.revertedWithCustomError( + registryManager, + "OperationError", + ); + }); +}); diff --git a/packages/contracts/ts/constants.ts b/packages/contracts/ts/constants.ts new file mode 100644 index 00000000..fa4c58b4 --- /dev/null +++ b/packages/contracts/ts/constants.ts @@ -0,0 +1,16 @@ +/** + * Enum representing request type + */ +export enum ERegistryManagerRequestType { + Add, + Change, +} + +/** + * Enum representing request status + */ +export enum ERegistryManagerRequestStatus { + Pending, + Approved, + Rejected, +} diff --git a/packages/contracts/ts/index.ts b/packages/contracts/ts/index.ts index cb0ff5c3..98027a6c 100644 --- a/packages/contracts/ts/index.ts +++ b/packages/contracts/ts/index.ts @@ -1 +1 @@ -export {}; +export { ERegistryManagerRequestType, ERegistryManagerRequestStatus } from "./constants";