Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contracts): add first registry and payout extensions #1631

Merged
merged 2 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title ISimpleProjectRegistry
/// @notice An interface for a simple project registry
interface ISimpleProjectRegistry {
/// @notice A struct representing a single registry project.
struct RegistryProject {
// project metadata url
bytes32 metadataUrl;
// project address
address project;
}

/// @notice Get a registry metadata url
/// @return The metadata url in bytes32 format
function getRegistryMetadataUrl() external view returns (bytes32);

/// @notice Get a project
/// @param index The index of the project
/// @return The address of the project
function getProject(uint256 index) external view returns (RegistryProject memory);

/// @notice Get all projects
/// @return The addresses of the projects
function getProjects() external view returns (RegistryProject[] memory);

/// @notice Get the number of projects
/// @return The number of projects
function getProjectsNumber() external view returns (uint256);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

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

/// @title SimpleProjectRegistry
/// @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 addresses from being
/// added as projects
/// @notice it does not allow to remove projects either
contract SimpleProjectRegistry is Ownable(msg.sender), ISimpleProjectRegistry {
/// @notice simple storage of projects is an array of addresses with the the index being the position in the array
RegistryProject[] internal projects;

/// @notice registry metadata url
bytes32 internal immutable metadataUrl;

/// @notice Create a new instance of the registry contract
/// @param _metadataUrl the registry metadata url
constructor(bytes32 _metadataUrl) payable {
metadataUrl = _metadataUrl;
}

/// @notice Add a project to the registry
/// @param project The address of the project to add
function addProject(RegistryProject calldata project) external onlyOwner {
projects.push(project);
}

/// @notice Add multiple projects to the registry
/// @param _projects The addresses of the projects to add
function addProjects(RegistryProject[] calldata _projects) external onlyOwner {
Dismissed Show dismissed Hide dismissed
uint256 len = _projects.length;
for (uint256 i = 0; i < len; ) {
projects.push(_projects[i]);

unchecked {
i++;
}
}
}

/// @inheritdoc ISimpleProjectRegistry
function getRegistryMetadataUrl() external view returns (bytes32) {
return metadataUrl;
}

/// @inheritdoc ISimpleProjectRegistry
function getProject(uint256 index) external view returns (RegistryProject memory) {
return projects[index];
}

/// @inheritdoc ISimpleProjectRegistry
function getProjects() external view returns (RegistryProject[] memory) {
return projects;
}

/// @inheritdoc ISimpleProjectRegistry
function getProjectsNumber() external view returns (uint256) {
return projects.length;
}
}
Dismissed Show dismissed Hide dismissed
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
pragma solidity ^0.8.20;

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

import { SignUpGatekeeper } from "./SignUpGatekeeper.sol";
import { SignUpToken } from "../SignUpToken.sol";

/// @title SignUpTokenGatekeeper
/// @notice This contract allows to gatekeep MACI signups
/// by requiring new voters to own a certain ERC721 token
contract SignUpTokenGatekeeper is SignUpGatekeeper, Ownable(msg.sender) {
/// @notice the reference to the SignUpToken contract
SignUpToken public token;
ERC721 public immutable token;

/// @notice the reference to the MACI contract
address public maci;

Expand All @@ -24,9 +25,9 @@ contract SignUpTokenGatekeeper is SignUpGatekeeper, Ownable(msg.sender) {
error OnlyMACI();

/// @notice creates a new SignUpTokenGatekeeper
/// @param _token the address of the SignUpToken contract
constructor(SignUpToken _token) payable {
token = _token;
/// @param _token the address of the ERC20 contract
constructor(address _token) payable {
token = ERC721(_token);
}

/// @notice Adds an uninitialised MACI instance to allow for token signups
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { InitialVoiceCreditProxy } from "./InitialVoiceCreditProxy.sol";
/// for MACI's voters.
contract ConstantInitialVoiceCreditProxy is InitialVoiceCreditProxy {
/// @notice the balance to be returned by getVoiceCredits
uint256 internal balance;
uint256 internal immutable balance;

/// @notice creates a new ConstantInitialVoiceCreditProxy
/// @param _balance the balance to be returned by getVoiceCredits
Expand Down
38 changes: 38 additions & 0 deletions packages/contracts/contracts/interfaces/ITally.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/// @title ITally
/// @notice Tally interface
interface ITally {
/// @notice Verify the number of spent voice credits per vote option from the tally.json
/// @param _voteOptionIndex the index of the vote option where credits were spent
/// @param _spent the spent voice credits for a given vote option index
/// @param _spentProof proof generated for the perVOSpentVoiceCredits
/// @param _spentSalt the corresponding salt given in the tally perVOSpentVoiceCredits object
/// @param _voteOptionTreeDepth depth of the vote option tree
/// @param _spentVoiceCreditsHash hashLeftRight(number of spent voice credits, spent salt)
/// @param _resultCommitment hashLeftRight(merkle root of the results.tally, results.salt)
// in the tally.json file
/// @return isValid Whether the provided proof is valid
function verifyPerVOSpentVoiceCredits(
uint256 _voteOptionIndex,
uint256 _spent,
uint256[][] calldata _spentProof,
uint256 _spentSalt,
uint8 _voteOptionTreeDepth,
uint256 _spentVoiceCreditsHash,
uint256 _resultCommitment
) external view returns (bool);

/// @notice Verify the number of spent voice credits from the tally.json
/// @param _totalSpent spent field retrieved in the totalSpentVoiceCredits object
/// @param _totalSpentSalt the corresponding salt in the totalSpentVoiceCredit object
/// @param _resultCommitment hashLeftRight(merkle root of the results.tally, results.salt) in tally.json file
/// @param _perVOSpentVoiceCreditsHash only for QV - hashLeftRight(merkle root of the no spent voice credits, salt)
function verifySpentVoiceCredits(
uint256 _totalSpent,
uint256 _totalSpentSalt,
uint256 _resultCommitment,
uint256 _perVOSpentVoiceCreditsHash
) external view returns (bool);
}
12 changes: 12 additions & 0 deletions packages/contracts/contracts/mocks/MockERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/// @title MockERC20
/// @notice A mock ERC20 contract that mints 100,000,000,000,000 tokens to the deployer
contract MockERC20 is ERC20 {
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {
_mint(msg.sender, 100e18);
}
}
37 changes: 37 additions & 0 deletions packages/contracts/contracts/mocks/MockTally.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MockTally is ITally {
bool public returnValue = true;

/// @notice Flip the return value
/// @dev used for mock testing
function flipReturnValue() external {
returnValue = !returnValue;
}

/// @inheritdoc ITally
function verifyPerVOSpentVoiceCredits(
uint256 _voteOptionIndex,
uint256 _spent,
uint256[][] calldata _spentProof,
uint256 _spentSalt,
uint8 _voteOptionTreeDepth,
uint256 _spentVoiceCreditsHash,
uint256 _resultCommitment
) external view returns (bool) {
return returnValue;
}

/// @inheritdoc ITally
function verifySpentVoiceCredits(
uint256 _totalSpent,
uint256 _totalSpentSalt,
uint256 _resultCommitment,
uint256 _perVOSpentVoiceCreditsHash
) external view returns (bool) {
return returnValue;
}
}
2 changes: 1 addition & 1 deletion packages/contracts/contracts/trees/AccQueue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ abstract contract AccQueue is Ownable(msg.sender), Hasher {
uint256 internal immutable subTreeCapacity;

// True hashLength == 2, false if hashLength == 5
bool internal isBinary;
bool internal immutable isBinary;

// The index of the current subtree. e.g. the first subtree has index 0, the
// second has 1, and so on
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"test:gitcoin_gatekeeper": "pnpm run test ./tests/GitcoinPassportGatekeeper.test.ts",
"test:zupass_gatekeeper": "pnpm run test ./tests/ZupassGatekeeper.test.ts",
"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",
"deploy": "hardhat deploy-full",
"deploy-poll": "hardhat deploy-poll",
"verify": "hardhat verify-full",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect } from "chai";
import { encodeBytes32String, Signer } from "ethers";

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

describe("SimpleProjecttRegistry", () => {
let projectRegistry: SimpleProjectRegistry;
let owner: Signer;
let user: Signer;

let ownerAddress: string;
let userAddress: string;

const metadataUrl = encodeBytes32String("url");

before(async () => {
[owner, user] = await getSigners();
[ownerAddress, userAddress] = await Promise.all([owner.getAddress(), user.getAddress()]);
projectRegistry = await deployContract("SimpleProjectRegistry", owner, true, metadataUrl);
});

it("should allow the owner to add a project", async () => {
await projectRegistry.addProject({ project: ownerAddress, metadataUrl });
});

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

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

it("should return registry metadata url properly", async () => {
const url = await projectRegistry.getRegistryMetadataUrl();

expect(url).to.equal(metadataUrl);
});

it("should return the correct address given an index", async () => {
const data = await projectRegistry.getProject(0n);

expect(data.project).to.equal(ownerAddress);
expect(data.metadataUrl).to.equal(metadataUrl);
});

it("should return the full list of addresses", async () => {
const projects = await projectRegistry.getProjects();
const data = projects.map((item) => ({ project: item.project, metadataUrl: item.metadataUrl }));

expect(data).to.deep.equal([
{ project: ownerAddress, metadataUrl },
{ project: ownerAddress, metadataUrl },
{ project: userAddress, metadataUrl },
{ project: ownerAddress, metadataUrl },
]);
});
});
Loading