-
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
608 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
197 changes: 197 additions & 0 deletions
197
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,197 @@ | ||
// 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++; | ||
} | ||
} | ||
} | ||
|
||
/// @inheritdoc IPayoutStrategy | ||
function claim( | ||
uint256 index, | ||
uint256 voiceCreditsPerOption, | ||
uint256 tallyResult, | ||
uint256 totalVotesSquares, | ||
uint256 totalSpent, | ||
IPoll poll | ||
) public override whenNotPaused afterVotingDeadline(poll) { | ||
uint256 budget = token.balanceOf(address(this)); | ||
uint256 contributions = totalSpent * voiceCreditFactor; | ||
|
||
if (budget < contributions) { | ||
revert InvalidBudget(); | ||
} | ||
|
||
if (totalVotesSquares <= totalSpent) { | ||
revert NoProjectHasMoreThanOneVote(); | ||
} | ||
|
||
/// Calculate the alpha for the capital constrained quadratic formula | ||
/// page 17 of https://arxiv.org/pdf/1809.06421.pdf | ||
uint256 alpha = ((budget - contributions) * ALPHA_PRECISION) / | ||
(voiceCreditFactor * (totalVotesSquares - totalSpent)); | ||
uint256 quadratic = alpha * voiceCreditFactor * tallyResult * tallyResult; | ||
uint256 totalSpentCredits = voiceCreditFactor * voiceCreditsPerOption; | ||
uint256 linearPrecision = ALPHA_PRECISION * totalSpentCredits; | ||
uint256 linearAlpha = alpha * totalSpentCredits; | ||
uint256 amount = ((quadratic + linearPrecision) - linearAlpha) / ALPHA_PRECISION; | ||
|
||
IRecipientRegistry registry = poll.getRegistry(); | ||
IRecipientRegistry.Recipient memory recipient = registry.getRecipient(index); | ||
totalAmount -= amount; | ||
|
||
token.safeTransfer(recipient.recipient, amount); | ||
} | ||
} |
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.