Skip to content

Commit

Permalink
Repo cleanups (#34)
Browse files Browse the repository at this point in the history
* add code coverage script

* alphebatize GrantFundTestHelper; move into test/utils directory; expand test coverage

* add test for maths lib

* additional cleanups

* expand natspec comments

* fix tests; further expand natspec

* begin migrating natspec documentation to interfaces

* additional natspec migration to interface; rearrange functions

* Remove unneeded function

* remove todos

* further expand natspec

* expand testFuzzExtraordinaryFunding checks

* remove todos

* comment pr feeback

Co-authored-by: Mike <[email protected]>
  • Loading branch information
MikeHathaway and Mike authored Jan 8, 2023
1 parent 283831c commit 973b855
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 213 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

# Report code coverage to discord
- name: Generate coverage
run: forge coverage --report lcov --no-match-test testLoad
run: forge coverage --report lcov
- name: Setup LCOV
uses: hrishikesh-kadam/setup-lcov@v1
- name: Filter lcov
Expand Down
20 changes: 17 additions & 3 deletions src/grants/GrantFund.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { Maths } from "./libraries/Maths.sol";
import { ExtraordinaryFunding } from "./base/ExtraordinaryFunding.sol";
import { StandardFunding } from "./base/StandardFunding.sol";

contract GrantFund is ExtraordinaryFunding, StandardFunding {
import { IGrantFund } from "./interfaces/IGrantFund.sol";

contract GrantFund is IGrantFund, ExtraordinaryFunding, StandardFunding {

using Checkpoints for Checkpoints.History;

Expand Down Expand Up @@ -55,6 +57,8 @@ contract GrantFund is ExtraordinaryFunding, StandardFunding {

/**
* @notice Given a proposalId, find if it is a standard or extraordinary proposal.
* @param proposalId_ The id of the proposal to query the mechanism of.
* @return FundingMechanism to which the proposal was submitted.
*/
function findMechanismOfProposal(uint256 proposalId_) public view returns (FundingMechanism) {
if (standardFundingProposals[proposalId_].proposalId != 0) return FundingMechanism.Standard;
Expand Down Expand Up @@ -192,6 +196,14 @@ contract GrantFund is ExtraordinaryFunding, StandardFunding {
}
}

/**
* @notice Retrieve the voting power of an account.
* @dev Voteing power is the minimum of the amount of votes available at a snapshot block 33 blocks prior to voting start, and at the vote starting block.
* @param account_ The voting account.
* @param snapshot_ One of block numbers to retrieve the voting power at. 33 blocks prior to the vote starting block.
* @param voteStartBlock_ The block number the vote started at.
* @return The voting power of the account.
*/
function _getVotesSinceSnapshot(address account_, uint256 snapshot_, uint256 voteStartBlock_) internal view returns (uint256) {
uint256 votes1 = token.getPastVotes(account_, snapshot_);

Expand All @@ -207,7 +219,9 @@ contract GrantFund is ExtraordinaryFunding, StandardFunding {
/**************************/

/**
* @notice Restrict voter to only voting once during the screening stage.
* @notice Check whether an account has voted on a proposal.
* @dev Votes can only votes once during the screening stage, and only once on proposals in the extraordinary funding round.
In the funding stage they can vote as long as they have budget.
* @dev See {IGovernor-hasVoted}.
* @return hasVoted_ Boolean for whether the account has already voted in the current proposal, and mechanism.
*/
Expand Down Expand Up @@ -287,7 +301,7 @@ contract GrantFund is ExtraordinaryFunding, StandardFunding {
}

/**
* @notice Required override; see {IGovernor-votingPeriod}.
* @notice Required override; see {IGovernor-votingPeriod}.
*/
function votingPeriod() public view override(IGovernor) returns (uint256) {}

Expand Down
40 changes: 16 additions & 24 deletions src/grants/base/ExtraordinaryFunding.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,7 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
/*** Proposal Functions ***/
/**************************/

/**
* @notice Execute an extraordinary funding proposal.
* @param targets_ The addresses of the contracts to call.
* @param values_ The amounts of ETH to send to each target.
* @param calldatas_ The calldata to send to each target.
* @param descriptionHash_ The hash of the proposal's description.
* @return proposalId_ The ID of the executed proposal.
*/
/// @inheritdoc IExtraordinaryFunding
function executeExtraordinary(address[] memory targets_, uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_) external nonReentrant returns (uint256 proposalId_) {
proposalId_ = hashProposal(targets_, values_, calldatas_, descriptionHash_);

Expand All @@ -54,7 +47,7 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
}

// check if the proposal has received more votes than minimumThreshold and tokensRequestedPercentage of all tokens
if (proposal.votesReceived >= proposal.tokensRequested + getSliceOfNonTreasury(getMinimumThresholdPercentage())) {
if (proposal.votesReceived >= proposal.tokensRequested + getSliceOfNonTreasury(_getMinimumThresholdPercentage())) {
proposal.succeeded = true;
} else {
proposal.succeeded = false;
Expand All @@ -70,15 +63,7 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
treasury -= proposal.tokensRequested;
}

/**
* @notice Submit a proposal to the extraordinary funding flow.
* @param endBlock_ Block number of the end of the extraordinary funding proposal voting period.
* @param targets_ Array of addresses to send transactions to.
* @param values_ Array of values to send with transactions.
* @param calldatas_ Array of calldata to execute in transactions.
* @param description_ Description of the proposal.
* @return proposalId_ ID of the newly submitted proposal.
*/
/// @inheritdoc IExtraordinaryFunding
function proposeExtraordinary(
uint256 endBlock_,
address[] memory targets_,
Expand All @@ -98,7 +83,7 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
uint256 totalTokensRequested = _validateCallDatas(targets_, values_, calldatas_);

// check tokens requested is within limits
if (totalTokensRequested > getSliceOfTreasury(Maths.WAD - getMinimumThresholdPercentage())) revert ExtraordinaryFundingProposalInvalid();
if (totalTokensRequested > getSliceOfTreasury(Maths.WAD - _getMinimumThresholdPercentage())) revert ExtraordinaryFundingProposalInvalid();

// store newly created proposal
ExtraordinaryFundingProposal storage newProposal = extraordinaryFundingProposals[proposalId_];
Expand Down Expand Up @@ -149,6 +134,11 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
emit VoteCast(account_, proposalId_, 1, votes_, "");
}

/**
* @notice Check if a proposal for extraordinary funding has succeeded.
* @param proposalId_ The ID of the proposal being checked.
* @return Boolean indicating whether the proposal has succeeded.
*/
function _extraordinaryFundingVoteSucceeded(uint256 proposalId_) internal view returns (bool) {
return extraordinaryFundingProposals[proposalId_].succeeded;
}
Expand All @@ -157,11 +147,7 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
/*** View Functions ****/
/***********************/

/**
* @notice Get the current minimum threshold percentage of Ajna tokens required for a proposal to exceed.
* @return The minimum threshold percentage, in WAD.
*/
function getMinimumThresholdPercentage() public view returns (uint256) {
function _getMinimumThresholdPercentage() internal view returns (uint256) {
// default minimum threshold is 50
if (fundedExtraordinaryProposals.length == 0) {
return 0.5 * 1e18;
Expand All @@ -172,6 +158,11 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
}
}

/// @inheritdoc IExtraordinaryFunding
function getMinimumThresholdPercentage() external view returns (uint256) {
return _getMinimumThresholdPercentage();
}

/**
* @notice Get the number of ajna tokens equivalent to a given percentage.
* @param percentage_ The percentage of the Non treasury to retrieve, in WAD.
Expand All @@ -191,6 +182,7 @@ abstract contract ExtraordinaryFunding is Funding, IExtraordinaryFunding {
return Maths.wmul(treasury, percentage_);
}

/// @inheritdoc IExtraordinaryFunding
function getExtraordinaryProposalInfo(uint256 proposalId_) external view returns (uint256, uint256, uint256, uint256, uint256, bool, bool) {
ExtraordinaryFundingProposal memory proposal = extraordinaryFundingProposals[proposalId_];
return (
Expand Down
123 changes: 41 additions & 82 deletions src/grants/base/StandardFunding.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,30 +87,7 @@ abstract contract StandardFunding is Funding, IStandardFunding {
/*** Distribution Management Functions ***/
/*****************************************/

/**
* @notice Retrieve the current QuarterlyDistribution distributionId.
*/
function getDistributionId() external view returns (uint256) {
return distributionIdCheckpoints.latest();
}

/**
* @notice Calculate the block at which the screening period of a distribution ends.
* @dev Screening period is 80 days, funding period is 10 days. Total distribution is 90 days.
* @param distributionId_ distribution Id of the distribution whose screening period is needed
*/
function getScreeningPeriodEndBlock(uint256 distributionId_) external view returns (uint256) {
QuarterlyDistribution memory currentDistribution = distributions[distributionId_];

// 10 days is equivalent to 72,000 blocks (12 seconds per block, 86400 seconds per day)
return currentDistribution.endBlock - 72000;
}

/**
* @notice Generate a unique hash of a list of proposal Ids for usage as a key for comparing proposal slates.
* @param proposalIds_ Array of proposal Ids to hash.
* @return Bytes32 hash of the list of proposals.
*/
/// @inheritdoc IStandardFunding
function getSlateHash(uint256[] calldata proposalIds_) external pure returns (bytes32) {
return keccak256(abi.encode(proposalIds_));
}
Expand Down Expand Up @@ -148,11 +125,7 @@ abstract contract StandardFunding is Funding, IStandardFunding {
isSurplusFundsUpdated[distributionId_] = true;
}

/**
* @notice Start a new Distribution Period and reset appropriate state.
* @dev Can be kicked off by anyone assuming a distribution period isn't already active.
* @return newDistributionId_ The new distribution period Id.
*/
/// @inheritdoc IStandardFunding
function startNewDistributionPeriod() external returns (uint256 newDistributionId_) {
// check that there isn't currently an active distribution period
uint256 currentDistributionId = distributionIdCheckpoints.latest();
Expand Down Expand Up @@ -194,22 +167,22 @@ abstract contract StandardFunding is Funding, IStandardFunding {
emit QuarterlyDistributionStarted(newDistributionId_, startBlock, endBlock);
}

function _sumBudgetAllocated(uint256[] memory proposalIdSubset_) internal view returns (uint256 sum) {
/**
* @notice Calculates the sum of quadratic budgets allocated to a list of proposals.
* @param proposalIdSubset_ Array of proposal Ids to sum.
* @return sum_ The sum of the budget across the given proposals.
*/
function _sumBudgetAllocated(uint256[] memory proposalIdSubset_) internal view returns (uint256 sum_) {
for (uint i = 0; i < proposalIdSubset_.length;) {
sum += uint256(standardFundingProposals[proposalIdSubset_[i]].qvBudgetAllocated);
sum_ += uint256(standardFundingProposals[proposalIdSubset_[i]].qvBudgetAllocated);

unchecked {
++i;
}
}
}

/**
* @notice Check if a slate of proposals meets requirements, and maximizes votes. If so, update QuarterlyDistribution.
* @param proposalIds_ Array of proposal Ids to check.
* @param distributionId_ Id of the current quarterly distribution.
* @return Boolean indicating whether the new proposal slate was set as the new top slate for distribution.
*/
/// @inheritdoc IStandardFunding
function checkSlate(uint256[] calldata proposalIds_, uint256 distributionId_) external returns (bool) {
QuarterlyDistribution storage currentDistribution = distributions[distributionId_];

Expand Down Expand Up @@ -247,8 +220,9 @@ abstract contract StandardFunding is Funding, IStandardFunding {

// get pointers for comparing proposal slates
bytes32 currentSlateHash = currentDistribution.fundedSlateHash;
bytes32 newSlateHash = keccak256(abi.encode(proposalIds_));
bytes32 newSlateHash = keccak256(abi.encode(proposalIds_));

// check if slate of proposals is new top slate
bool newTopSlate = currentSlateHash == 0 ||
(currentSlateHash!= 0 && sum > _sumBudgetAllocated(fundedProposalSlates[distributionId_][currentSlateHash]));

Expand All @@ -271,19 +245,7 @@ abstract contract StandardFunding is Funding, IStandardFunding {
return newTopSlate;
}

/**
* @notice Get the current maximum possible distribution of Ajna tokens that will be released from the treasury this quarter.
*/
function maximumQuarterlyDistribution() external view returns (uint256) {
return Maths.wmul(IERC20(ajnaTokenAddress).balanceOf(address(this)), GLOBAL_BUDGET_CONSTRAINT);
}

/**
* @notice distributes delegate reward based on delegatee Vote share
* @dev Can be called by anyone who has voted in both screening and funding period
* @param distributionId_ Id of distribution from which delegatee wants to claim his reward
* @return rewardClaimed_ Amount of reward claimed by delegatee
*/
/// @inheritdoc IStandardFunding
function claimDelegateReward(uint256 distributionId_) external returns(uint256 rewardClaimed_) {
// Revert if delegatee didn't vote in screening stage
if(!hasVotedScreening[distributionId_][msg.sender]) revert DelegateRewardInvalid();
Expand Down Expand Up @@ -318,14 +280,8 @@ abstract contract StandardFunding is Funding, IStandardFunding {
/*** Proposal Functions ***/
/**************************/

/**
* @notice Execute a proposal that has been approved by the community.
* @dev Calls out to Governor.execute()
* @dev Check for proposal being succesfully funded or previously executed is handled by Governor.execute().
* @return proposalId_ of the executed proposal.
*/
function executeStandard(address[] memory targets_, uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_) external payable nonReentrant returns (uint256 proposalId_) {

/// @inheritdoc IStandardFunding
function executeStandard(address[] memory targets_, uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_) external nonReentrant returns (uint256 proposalId_) {
proposalId_ = hashProposal(targets_, values_, calldatas_, descriptionHash_);
Proposal memory proposal = standardFundingProposals[proposalId_];

Expand All @@ -336,14 +292,7 @@ abstract contract StandardFunding is Funding, IStandardFunding {
standardFundingProposals[proposalId_].executed = true;
}

/**
* @notice Submit a new proposal to the Grant Coordination Fund Standard Funding mechanism.
* @dev All proposals can be submitted by anyone. There can only be one value in each array. Interface inherits from OZ.propose().
* @param targets_ List of contracts the proposal calldata will interact with. Should be the Ajna token contract for all proposals.
* @param values_ List of values to be sent with the proposal calldata. Should be 0 for all proposals.
* @param calldatas_ List of calldata to be executed. Should be the transfer() method.
* @return proposalId_ The id of the newly created proposal.
*/
/// @inheritdoc IStandardFunding
function proposeStandard(
address[] memory targets_,
uint256[] memory values_,
Expand All @@ -358,6 +307,7 @@ abstract contract StandardFunding is Funding, IStandardFunding {
QuarterlyDistribution memory currentDistribution = distributions[distributionIdCheckpoints.latest()];

// cannot add new proposal after end of screening period
// screening period ends 72000 blocks before end of distribution period, ~ 80 days.
if (block.number > currentDistribution.endBlock - 72000) revert ScreeningPeriodEnded();

// check params have matching lengths
Expand Down Expand Up @@ -486,6 +436,8 @@ abstract contract StandardFunding is Funding, IStandardFunding {

/**
* @notice Check to see if a proposal is in the current funded slate hash of proposals.
* @param proposalId_ The proposalId to check.
* @return True if the proposal is in the it's distribution period's slate hash.
*/
function _standardFundingVoteSucceeded(uint256 proposalId_) internal view returns (bool) {
Proposal memory proposal = standardFundingProposals[proposalId_];
Expand All @@ -497,13 +449,17 @@ abstract contract StandardFunding is Funding, IStandardFunding {
/*** External Functions ***/
/**************************/

/**
* @notice Retrieve the QuarterlyDistribution distributionId at a given block.
*/
/// @inheritdoc IStandardFunding
function getDistributionIdAtBlock(uint256 blockNumber) external view returns (uint256) {
return distributionIdCheckpoints.getAtBlock(blockNumber);
}

/// @inheritdoc IStandardFunding
function getDistributionId() external view returns (uint256) {
return distributionIdCheckpoints.latest();
}

/// @inheritdoc IStandardFunding
function getDistributionPeriodInfo(uint256 distributionId_) external view returns (uint256, uint256, uint256, uint256, uint256, bytes32) {
QuarterlyDistribution memory distribution = distributions[distributionId_];
return (
Expand All @@ -516,16 +472,12 @@ abstract contract StandardFunding is Funding, IStandardFunding {
);
}

/**
* @notice Get the funded proposal slate for a given distributionId, and slate hash
*/
/// @inheritdoc IStandardFunding
function getFundedProposalSlate(uint256 distributionId_, bytes32 slateHash_) external view returns (uint256[] memory) {
return fundedProposalSlates[distributionId_][slateHash_];
}

/**
* @notice Get the current state of a given proposal.
*/
/// @inheritdoc IStandardFunding
function getProposalInfo(uint256 proposalId_) external view returns (uint256, uint256, uint256, uint256, int256, bool) {
Proposal memory proposal = standardFundingProposals[proposalId_];
return (
Expand All @@ -538,9 +490,12 @@ abstract contract StandardFunding is Funding, IStandardFunding {
);
}

/**
* @notice Get the current state of a given voter in the funding stage.
*/
/// @inheritdoc IStandardFunding
function getTopTenProposals(uint256 distributionId_) external view returns (uint256[] memory) {
return topTenProposals[distributionId_];
}

/// @inheritdoc IStandardFunding
function getVoterInfo(uint256 distributionId_, address account_) external view returns (uint256, int256) {
QuadraticVoter memory voter = quadraticVoters[distributionId_][account_];
return (
Expand All @@ -549,15 +504,19 @@ abstract contract StandardFunding is Funding, IStandardFunding {
);
}

function getTopTenProposals(uint256 distributionId_) external view returns (uint256[] memory) {
return topTenProposals[distributionId_];
/// @inheritdoc IStandardFunding
function maximumQuarterlyDistribution() external view returns (uint256) {
return Maths.wmul(treasury, GLOBAL_BUDGET_CONSTRAINT);
}

/*************************/
/*** Sorting Functions ***/
/*************************/

// return the index of the proposalId in the array, else -1
/**
* @notice Identify where in an array of proposalIds the proposal exists.
* @return index_ The index of the proposalId in the array, else -1.
*/
function _findProposalIndex(uint256 proposalId, uint256[] memory array) internal pure returns (int256 index_) {
index_ = -1; // default value indicating proposalId not in the array

Expand Down
Loading

0 comments on commit 973b855

Please sign in to comment.