-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(contracts): add eas project registry
- Loading branch information
Showing
4 changed files
with
269 additions
and
0 deletions.
There are no files selected for viewing
130 changes: 130 additions & 0 deletions
130
packages/contracts/contracts/extensions/registry/EASProjectRegistry.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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; | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
packages/contracts/tests/extensions/EASProjectRegistry.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
]); | ||
}); | ||
}); |