Skip to content

Commit

Permalink
feat(contracts): add eas project registry
Browse files Browse the repository at this point in the history
  • Loading branch information
0xmad committed Aug 9, 2024
1 parent ba62b2e commit 60fdc2c
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { IEAS } from "../../interfaces/IEAS.sol";

/// @title EASProjectRegistry
/// @notice This contract is a simple registry of projects
/// @dev This does not constrain the number of projects
/// which might be > vote options
/// @dev it does not prevent duplicate projects from being
/// added as projects
/// @notice It allows the attestation owners to change their own address
contract EASProjectRegistry is Ownable(msg.sender) {
/// @notice a Structure holding the attestationId and the projectAddress
/// for each approved project
struct EASProject {
bytes32 attestationId;
bytes32 metadataUrl;
address project;
}

/// @notice The EAS contract
IEAS public immutable eas;

/// @notice The registry metadata url
bytes32 public immutable metadataUrl;

// simple storage of projects is an array of EASProject
// with the the index being the position in the array
EASProject[] internal projects;

/// @notice The maximum number of projects that can be registered
uint256 public immutable maxProjects;

/// @notice Events
event ProjectAdded(bytes32 indexed attestationId, bytes32 metadataUrl, uint256 indexed index);
event ProjectAddressChanged(uint256 indexed index, bytes32 newMetadataUrl, address newAddress);

/// @notice Custom errors
error MaxProjectsReached();
error NotYourAttestation();

/// @notice Create a new instance of the registry contract
/// @param _maxProjects The maximum number of projects that can be registered
/// @param _metadataUrl The metadata url
/// @param _eas The EAS adddress

Check warning on line 48 in packages/contracts/contracts/extensions/registry/EASProjectRegistry.sol

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"adddress" should be "address".
constructor(uint256 _maxProjects, bytes32 _metadataUrl, address _eas) payable {
maxProjects = _maxProjects;
metadataUrl = _metadataUrl;
eas = IEAS(_eas);
}

/// @notice Add a project to the registry
/// @param project The details of the project to add
function addProject(EASProject calldata project) public onlyOwner {
// we first check if we already have reached the max number of projects
if (projects.length >= maxProjects) revert MaxProjectsReached();

// add it
projects.push(project);

// event for subgraph
emit ProjectAdded(project.attestationId, project.metadataUrl, projects.length - 1);
}

/// @notice Add multiple projects to the registry
/// @param _projects The addresses of the projects to add
function addProjects(EASProject[] calldata _projects) external onlyOwner {
uint256 len = _projects.length;
for (uint256 i = 0; i < len; ) {
// call addProject for each project to benefit from the length check
// and event emitting
addProject(_projects[i]);

unchecked {
i++;
}
}
}

/// @notice Edit the address of a project
/// @param index The index of the project to edit
/// @param newMetadataUrl The new metadata url
/// @param newAddress The new address of the project
function editAddress(uint256 index, bytes32 newMetadataUrl, address newAddress) external {
// 1. we get the attestation id
bytes32 attestationId = projects[index].attestationId;

// 2. we get the attestation data from the EAS registry
IEAS.Attestation memory attestation = eas.getAttestation(attestationId);

// 3. check if caller is the attestation receiver
if (attestation.recipient != msg.sender) revert NotYourAttestation();

// 4. change the project address and metadata url
projects[index].project = newAddress;
projects[index].metadataUrl = newMetadataUrl;

// 5. emit event
emit ProjectAddressChanged(index, newMetadataUrl, newAddress);
}

/// @notice Get a project from the registry
/// @param index The index of the project to get
/// @return The project address at the given index
function getProjectAddress(uint256 index) external view returns (address) {
return projects[index].project;
}

/// @notice Get a project from the registry
/// @param index The index of the project to get
/// @return The project at the given index
function getProject(uint256 index) external view returns (EASProject memory) {
return projects[index];
}

/// @notice Get all projects from the registry
/// @return The projects
function getProjects() external view returns (EASProject[] memory) {
return projects;
}

/// @notice Get the number of projects in the registry
/// @return The number of projects
function getProjectsNumber() external view returns (uint256) {
return projects.length;
}
}
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"test:semaphore_gatekeeper": "pnpm run test ./tests/SemaphoreGatekeeper.test.ts",
"test:simpleProjectRegistry": "pnpm run test ./tests/extensions/SimpleProjectRegistry.test.ts",
"test:simplePayout": "pnpm run test ./tests/extensions/SimplePayout.test.ts",
"test:easProjectRegistry": "pnpm run test ./tests/extensions/EASProjectRegistry.test.ts",
"deploy": "hardhat deploy-full",
"deploy-poll": "hardhat deploy-poll",
"verify": "hardhat verify-full",
Expand Down
138 changes: 138 additions & 0 deletions packages/contracts/tests/extensions/EASProjectRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { expect } from "chai";
import { encodeBytes32String, Signer, ZeroAddress } from "ethers";

