-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
- [x] Use voice credit proxy 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,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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
// 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 owner The contract owner | ||
constructor(uint256 cooldownTime, uint256 maxContribution, ERC20 payoutToken, address owner) Ownable(owner) { | ||
Check notice Code scanning / Slither Local variable shadowing Low
MaxCapVoiceCreditProxy.constructor(uint256,uint256,ERC20,address).owner shadows:
- Ownable.owner() (function) |
||
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 sum = 0; | ||
|
||
for (uint256 index = 0; index < amountLength; ) { | ||
uint256 amount = amounts[index]; | ||
sum += amount; | ||
|
||
unchecked { | ||
index++; | ||
} | ||
} | ||
|
||
if (sum > totalAmount) { | ||
revert InvalidWithdrawal(); | ||
} | ||
|
||
for (uint256 index = 0; index < amountLength; ) { | ||
uint256 amount = amounts[index]; | ||
address receiver = receivers[index]; | ||
totalAmount -= amount; | ||
|
||
if (amount > 0) { | ||
token.safeTransfer(receiver, amount); | ||
} | ||
|
||
unchecked { | ||
index++; | ||
} | ||
} | ||
} | ||
Check warning Code scanning / Slither Reentrancy vulnerabilities Medium
Reentrancy in MaxCapVoiceCreditProxy.withdrawExtra(address[],uint256[],IPoll):
External calls: - token.safeTransfer(receiver,amount_scope_1) State variables written after the call(s): - totalAmount -= amount_scope_1 MaxCapVoiceCreditProxy.totalAmount can be used in cross function reentrancies: - MaxCapVoiceCreditProxy.deposit(uint256,IPoll) - MaxCapVoiceCreditProxy.totalAmount - MaxCapVoiceCreditProxy.withdrawExtra(address[],uint256[],IPoll) Check warning Code scanning / Slither Costly operations inside a loop Warning
MaxCapVoiceCreditProxy.withdrawExtra(address[],uint256[],IPoll) has costly operations inside a loop:
- 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); | ||
} | ||
|
||
/// @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; | ||
} | ||
Check warning Code scanning / Slither Dead-code Warning
MaxCapVoiceCreditProxy.getAllocatedAmount(uint256,uint256,uint256,uint256) is never used and should be removed
|
||
|
||
/// @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)); | ||
} | ||
Check warning Code scanning / Slither Dead-code Warning
MaxCapVoiceCreditProxy.calculateAlpha(uint256,uint256) is never used and should be removed
|
||
} |
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) { | ||
Check notice Code scanning / Slither Local variable shadowing Low
MockERC20.constructor(string,string).name shadows:
- ERC20.name() (function) - IERC20Metadata.name() (function) Check notice Code scanning / Slither Local variable shadowing Low
MockERC20.constructor(string,string).symbol shadows:
- ERC20.symbol() (function) - IERC20Metadata.symbol() (function) |
||
_mint(msg.sender, 100e21); | ||
} | ||
} |