Skip to content

Commit

Permalink
feat(contracts): add registry manager
Browse files Browse the repository at this point in the history
- [x] Add RegistryManager contract
- [x] Add EASRegistryManager contract
- [x] Add Common contract with common errors
- [x] Minor refactoring
- [x] Add tests
  • Loading branch information
0xmad committed Aug 22, 2024
1 parent e72cfbb commit bde517b
Show file tree
Hide file tree
Showing 14 changed files with 772 additions and 13 deletions.
12 changes: 11 additions & 1 deletion packages/contracts/.solcover.js
Original file line number Diff line number Diff line change
@@ -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");

Expand All @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions packages/contracts/contracts/common/Common.sol
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 84 additions & 0 deletions packages/contracts/contracts/interfaces/IRegistryManager.sol
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 26 in packages/contracts/contracts/interfaces/IRegistryManager.sol

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"adderss" should be "address".
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);
}
13 changes: 13 additions & 0 deletions packages/contracts/contracts/mocks/MockRegistry.sol
Original file line number Diff line number Diff line change
@@ -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) {}
}
3 changes: 2 additions & 1 deletion packages/contracts/contracts/registry/BaseRegistry.sol
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
22 changes: 14 additions & 8 deletions packages/contracts/contracts/registry/EASRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 18 in packages/contracts/contracts/registry/EASRegistry.sol

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"Addess" should be "Address".
constructor(
uint256 _maxRecipients,
bytes32 _metadataUrl,
address _eas
) payable Ownable(msg.sender) BaseRegistry(_maxRecipients, _metadataUrl) {
eas = IEAS(_eas);
uint256 maxRecipients,
bytes32 metadataUrl,
address easAddess

Check warning on line 22 in packages/contracts/contracts/registry/EASRegistry.sol

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"Addess" should be "Address".
) payable Ownable(msg.sender) BaseRegistry(maxRecipients, metadataUrl) {
if (easAddess == address(0)) {

Check warning on line 24 in packages/contracts/contracts/registry/EASRegistry.sol

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"Addess" should be "Address".
revert InvalidAddress();
}

eas = IEAS(easAddess);

Check warning on line 28 in packages/contracts/contracts/registry/EASRegistry.sol

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"Addess" should be "Address".
}

/// @notice Add multiple recipients to the registry
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
122 changes: 122 additions & 0 deletions packages/contracts/contracts/registryManager/RegistryManager.sol
Original file line number Diff line number Diff line change
@@ -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];
}
}
5 changes: 5 additions & 0 deletions packages/contracts/tests/EASRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
Expand Down
Loading

0 comments on commit bde517b

Please sign in to comment.