-
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 voice credit proxy as payout strategy - [x] Add deposit/claim/withdraw functions - [x] Add tests
- Loading branch information
Showing
10 changed files
with
627 additions
and
48 deletions.
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
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,43 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.20; | ||
|
||
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
|
||
import { IPoll } from "./IPoll.sol"; | ||
|
||
/// @title IPayoutStrategy | ||
/// @notice Interface responsible for payout strategy | ||
interface IPayoutStrategy { | ||
/// @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 | ||
/// @param poll The poll | ||
function deposit(uint256 amount, IPoll poll) external; | ||
|
||
/// @notice Withdraw extra amount | ||
/// @param receivers The receivers addresses | ||
/// @param amounts The amounts | ||
/// @param poll The poll | ||
function withdrawExtra(address[] calldata receivers, uint256[] calldata amounts, IPoll poll) external; | ||
|
||
/// @notice Claim funds for recipient | ||
/// @param index The recipient index in registry | ||
/// @param voiceCreditsPerOption The voice credit options received for recipient | ||
/// @param tallyResult The tally result for recipient | ||
/// @param totalVotesSquares The sum of tally results squares | ||
/// @param totalSpent The total amount of voice credits spent | ||
/// @param poll The poll | ||
function claim( | ||
uint256 index, | ||
uint256 voiceCreditsPerOption, | ||
uint256 tallyResult, | ||
uint256 totalVotesSquares, | ||
uint256 totalSpent, | ||
IPoll poll | ||
) 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
216 changes: 216 additions & 0 deletions
216
packages/contracts/contracts/maci/MaxCapVoiceCreditProxy.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,216 @@ | ||
// 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 { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import { InitialVoiceCreditProxy } from "maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; | ||
|
||
import { IPoll } from "../interfaces/IPoll.sol"; | ||
import { IPayoutStrategy } from "../interfaces/IPayoutStrategy.sol"; | ||
import { IRecipientRegistry } from "../interfaces/IRecipientRegistry.sol"; | ||
|
||
/// @title MaxCapVoiceCreditProxy - voice credit proxy and payout strategy | ||
/// @notice A contract which allows users to deposit and claim rewards for recipients | ||
contract MaxCapVoiceCreditProxy is InitialVoiceCreditProxy, IPayoutStrategy, Ownable, Pausable { | ||
using SafeERC20 for ERC20; | ||
|
||
/// @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 | ||
ERC20 public immutable token; | ||
|
||
/// @notice The total amount of funds deposited | ||
uint256 public totalAmount; | ||
|
||
/// @notice The max contribution amount | ||
uint256 public immutable maxContributionAmount; | ||
|
||
/// @notice The voice credit factor (needed for allocated amount calculation) | ||
uint256 public immutable voiceCreditFactor; | ||
|
||
/// @notice The cooldown duration for withdrawal extra funds | ||
uint256 public immutable cooldown; | ||
|
||
/// @notice custom errors | ||
error CooldownPeriodNotOver(); | ||
error VotingPeriodNotOver(); | ||
error VotingPeriodOver(); | ||
error InvalidBudget(); | ||
error NoProjectHasMoreThanOneVote(); | ||
error InvalidWithdrawal(); | ||
|
||
/// @notice Contract initialization | ||
/// @param cooldownTime The cooldown duration for withdrawal extra funds | ||
/// @param maxContribution The max contribution amount | ||
/// @param payoutToken The payout token | ||
/// @param ownerAddress The contract owner | ||
constructor( | ||
uint256 cooldownTime, | ||
uint256 maxContribution, | ||
ERC20 payoutToken, | ||
address ownerAddress | ||
) Ownable(ownerAddress) { | ||
cooldown = cooldownTime; | ||
token = payoutToken; | ||
maxContributionAmount = maxContribution; | ||
voiceCreditFactor = maxContributionAmount / MAX_VOICE_CREDITS; | ||
} | ||
|
||
/// @notice A modifier that causes the function to revert if the cooldown period is not over | ||
modifier afterCooldown(IPoll poll) { | ||
(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(IPoll poll) { | ||
(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(IPoll poll) { | ||
(uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); | ||
uint256 secondsPassed = block.timestamp - deployTime; | ||
|
||
if (secondsPassed <= duration) { | ||
revert VotingPeriodNotOver(); | ||
} | ||
|
||
_; | ||
} | ||
|
||
/// @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 InitialVoiceCreditProxy | ||
function getVoiceCredits(address user, bytes memory /** data */) public view override returns (uint256) { | ||
return token.balanceOf(user); | ||
} | ||
|
||
/// @inheritdoc IPayoutStrategy | ||
function deposit(uint256 amount, IPoll poll) public whenNotPaused beforeVotingDeadline(poll) { | ||
totalAmount += amount; | ||
|
||
token.safeTransferFrom(msg.sender, address(this), amount); | ||
} | ||
|
||
/// @inheritdoc IPayoutStrategy | ||
function withdrawExtra( | ||
address[] calldata receivers, | ||
uint256[] calldata amounts, | ||
IPoll poll | ||
) public override onlyOwner whenNotPaused afterCooldown(poll) { | ||
uint256 amountLength = amounts.length; | ||
uint256 totalFunds = totalAmount; | ||
uint256 sum = 0; | ||
|
||
for (uint256 index = 0; index < amountLength; ) { | ||
uint256 amount = amounts[index]; | ||
sum += amount; | ||
totalAmount -= amount; | ||
|
||
unchecked { | ||
index++; | ||
} | ||
} | ||
|
||
if (sum > totalFunds) { | ||
revert InvalidWithdrawal(); | ||
} | ||
|
||
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
MaxCapVoiceCreditProxy.withdrawExtra(address[],uint256[],IPoll) has costly operations inside a loop:
- totalAmount -= amount Check notice Code scanning / Slither Missing events arithmetic Low
MaxCapVoiceCreditProxy.withdrawExtra(address[],uint256[],IPoll) should emit an event for:
- totalAmount -= amount |
||
|
||
/// @inheritdoc IPayoutStrategy | ||
function claim( | ||
uint256 index, | ||
uint256 voiceCreditsPerOption, | ||
uint256 tallyResult, | ||
uint256 totalVotesSquares, | ||
uint256 totalSpent, | ||
IPoll poll | ||
) public override whenNotPaused afterVotingDeadline(poll) { | ||
uint256 amount = getAllocatedAmount(voiceCreditsPerOption, totalVotesSquares, totalSpent, tallyResult); | ||
IRecipientRegistry registry = poll.getRegistry(); | ||
IRecipientRegistry.Recipient memory recipient = registry.getRecipient(index); | ||
totalAmount -= amount; | ||
|
||
token.safeTransfer(recipient.recipient, amount); | ||
} | ||
Check notice Code scanning / Slither Reentrancy vulnerabilities Low
Reentrancy in MaxCapVoiceCreditProxy.claim(uint256,uint256,uint256,uint256,uint256,IPoll):
External calls: - registry = poll.getRegistry() State variables written after the call(s): - totalAmount -= 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)); | ||
} | ||
} |
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,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,000 tokens to the deployer | ||
contract MockERC20 is ERC20 { | ||
constructor(string memory name, string memory symbol) ERC20(name, symbol) { | ||
_mint(msg.sender, 100e21); | ||
} | ||
} |
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
Oops, something went wrong.