-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- [x] Use tally as payout strategy - [x] Add deposit/claim/withdraw functions - [x] Add tests
- Loading branch information
Showing
15 changed files
with
951 additions
and
73 deletions.
There are no files selected for viewing
63 changes: 63 additions & 0 deletions
63
packages/contracts/contracts/interfaces/IPayoutStrategy.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
/// @notice The alpha precision (needed for allocated amount calculation) | ||
uint256 private constant ALPHA_PRECISION = 10 ** 18; | ||
|
||
/// @notice The payout token | ||
IERC20 public token; | ||
|
||
/// @notice The poll registry | ||
IRecipientRegistry public registry; | ||
|
||
/// @notice The total amount of funds deposited | ||
uint256 public totalAmount; | ||
|
||
/// @notice The max contribution amount | ||
uint256 public maxContributionAmount; | ||
|
||
/// @notice The voice credit factor (needed for allocated amount calculation) | ||
uint256 public voiceCreditFactor; | ||
|
||
/// @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++; | ||
} | ||
} | ||
} | ||
|
||
/// @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)); | ||
} | ||
} |
Oops, something went wrong.