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
ctrlc03 committed Jul 8, 2024
1 parent dad8c3c commit 3af663b
Show file tree
Hide file tree
Showing 13 changed files with 676 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);
}
190 changes: 190 additions & 0 deletions contracts/contracts/extensions/payout/SimplePayout.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// 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 { IWETH } from "../../interfaces/IWETH.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 WETH address
IWETH public immutable WETH;

Check warning on line 20 in contracts/contracts/extensions/payout/SimplePayout.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Immutable variables names are set to be in mixedCase

/// @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 NotEnoughEther();
error TransferFailed();

/// @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,

Check notice

Code scanning / Slither

Missing zero address validation Low

address _tally,
uint256 _totalAmount,
uint256 _totalSpent,
uint256 _totalSpentSalt,
uint256 _resultCommitment,
uint256 _perVOSpentVoiceCreditsHash,
address _weth
) payable {
projectRegistry = ISimpleProjectRegistry(_projectRegistry);
payoutToken = _token;
tally = ITally(_tally);
WETH = IWETH(_weth);

// 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 {
if (payoutToken != address(0)) {
// 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,
uint256[][] calldata _proof,
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,
uint8 _voteOptionTreeDepth
) 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
if (payoutToken == address(0)) {
// transfer the native token amount
_handleNativeTransfer(project, tokensToPay);
} else {
// transfer the ERC20 token amount
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;
if (token == address(0)) {
// transfer the native token amount
(bool success, ) = payable(msg.sender).call{ value: address(this).balance }("");
if (!success) revert TransferFailed();
} else {
// transfer the ERC20 token amount
IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
}

/// @notice Handle native transfers
/// @param _dest The destination address
/// @param _amount The amount to transfer
function _handleNativeTransfer(address _dest, uint256 _amount) internal {
// Handle Ether payment
if (address(this).balance < _amount) revert NotEnoughEther();
uint256 gas = gasleft();
(bool success, ) = _dest.call{ value: _amount, gas: gas }("");
// If the Ether transfer fails, wrap the Ether and try to send it as WETH.
if (!success) {
WETH.deposit{ value: _amount }();
IERC20(address(WETH)).safeTransfer(_dest, _amount);
}
}

Check failure

Code scanning / Slither

Functions that send Ether to arbitrary destinations High

Check warning

Code scanning / Slither

Low-level calls Warning

}
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 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 {
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;
}
}
11 changes: 6 additions & 5 deletions contracts/contracts/gatekeepers/SignUpTokenGatekeeper.sol
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 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);
}
9 changes: 9 additions & 0 deletions contracts/contracts/interfaces/IWETH.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @title IWETH
/// @notice Interface for the WETH9 contract
interface IWETH {
function deposit() external payable;
function withdraw(uint256 wad) external;
}
12 changes: 12 additions & 0 deletions 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);
}
}
Loading

0 comments on commit 3af663b

Please sign in to comment.