-
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 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); | ||
} |
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 GitHub Actions / check (lint:sol)
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
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._voteOptionIndex is not in mixedCase
|
||
uint256 _spent, | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._spent is not in mixedCase
|
||
uint256[][] calldata _proof, | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._proof is not in mixedCase
|
||
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, | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._spentVoiceCreditsCommitment is not in mixedCase
|
||
uint8 _voteOptionTreeDepth | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SimplePayout.payout(uint256,uint256,uint256[][],uint256,uint256,uint256,uint8)._voteOptionTreeDepth is not in mixedCase
|
||
) 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
SimplePayout.collectDust() ignores return value by address(msg.sender).call{value: address(this).balance}()
Check warning Code scanning / Slither Low-level calls Warning
Low level call in SimplePayout.collectDust():
- address(msg.sender).call{value: address(this).balance}() |
||
|
||
/// @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 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 {} | ||
|
||
/// @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
Parameter SimpleRecipientRegistry.addRecipients(address[])._recipients is not in mixedCase
|
||
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
SimpleRecipientRegistry should inherit from ISimpleRecipientRegistry
|
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,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; | ||
} |
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); | ||
} | ||
} |