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 5, 2024
1 parent e714d5a commit f8357bc
Show file tree
Hide file tree
Showing 12 changed files with 599 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 ISimpleRecipientRegistry
/// @notice An interface for a simple recipient registry
interface ISimpleRecipientRegistry {
/// @notice Get a recipient
/// @param index The index of the recipient
/// @return The address of the recipient
function getRecipient(uint256 index) external view returns (address);

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

/// @notice Get the number of recipients
/// @return The number of recipients
function getRecipientsNumber() external view returns (uint256);
}
188 changes: 188 additions & 0 deletions contracts/contracts/extensions/payout/SimplePayout.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// 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 { ISimpleRecipientRegistry } from "../interfaces/ISimpleRecipientRegistry.sol";
import { IWETH } from "../../interfaces/IWETH.sol";
import { ITally } from "../../interfaces/ITally.sol";

/// @title SimplePayout
/// @notice A simple payout contract that works with a SimpleRecipientRegistry
/// 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

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Variable SimplePayout.WETH is not in mixedCase

/// @notice the recipient registry
ISimpleRecipientRegistry public immutable recipientRegistry;

/// @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 paidUsers;

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

/// @notice Create a new instance of the payout contract
/// @param _recipientRegistry the address of the recipient 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 _recipientRegistry,
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 {
recipientRegistry = ISimpleRecipientRegistry(_recipientRegistry);
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 recipient
/// @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 (paidUsers[_voteOptionIndex]) revert ProjectAlreadyPaid();
// set the vote option index as paid
paidUsers[_voteOptionIndex] = true;

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

// get the address of the recipient
address recipient = recipientRegistry.getRecipient(_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(recipient, tokensToPay);
} else {
// transfer the ERC20 token amount
IERC20(payoutToken).safeTransfer(recipient, 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 (recipientRegistry.getRecipientsNumber() != paid) revert NotAllProjectsPaid();

address token = payoutToken;
if (token == address(0)) {
// transfer the native token amount
payable(msg.sender).call{ value: address(this).balance }("");
} else {
// transfer the ERC20 token amount
IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this)));
}
}

Check warning

Code scanning / Slither

Unchecked low-level calls Medium

Check warning

Code scanning / Slither

Low-level calls Warning


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

Code scanning / Slither

Return Bomb Low

SimplePayout._handleNativeTransfer(address,uint256) tries to limit the gas of an external call that controls implicit decoding
(success,None) = _dest.call{gas: gas,value: _amount}()

Check warning

Code scanning / Slither

Low-level calls Warning

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

/// @title SimpleRecipientRegistry
/// @notice This contract is a simple registry of recipients
/// @dev This does not constrain the number of recipients
/// which might be > vote options
/// @dev it does not prevent duplicate addresses from being
/// added as recipients
/// @notice it does not allow to remove recipients either
contract SimpleRecipientRegistry is Ownable(msg.sender) {
// simple storage of recipients is an array of addresses
// with the the index being the position in the array
address[] internal recipients;

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

Check warning on line 19 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 recipient to the registry
/// @param recipient The address of the recipient to add
function addRecipient(address recipient) external onlyOwner {
recipients.push(recipient);
}

/// @notice Add multiple recipients to the registry
/// @param _recipients The addresses of the recipients to add
function addRecipients(address[] calldata _recipients) external onlyOwner {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

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

unchecked {
i++;
}
}
}

/// @notice Get a recipient from the registry
/// @param index The index of the recipient
/// @return The address of the recipient
function getRecipient(uint256 index) external view returns (address) {
return recipients[index];
}

/// @notice Get all recipients
/// @return The addresses of the recipients
function getRecipients() external view returns (address[] memory) {
return recipients;
}

/// @notice Get the number of recipients
/// @return The number of recipients
function getRecipientsNumber() external view returns (uint256) {
return recipients.length;
}
}

Check warning

Code scanning / Slither

Missing inheritance Warning

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

Check warning

Code scanning / Slither

Incorrect versions of Solidity Warning

Version constraint ^0.8.0 contains known severe issues (https://solidity.readthedocs.io/en/latest/bugs.html)
- FullInlinerNonExpressionSplitArgumentEvaluationOrder
- MissingSideEffectsOnSelectorAccess
- AbiReencodingHeadOverflowWithStaticArrayCleanup
- DirtyBytesArrayToStorage
- DataLocationChangeInInternalOverride
- NestedCalldataArrayAbiReencodingSizeValidation
- SignedImmutables
- ABIDecodeTwoDimensionalArrayMemory
- KeccakCaching.
It is used by:
- ^0.8.0

// import { IERC20 } from "./IERC20.sol";

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

Please sign in to comment.