Skip to content

Commit

Permalink
Merge pull request #1631 from privacy-scaling-explorations/feat/regis…
Browse files Browse the repository at this point in the history
…try-payout-extensions

feat(contracts): add first registry and payout extensions
  • Loading branch information
0xmad authored Aug 9, 2024
2 parents 98c2181 + 049bab7 commit ba62b2e
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 7 deletions.
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 {
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;
}
}
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
65 changes: 65 additions & 0 deletions packages/contracts/tests/extensions/SimpleProjectRegistry.test.ts
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 },
]);
});
});

0 comments on commit ba62b2e

Please sign in to comment.