import { deployContract, getSigners } from "../../ts";
import { EASProjectRegistry, MockEAS } from "../../typechain-types";

describe("EASProjectRegistry", () => {
let projectRegistry: EASProjectRegistry;
let mockEAS: MockEAS;
let owner: Signer;
let user: Signer;

let ownerAddress: string;
let userAddress: string;

const maxProjects = 5;

const schema = "0xfdcfdad2dbe7489e0ce56b260348b7f14e8365a8a325aef9834818c00d46b31b";
const attestation = "0x0000000000000000000000000000000000000000000000000000000000000000";
const metadataUrl = encodeBytes32String("url");

before(async () => {
[owner, user] = await getSigners();
[ownerAddress, userAddress] = await Promise.all([owner.getAddress(), user.getAddress()]);

mockEAS = await deployContract("MockEAS", owner, true, ownerAddress, schema, userAddress);

projectRegistry = await deployContract(
"EASProjectRegistry",
owner,
true,
maxProjects,
metadataUrl,
await mockEAS.getAddress(),
);
});

it("should allow the owner to add a project", async () => {
expect(
await projectRegistry.addProject({
attestationId: attestation,
project: ownerAddress,
metadataUrl,
}),
)
.to.emit(projectRegistry, "ProjectAdded")
.withArgs(attestation, metadataUrl, 0);
});

it("should allow the owner to add multiple projects", async () => {
const tx = await projectRegistry.addProjects([
{
attestationId: attestation,
project: ownerAddress,
metadataUrl,
},
{
attestationId: attestation,
project: userAddress,
metadataUrl,
},
{
attestationId: attestation,
project: ownerAddress,
metadataUrl,
},
]);

await Promise.all([
expect(tx).to.emit(projectRegistry, "ProjectAdded").withArgs(attestation, metadataUrl, 1),
expect(tx).to.emit(projectRegistry, "ProjectAdded").withArgs(attestation, metadataUrl, 2),
expect(tx).to.emit(projectRegistry, "ProjectAdded").withArgs(attestation, metadataUrl, 3),
]);
});

it("should throw if the caller is not the owner", async () => {
await expect(
projectRegistry.connect(user).addProject({
attestationId: attestation,
project: ownerAddress,
metadataUrl,
}),
).to.be.revertedWithCustomError(projectRegistry, "OwnableUnauthorizedAccount");
});

it("should throw if the max projects is reached", async () => {
// add one more
await projectRegistry.addProject({
attestationId: attestation,
project: ownerAddress,
metadataUrl,
});

// next one will fail
await expect(
projectRegistry.addProject({
attestationId: attestation,
project: ownerAddress,
metadataUrl,
}),
).to.be.revertedWithCustomError(projectRegistry, "MaxProjectsReached");
});

it("should allow a project owner to change their project address", async () => {
expect(await projectRegistry.connect(user).editAddress(2n, metadataUrl, ZeroAddress))
.to.emit(projectRegistry, "ProjectAddressChanged")
.withArgs(2n, metadataUrl, ZeroAddress);

expect(await projectRegistry.getProjectAddress(2n)).to.equal(ZeroAddress);
});

it("should fail to change the project address if the caller is not the project owner", async () => {
await expect(
projectRegistry.connect(owner).editAddress(2n, metadataUrl, ZeroAddress),
).to.be.revertedWithCustomError(projectRegistry, "NotYourAttestation");
});

it("should return the correct address given an index", async () => {
expect(await projectRegistry.getProjectAddress(0n)).to.equal(ownerAddress);
expect(await projectRegistry.getProjectAddress(1n)).to.equal(ownerAddress);
expect(await projectRegistry.getProjectAddress(2n)).to.equal(ZeroAddress);
expect(await projectRegistry.getProjectAddress(3n)).to.equal(ownerAddress);
});

it("should return the correct project data given an index", async () => {
expect(await projectRegistry.getProject(0n)).to.deep.equal([attestation, metadataUrl, ownerAddress]);
});

it("should return the full list of projects", async () => {
expect(await projectRegistry.getProjects()).to.deep.equal([
[attestation, metadataUrl, ownerAddress],
[attestation, metadataUrl, ownerAddress],
[attestation, metadataUrl, ZeroAddress],
[attestation, metadataUrl, ownerAddress],
[attestation, metadataUrl, ownerAddress],
]);
});
});

0 comments on commit 60fdc2c

Please sign in to comment.