-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
- Loading branch information
There are no files selected for viewing
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); | ||
} |
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; | ||
|
||
/// @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
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._voteOptionIndex is not in mixedCase
|
||
uint256 _spent, | ||
uint256[][] calldata _proof, | ||
uint256 _spentSalt, | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._spentSalt is not in mixedCase
|
||
uint256 _resultsCommitment, | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._resultsCommitment is not in mixedCase
|
||
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
SimplePayout._handleNativeTransfer(address,uint256) sends eth to arbitrary user
Dangerous calls: - (success,None) = _dest.call{gas: gas,value: _amount}() - WETH.deposit{value: _amount}() 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 {} | ||
|
||
/// @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; | ||
} | ||
} |
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); | ||
} |
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; | ||
} |
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); | ||
} | ||
} |