Skip to content

Commit

Permalink
feat(contracts): add first registry and payout extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
0xmad committed Aug 8, 2024
1 parent 98c2181 commit 53e53af
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title ISimpleProjectRegistry
/// @notice An interface for a simple project registry
interface ISimpleProjectRegistry {
/// @notice Get a project
/// @param index The index of the project
/// @return The address of the project
function getProject(uint256 index) external view returns (address);

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

/// @notice Get the number of projects
/// @return The number of projects
function getProjectsNumber() external view returns (uint256);
}
159 changes: 159 additions & 0 deletions packages/contracts/contracts/extensions/payout/SimplePayout.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

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

/// @title SimplePayout
/// @notice A simple payout contract that works with a SimpleProjectRegistry
/// and a MACI Tally contract
contract SimplePayout is Ownable(msg.sender) {
/// @notice use safe ERC20 functions
using SafeERC20 for IERC20;

/// @notice the project registry
ISimpleProjectRegistry public immutable projectRegistry;

/// @notice the payout token
address public immutable payoutToken;

/// @notice the tally contract
ITally public immutable tally;

/// @notice keep track of which users have already been paid
mapping(uint256 => bool) public paidProjects;

/// @notice the total amount of tokens available to be paid out
uint256 public immutable totalAmount;

/// @notice the total amount of voice credits spent
uint256 public immutable totalVoiceCreditsSpent;

/// @notice keep track of how many users have been paid
uint256 public paid;

/// @notice Custom errors
error InvalidSpentVoiceCredits();
error ProjectAlreadyPaid();
error InvalidProof();
error NotAllProjectsPaid();
error ZeroAddress();

/// @notice Create a new instance of the payout contract
/// @param _projectRegistry the address of the project registry
/// @param _token the address of the payout token
/// @param _tally the address of the tally contract
/// @param _totalAmount the total amount of tokens available to be paid out
/// @param _totalSpent the total amount of voice credits spent
/// @param _totalSpentSalt the salt of the spent amount
/// @param _resultCommitment the commitment of the results
/// @param _perVOSpentVoiceCreditsHash the hash of the spent voice credits
constructor(
address _projectRegistry,
address _token,
address _tally,
uint256 _totalAmount,
uint256 _totalSpent,
uint256 _totalSpentSalt,
uint256 _resultCommitment,
uint256 _perVOSpentVoiceCreditsHash
) payable {
if (_token == address(0)) {
revert ZeroAddress();
}

projectRegistry = ISimpleProjectRegistry(_projectRegistry);
payoutToken = _token;
tally = ITally(_tally);

// set the total amount of tokens available to be paid out
totalAmount = _totalAmount;

// ensure that the total amount of voice credit spent is correct
if (
!ITally(_tally).verifySpentVoiceCredits(
_totalSpent,
_totalSpentSalt,
_resultCommitment,
_perVOSpentVoiceCreditsHash
)
) revert InvalidSpentVoiceCredits();

// set the total amount of voice credits spent
totalVoiceCreditsSpent = _totalSpent;
}

/// @notice Deposit the amount of tokens to the contract
/// @dev This function is only callable by the owner
function deposit() external payable onlyOwner {
// transfer the ERC20 token amount
IERC20(payoutToken).safeTransferFrom(msg.sender, address(this), totalAmount);
}

/// @notice Payout the amount of tokens to the projects
/// @param _voteOptionIndex the index of the vote option
/// @param _spent the amount of voice credits spent
/// @param _proof the proof of the spent amount
/// @param _spentSalt the salt of the spent amount
/// @param _resultsCommitment the commitment of the results
/// @param _spentVoiceCreditsCommitment the commitment of the spent voice credits
/// @param _voteOptionTreeDepth the depth of the vote option tree
function payout(
uint256 _voteOptionIndex,

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint256 _spent,

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint256[][] calldata _proof,

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint256 _spentSalt,

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint256 _resultsCommitment,

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint256 _spentVoiceCreditsCommitment,

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint8 _voteOptionTreeDepth

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

) external {
// check if the user has been paid already
if (paidProjects[_voteOptionIndex]) revert ProjectAlreadyPaid();
// set the vote option index as paid
paidProjects[_voteOptionIndex] = true;

// increment the number of paid users
unchecked {
paid++;
}

// get the address of the project
address project = projectRegistry.getProject(_voteOptionIndex);

// we verify the proof
if (
!tally.verifyPerVOSpentVoiceCredits(
_voteOptionIndex,
_spent,
_proof,
_spentSalt,
_voteOptionTreeDepth,
_spentVoiceCreditsCommitment,
_resultsCommitment
)
) revert InvalidProof();

// we need to calculate the amount that is to be given to the project (round down)
uint256 tokensToPay = (_spent * totalAmount) / totalVoiceCreditsSpent;

// transfer the token amount
// check whether is native token
IERC20(payoutToken).safeTransfer(project, tokensToPay);
}

/// @notice A function to collect the dust left from round downs
/// can only be called once all projects have been paid
function collectDust() external onlyOwner {
// check if all projects have been paid
if (projectRegistry.getProjectsNumber() != paid) revert NotAllProjectsPaid();

address token = payoutToken;

// transfer the ERC20 token amount
IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 {
// simple storage of projects is an array of addresses
// with the the index being the position in the array
address[] internal projects;

/// @notice Create a new instance of the registry contract
constructor() payable {}

Check warning on line 21 in packages/contracts/contracts/extensions/registry/SimpleRecipientRegistry.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Code contains empty blocks

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

/// @notice Add multiple projects to the registry
/// @param _projects The addresses of the projects to add
function addProjects(address[] calldata _projects) external onlyOwner {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

uint256 len = _projects.length;
for (uint256 i = 0; i < len; ) {
projects.push(_projects[i]);

unchecked {
i++;
}
}
}

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

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

/// @inheritdoc ISimpleProjectRegistry
function getProjectsNumber() external view returns (uint256) {
return projects.length;
}
}

Check warning

Code scanning / Slither

Contracts that lock Ether Medium

Contract locking ether found:
Contract SimpleProjectRegistry has payable functions:
- SimpleProjectRegistry.constructor()
But does not have a function to withdraw the ether
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
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: 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
Loading

0 comments on commit 53e53af

Please sign in to comment.