-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
- [x] Use tally as payout strategy - [x] Add deposit/claim/withdraw functions - [x] Add tests
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
import { IPoll } from "./IPoll.sol"; | ||
|
||
/// @title IPayoutStrategy | ||
/// @notice Interface responsible for payout strategy | ||
interface IPayoutStrategy { | ||
/// @notice Strategy initialization params | ||
struct StrategyInit { | ||
/// @notice The cooldown duration for withdrawal extra funds | ||
uint256 cooldownTime; | ||
/// @notice The max contribution amount | ||
uint256 maxContribution; | ||
/// @notice The payout token | ||
address payoutToken; | ||
/// @notice The poll address | ||
address poll; | ||
} | ||
|
||
/// @notice Claim params | ||
struct Claim { | ||
/// @notice The index of the vote option to verify the correctness of the tally | ||
uint256 index; | ||
/// @notice The voice credit options received for recipient | ||
uint256 voiceCreditsPerOption; | ||
/// @notice Flattened array of the tally | ||
uint256 tallyResult; | ||
/// @notice The sum of tally results squares | ||
uint256 totalVotesSquares; | ||
/// @notice The total amount of voice credits spent | ||
uint256 totalSpent; | ||
/// @notice Corresponding proof of the tally result | ||
uint256[][] tallyResultProof; | ||
/// @notice The respective salt in the results object in the tally.json | ||
uint256 tallyResultSalt; | ||
/// @notice Depth of the vote option tree | ||
uint8 voteOptionTreeDepth; | ||
/// @notice hashLeftRight(number of spent voice credits, spent salt) | ||
uint256 spentVoiceCreditsHash; | ||
/// @notice hashLeftRight(merkle root of the no spent voice | ||
uint256 perVOSpentVoiceCreditsHash; | ||
} | ||
|
||
/// @notice Total deposited amount | ||
function totalAmount() external view returns (uint256); | ||
|
||
/// @notice The cooldown timeout | ||
function cooldown() external view returns (uint256); | ||
|
||
/// @notice Deposit amount | ||
/// @param amount The amount | ||
function deposit(uint256 amount) external; | ||
|
||
/// @notice Withdraw extra amount | ||
/// @param receivers The receivers addresses | ||
/// @param amounts The amounts | ||
function withdrawExtra(address[] calldata receivers, uint256[] calldata amounts) external; | ||
|
||
/// @notice Claim funds for recipient | ||
/// @param params The claim params | ||
function claim(Claim calldata params) external; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; | ||
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; | ||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { Tally as TallyBase } from "maci-contracts/contracts/Tally.sol"; | ||
|
||
import { IPoll } from "../interfaces/IPoll.sol"; | ||
import { IPayoutStrategy } from "../interfaces/IPayoutStrategy.sol"; | ||
import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; | ||
|
||
/// @title Tally - poll tally and payout strategy | ||
/// @notice The Tally contract is used during votes tallying and by users to verify the tally results. | ||
/// @notice Allows users to deposit and claim rewards for recipients | ||
contract Tally is TallyBase, IPayoutStrategy, Pausable { | ||
using SafeERC20 for IERC20; | ||
|
||
/// @notice The max voice credits (MACI allows 2 ** 32 voice credits max) | ||
uint256 private constant MAX_VOICE_CREDITS = 10 ** 9; | ||
Check warning Code scanning / Slither Unused state variable Warning
Tally.MAX_VOICE_CREDITS is never used in Tally
|
||
|
||
/// @notice The alpha precision (needed for allocated amount calculation) | ||
uint256 private constant ALPHA_PRECISION = 10 ** 18; | ||
|
||
/// @notice The payout token | ||
IERC20 public token; | ||
Check failure Code scanning / Slither Uninitialized state variables High
Tally.token is never initialized. It is used in:
- Tally.deposit(uint256) - Tally.withdrawExtra(address[],uint256[]) - Tally.claim(IPayoutStrategy.Claim) - Tally.calculateAlpha(uint256) Check warning Code scanning / Slither State variables that could be declared constant Warning
Tally.token should be constant
|
||
|
||
/// @notice The poll registry | ||
IRecipientRegistry public registry; | ||
Check failure Code scanning / Slither Uninitialized state variables High
Tally.registry is never initialized. It is used in:
- Tally.addTallyResults(uint256[],uint256[],uint256[][][],uint256,uint256,uint256) - Tally.claim(IPayoutStrategy.Claim) Check warning Code scanning / Slither State variables that could be declared constant Warning
Tally.registry should be constant
|
||
|
||
/// @notice The total amount of funds deposited | ||
uint256 public totalAmount; | ||
|
||
/// @notice The max contribution amount | ||
uint256 public maxContributionAmount; | ||
Check warning Code scanning / Slither State variables that could be declared constant Warning
Tally.maxContributionAmount should be constant
|
||
|
||
/// @notice The voice credit factor (needed for allocated amount calculation) | ||
uint256 public voiceCreditFactor; | ||
Check failure Code scanning / Slither Uninitialized state variables High
Tally.voiceCreditFactor is never initialized. It is used in:
- Tally.getAllocatedAmount(uint256,uint256) - Tally.calculateAlpha(uint256) Check warning Code scanning / Slither State variables that could be declared constant Warning
Tally.voiceCreditFactor should be constant
|
||
|
||
/// @notice The cooldown duration for withdrawal extra funds | ||
uint256 public cooldown; | ||
|
||
/// @notice Initialized or not | ||
bool internal initialized; | ||
|
||
/// @notice custom errors | ||
error CooldownPeriodNotOver(); | ||
error VotingPeriodNotOver(); | ||
error VotingPeriodOver(); | ||
error InvalidBudget(); | ||
error NoProjectHasMoreThanOneVote(); | ||
error InvalidWithdrawal(); | ||
error AlreadyInitialized(); | ||
error NotInitialized(); | ||
error InvalidPoll(); | ||
|
||
/// @notice Create a new Tally contract | ||
/// @param verifierContract The Verifier contract | ||
/// @param vkRegistryContract The VkRegistry contract | ||
/// @param pollContract The Poll contract | ||
/// @param mpContract The MessageProcessor contract | ||
/// @param tallyOwner The owner of the Tally contract | ||
/// @param pollMode The mode of the poll | ||
constructor( | ||
address verifierContract, | ||
address vkRegistryContract, | ||
address pollContract, | ||
address mpContract, | ||
address tallyOwner, | ||
Mode pollMode | ||
) payable TallyBase(verifierContract, vkRegistryContract, pollContract, mpContract, tallyOwner, pollMode) { | ||
} | ||
|
||
/// @notice A modifier that causes the function to revert if the cooldown period is not over | ||
modifier afterCooldown() { | ||
(uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); | ||
uint256 secondsPassed = block.timestamp - deployTime; | ||
|
||
if (secondsPassed <= duration + cooldown) { | ||
revert CooldownPeriodNotOver(); | ||
} | ||
|
||
_; | ||
} | ||
|
||
/// @notice A modifier that causes the function to revert if the voting period is over | ||
modifier beforeVotingDeadline() { | ||
(uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); | ||
uint256 secondsPassed = block.timestamp - deployTime; | ||
|
||
if (secondsPassed > duration) { | ||
revert VotingPeriodOver(); | ||
} | ||
|
||
_; | ||
} | ||
|
||
/// @notice A modifier that causes the function to revert if the voting period is not over | ||
modifier afterVotingDeadline() { | ||
(uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); | ||
uint256 secondsPassed = block.timestamp - deployTime; | ||
|
||
if (secondsPassed <= duration) { | ||
revert VotingPeriodNotOver(); | ||
} | ||
|
||
_; | ||
} | ||
|
||
/// @notice A modifier that causes the function to revert if the strategy is not initialized | ||
modifier isInitialized() { | ||
if (!initialized) { | ||
revert NotInitialized(); | ||
} | ||
|
||
_; | ||
} | ||
|
||
/// @notice Initialize tally with strategy params | ||
/// @param params The strategy initialization params | ||
function init(IPayoutStrategy.StrategyInit calldata params) public onlyOwner { | ||
if (initialized) { | ||
revert AlreadyInitialized(); | ||
} | ||
|
||
if (params.poll != address(poll)) { | ||
revert InvalidPoll(); | ||
} | ||
|
||
initialized = true; | ||
cooldown = params.cooldownTime; | ||
registry = IPoll(params.poll).getRegistry(); | ||
token = IERC20(params.payoutToken); | ||
maxContributionAmount = params.maxContribution; | ||
voiceCreditFactor = params.maxContribution / MAX_VOICE_CREDITS; | ||
} | ||
|
||
/// @notice Pause contract calls (deposit, claim, withdraw) | ||
function pause() public onlyOwner { | ||
_pause(); | ||
} | ||
|
||
/// @notice Unpause contract calls (deposit, claim, withdraw) | ||
function unpause() public onlyOwner { | ||
_unpause(); | ||
} | ||
|
||
/// @inheritdoc IPayoutStrategy | ||
function deposit(uint256 amount) public isInitialized whenNotPaused beforeVotingDeadline { | ||
totalAmount += amount; | ||
|
||
token.safeTransferFrom(msg.sender, address(this), amount); | ||
} | ||
|
||
/// @inheritdoc IPayoutStrategy | ||
function withdrawExtra( | ||
address[] calldata receivers, | ||
uint256[] calldata amounts | ||
) public override isInitialized onlyOwner whenNotPaused afterCooldown { | ||
uint256 amountLength = amounts.length; | ||
uint256 totalFunds = totalAmount; | ||
uint256 sum = 0; | ||
|
||
for (uint256 index = 0; index < amountLength; ) { | ||
uint256 amount = amounts[index]; | ||
sum += amount; | ||
|
||
if (sum > totalFunds) { | ||
revert InvalidWithdrawal(); | ||
} | ||
|
||
totalAmount -= amount; | ||
|
||
unchecked { | ||
index++; | ||
} | ||
} | ||
|
||
for (uint256 index = 0; index < amountLength; ) { | ||
uint256 amount = amounts[index]; | ||
address receiver = receivers[index]; | ||
|
||
if (amount > 0) { | ||
token.safeTransfer(receiver, amount); | ||
} | ||
|
||
unchecked { | ||
index++; | ||
} | ||
} | ||
} | ||
Check warning Code scanning / Slither Costly operations inside a loop Warning
Tally.withdrawExtra(address[],uint256[]) has costly operations inside a loop:
- totalAmount -= amount |
||
|
||
/// @inheritdoc IPayoutStrategy | ||
function claim( | ||
IPayoutStrategy.Claim calldata params | ||
) public override isInitialized whenNotPaused afterVotingDeadline { | ||
uint256 amount = getAllocatedAmount( | ||
params.voiceCreditsPerOption, | ||
params.totalVotesSquares, | ||
params.totalSpent, | ||
params.tallyResult | ||
); | ||
totalAmount -= amount; | ||
|
||
bool isValid = super.verifyTallyResult( | ||
params.index, | ||
params.tallyResult, | ||
params.tallyResultProof, | ||
params.tallyResultSalt, | ||
params.voteOptionTreeDepth, | ||
params.spentVoiceCreditsHash, | ||
params.perVOSpentVoiceCreditsHash | ||
); | ||
|
||
if (!isValid) { | ||
revert InvalidTallyVotesProof(); | ||
} | ||
|
||
IRecipientRegistry.Recipient memory recipient = registry.getRecipient(params.index); | ||
|
||
token.safeTransfer(recipient.recipient, amount); | ||
} | ||
|
||
/// @notice Get allocated token amount (without verification). | ||
/// @param voiceCreditsPerOption The voice credit options received for recipient | ||
/// @param totalVotesSquares The sum of tally results squares | ||
/// @param totalSpent The total amount of voice credits spent | ||
/// @param tallyResult The tally result for recipient | ||
function getAllocatedAmount( | ||
uint256 voiceCreditsPerOption, | ||
uint256 totalVotesSquares, | ||
uint256 totalSpent, | ||
uint256 tallyResult | ||
) internal view returns (uint256) { | ||
uint256 alpha = calculateAlpha(totalVotesSquares, totalSpent); | ||
uint256 quadratic = alpha * voiceCreditFactor * tallyResult * tallyResult; | ||
uint256 totalSpentCredits = voiceCreditFactor * voiceCreditsPerOption; | ||
uint256 linearPrecision = ALPHA_PRECISION * totalSpentCredits; | ||
uint256 linearAlpha = alpha * totalSpentCredits; | ||
|
||
return ((quadratic + linearPrecision) - linearAlpha) / ALPHA_PRECISION; | ||
} | ||
|
||
/// @notice Calculate the alpha for the capital constrained quadratic formula | ||
/// @dev page 17 of https://arxiv.org/pdf/1809.06421.pdf | ||
/// @param totalVotesSquares The sum of tally results squares | ||
/// @param totalSpent The total amount of voice credits spent | ||
function calculateAlpha(uint256 totalVotesSquares, uint256 totalSpent) internal view returns (uint256) { | ||
uint256 budget = token.balanceOf(address(this)); | ||
uint256 contributions = totalSpent * voiceCreditFactor; | ||
|
||
if (budget < contributions) { | ||
revert InvalidBudget(); | ||
} | ||
|
||
if (totalVotesSquares <= totalSpent) { | ||
revert NoProjectHasMoreThanOneVote(); | ||
} | ||
|
||
return ((budget - contributions) * ALPHA_PRECISION) / (voiceCreditFactor * (totalVotesSquares - totalSpent)); | ||
} | ||
} |