Platform: Code4rena
Start Date: 03/05/2023
Pot Size: $60,500 USDC
Total HM: 25
Participants: 114
Period: 8 days
Judge: Picodes
Total Solo HM: 6
Id: 234
League: ETH
Rank: 23/114
Findings: 2
Award: $423.24
🌟 Selected for report: 1
🚀 Solo Findings: 0
243.4033 USDC - $243.40
https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-grants/src/grants/base/StandardFunding.sol#L236-L265 https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-grants/src/grants/base/StandardFunding.sol#L216-L217
Each period reserves a reward for granting up to 3% (GBC: Global Budget Constraint). The GBC is split into two parts:
Voters who have participated can claim their reward after the period has ended via claimDelegateReward()
. However, the claim function does not account for the claimed reward towards treasury granting. As a result, the treasury technically reserves up to 90% in each period while actually granting 100%.
Consider this example:
1000 AJNA
. 3% is reserved for this period, resulting in a GBC of 30 AJNA
. The treasury is updated to 1000 - 30 = 970 AJNA
.27 AJNA
) and 10% is for voters (3 AJNA
).27 AJNA
are fully granted among winning proposals.0.3 AJNA
, totaling 3 AJNA
.27 AJNA + 3 AJNA
, leaving an actual balance of 970 AJNA
.970 += (30 - 27)
= 973
.973 AJNA
while having only 970 AJNA
in actuality.When the current period has ended and before starting a new one, the treasury will re-account its amount in case the last period did not utilize all the reserved reward. For example, if the last period granted only 80% of the GBC among winning proposals, the remaining 10% will be re-added to the treasury.
File: ajna-grants/src/grants/base/StandardFunding.sol 197: function _updateTreasury( 198: uint24 distributionId_ 199: ) private { 200: bytes32 fundedSlateHash = _distributions[distributionId_].fundedSlateHash; 201: uint256 fundsAvailable = _distributions[distributionId_].fundsAvailable; 202: 203: uint256[] memory fundingProposalIds = _fundedProposalSlates[fundedSlateHash]; 204: 205: uint256 totalTokensRequested; 206: uint256 numFundedProposals = fundingProposalIds.length; 207: 208: for (uint i = 0; i < numFundedProposals; ) { 209: Proposal memory proposal = _standardFundingProposals[fundingProposalIds[i]]; 210: 211: totalTokensRequested += proposal.tokensRequested; 212: 213: unchecked { ++i; } 214: } 215: 216: // readd non distributed tokens to the treasury 217: treasury += (fundsAvailable - totalTokensRequested);
In the code block above, fundsAvailable
represents 100% of the GBC and totalTokensRequested
represents up to 90% of the GBC. As a result, the treasury always adds 10% of the reserve back to its accounting.
The following PoC code is quite long because it must go through all stages. Please append and run this function in the file ajna-grants/test/unit/StandardFunding.t.sol
. The test should pass without errors.
File: ajna-grants/test/unit/StandardFunding.t.sol /* 1. startDistributionPeriod 2. proposeStandard 3. screeningVote 4. fundingVote 5. updateSlate 6. executeStandard 7. claimDelegateReward */ function testPoCTreasuryPrecisionLoss() public { // 14 tokenholders self delegate their tokens to enable voting on the proposals _selfDelegateVoters(_token, _votersArr); uint allVotersInitBalance = 50_000_000 * 1e18; emit log_named_uint("Treasury initial amount", _grantFund.treasury()); vm.roll(_startBlock + 150); /* ========================= 1. startDistributionPeriod() ========================= */ assertEq(_token.balanceOf(address(_grantFund)), 500_000_000 * 1e18, "No token should have left the treasury"); uint24 distributionId = _grantFund.startNewDistributionPeriod(); assertEq(_grantFund.getDistributionId(), distributionId, "Should have the same ID"); uint oldTreasury = _grantFund.treasury(); emit log_named_uint("Treasury after start, deduct 3%", oldTreasury); (, , , uint128 gbc, , ) = _grantFund.getDistributionPeriodInfo(distributionId); assertEq(gbc, 15_000_000 * 1e18); emit log_named_uint("GBC", uint(gbc)); assertEq(oldTreasury + gbc, 500_000_000 * 1e18, "Should be equal to the initial treasury fund"); /* ================= 2. proposeStandard() ================= */ // Request 9/10 of GBC (maximal) // 9/10 of GBC = 13_500_000 == 8_500_000 + 5_000_000 (all in WAD uint) TestProposalParams[] memory testProposalParams = new TestProposalParams[](2); testProposalParams[0] = TestProposalParams(address(this), 8_500_000 * 1e18); testProposalParams[1] = TestProposalParams(address(this), 5_000_000 * 1e18); TestProposal[] memory testProposals = _createNProposals(_grantFund, _token, testProposalParams); assertEq(testProposals.length, 2, "Should created exact 2 proposals"); vm.roll(_startBlock + 200); /* =============== 3. screeningVote() =============== */ // Demonstrate only 6 voters, all fully use their vote power (50_000_000 * 1e18) // #0 got 2 votes // #1 got 4 votes _screeningVote(_grantFund, _tokenHolder1, testProposals[0].proposalId, _getScreeningVotes(_grantFund, _tokenHolder1)); _screeningVote(_grantFund, _tokenHolder2, testProposals[0].proposalId, _getScreeningVotes(_grantFund, _tokenHolder2)); _screeningVote(_grantFund, _tokenHolder3, testProposals[1].proposalId, _getScreeningVotes(_grantFund, _tokenHolder3)); _screeningVote(_grantFund, _tokenHolder4, testProposals[1].proposalId, _getScreeningVotes(_grantFund, _tokenHolder4)); _screeningVote(_grantFund, _tokenHolder5, testProposals[1].proposalId, _getScreeningVotes(_grantFund, _tokenHolder5)); _screeningVote(_grantFund, _tokenHolder6, testProposals[1].proposalId, _getScreeningVotes(_grantFund, _tokenHolder6)); // /* ============= // 4. fundingVote() // ============= */ // skip time to move from screening period to funding period vm.roll(_startBlock + 600_000); GrantFund.Proposal[] memory proposals = _getProposalListFromProposalIds(_grantFund, _grantFund.getTopTenProposals(distributionId)); assertEq(proposals.length, 2); // Proposals should be sorted descending according to votes received so #1 should be the first and #0 should be the second assertEq(proposals[0].proposalId, testProposals[1].proposalId, "Should have the correct proposalId #1"); assertEq(proposals[0].votesReceived, 200_000_000 * 1e18, "Should have the voting score of 4 voters"); assertEq(proposals[1].proposalId, testProposals[0].proposalId, "Should have the correct proposalId #0"); assertEq(proposals[1].votesReceived, 100_000_000 * 1e18, "Should have the voting score of 2 voters"); // funding period votes for two competing slates, 1, or 2 and 3 // #1 got 3 funding votes // #0 got 3 funding votes _fundingVote(_grantFund, _tokenHolder1, proposals[0].proposalId, voteYes, 50_000_000 * 1e18); _fundingVote(_grantFund, _tokenHolder2, proposals[1].proposalId, voteYes, 50_000_000 * 1e18); _fundingVote(_grantFund, _tokenHolder3, proposals[1].proposalId, voteYes, 50_000_000 * 1e18); _fundingVote(_grantFund, _tokenHolder4, proposals[1].proposalId, voteYes, 50_000_000 * 1e18); _fundingVote(_grantFund, _tokenHolder5, proposals[0].proposalId, voteYes, 50_000_000 * 1e18); _fundingVote(_grantFund, _tokenHolder6, proposals[0].proposalId, voteYes, 50_000_000 * 1e18); // Ensure that all 6 holders have fully voted. for (uint i = 0; i < 6; i++) { (uint128 voterPower, uint128 votingPowerRemaining, uint256 votesCast) = _grantFund.getVoterInfo(distributionId, _votersArr[i]); assertEq(voterPower, 2_500_000_000_000_000 * 1e18, "Should have 50m^2 voting power"); assertEq(votingPowerRemaining, 0, "Should have fully voted"); } // /* ============= // 5. updateSlate() // ============= */ // skip to the end of the DistributionPeriod vm.roll(_startBlock + 650_000); // Updating potential Proposal Slate to include proposal that is in topTenProposal (funding Stage) uint256[] memory slate = new uint256[](proposals.length); // length = 2 slate[0] = proposals[0].proposalId; slate[1] = proposals[1].proposalId; require(_grantFund.updateSlate(slate, distributionId), "Should update slate success"); (, , , , , bytes32 slateHash) = _grantFund.getDistributionPeriodInfo(distributionId); assertTrue(slateHash != bytes32(0)); proposals = _getProposalListFromProposalIds(_grantFund, _grantFund.getFundedProposalSlate(slateHash)); // /* ================= // 6. executeStandard() // ================= */ // skip to the end of the Distribution's challenge period vm.roll(_startBlock + 700_000); // execute funded proposals assertEq(_token.balanceOf(address(this)), 0, "This contract should have 0 token amount"); _grantFund.executeStandard(testProposals[0].targets, testProposals[0].values, testProposals[0].calldatas, keccak256(bytes(testProposals[0].description))); _grantFund.executeStandard(testProposals[1].targets, testProposals[1].values, testProposals[1].calldatas, keccak256(bytes(testProposals[1].description))); assertEq(testProposals[0].tokensRequested + testProposals[1].tokensRequested, _token.balanceOf(address(this)), "The contract should received correct granted amount"); emit log_named_uint("totalTokensRequested", _token.balanceOf(address(this))); assertEq(_token.balanceOf(address(this)), gbc * 9/10, "Should be equal to 90% of GBC"); proposals = _getProposalListFromProposalIds(_grantFund, _grantFund.getFundedProposalSlate(slateHash)); assertTrue(proposals[0].executed && proposals[1].executed, "Should have successfully executed"); // /* ================= // 7. claimDelegateReward() // ================= */ // Claim delegate reward for all delegatees // delegates who didn't vote with their full power receive fewer rewards uint totalDelegationRewards; for (uint i = 0; i < _votersArr.length; i++) { uint estimatedRewards = _grantFund.getDelegateReward(distributionId, _votersArr[i]); changePrank(_votersArr[i]); if (i > 5) { // these are holders who haven't participated in this period, should have 0 reward // _tokenHolder7 and above vm.expectRevert(IStandardFunding.DelegateRewardInvalid.selector); uint actualRewards = _grantFund.claimDelegateReward(distributionId); assertTrue(estimatedRewards == 0 && actualRewards == 0, "Should be ineligible for rewards"); assertFalse(_grantFund.hasClaimedReward(distributionId, _votersArr[i]), "Should unable to claim"); assertEq(_token.balanceOf(_votersArr[i]), allVotersInitBalance, "Balance should be the same as starting"); } else { // these are holders who have voted // _tokenHolder1 - 6 uint actualRewards = _grantFund.claimDelegateReward(distributionId); assertEq(estimatedRewards, actualRewards, "Should received the exact reward amount"); assertTrue(estimatedRewards != 0 && actualRewards != 0, "Should be eligible for rewards"); assertTrue(_grantFund.hasClaimedReward(distributionId, _votersArr[i]), "Should claim successfully"); assertEq(_token.balanceOf(_votersArr[i]), allVotersInitBalance + actualRewards, "Should have the final balance equal to init+reward"); totalDelegationRewards += actualRewards; } } emit log_named_uint("Total claimed rewards", totalDelegationRewards); assertEq(totalDelegationRewards, gbc / 10, "Should be equal to 10% of GBC"); assertEq(totalDelegationRewards + _token.balanceOf(address(this)), gbc, "10% + 90% = 100%"); assertEq(totalDelegationRewards + _token.balanceOf(address(this)) + oldTreasury, 500_000_000 * 1e18, "10% + 90% + remaining = initial treasury"); emit log_named_uint("Treasury at the end of the period (should be the same as started)", _grantFund.treasury()); // Put the treasury back to the same value as the last period to have the same GBC for easier to compare. // Remember this equation? "10% + 90% + remaining = initial treasury" // Current _grantFund.treasury() = remaining. // _token.balanceOf(address(this)) = 90% // _grantFund.startNewDistributionPeriod() -> _grantFund._updateTreasury() = 10% (because of the invalid logic) changePrank(address(this)); _token.approve(address(_grantFund), _token.balanceOf(address(this))); // only put 90% back to the treasury _grantFund.fundTreasury(_token.balanceOf(address(this))); // 10% + (90%&remaining) = initial treasury assertEq(totalDelegationRewards + _grantFund.treasury(), 500_000_000 * 1e18, "Should be equal to the initial treasury"); // The function put 10% back in, while in the actual all 100% has been spent. Loss 10%. _grantFund.startNewDistributionPeriod(); emit log_named_uint("Treasury at the new period (got updated)", _grantFund.treasury()); assertEq(_token.balanceOf(address(_grantFund)), 498_500_000 * 1e18, "Should be initial-10%"); emit log_named_uint("treasury actual balance", _token.balanceOf(address(_grantFund))); // The same GBC evidenced that treasury = 500_000_000 * 1e18 at the time it was calculated, // But the actual balance is 500_000_000 * 1e18 - 10% = 498_500_000 * 1e18. (, , , uint128 newGbc, , ) = _grantFund.getDistributionPeriodInfo(distributionId); assertEq(oldTreasury + gbc, _grantFund.treasury() + gbc, "Should have the same GBC as previous period"); assertEq(gbc, newGbc, "Should have the same GBC as previous period"); }
run: forge test --match-test testPoCTreasuryPrecisionLoss -vv Running 1 test for test/unit/StandardFunding.t.sol:StandardFundingGrantFundTest [PASS] testPoCTreasuryPrecisionLoss() (gas: 3451937) Logs: Treasury initial amount: 500000000000000000000000000 Treasury after start, deduct 3%: 485000000000000000000000000 GBC: 15000000000000000000000000 totalTokensRequested: 13500000000000000000000000 Total claimed rewards: 1500000000000000000000000 Treasury at the end of the period (should be the same as started): 485000000000000000000000000 Treasury at the new period (got updated): 485000000000000000000000000 treasury actual balance: 498500000000000000000000000 Test result: ok. 1 passed; 0 failed; finished in 1.20s
If it is safe to assume that all periods will always have 10% for delegation rewards, the contract should calculate only 90% of fundsAvailable
when updating the treasury.
File: ajna-grants/src/grants/base/StandardFunding.sol 197: function _updateTreasury( 198: uint24 distributionId_ 199: ) private { 200: bytes32 fundedSlateHash = _distributions[distributionId_].fundedSlateHash; 201: uint256 fundsAvailable = _distributions[distributionId_].fundsAvailable; ... 216: // readd non distributed tokens to the treasury +217: treasury += ((fundsAvailable * 9/10) - totalTokensRequested);
The claimDelegateReward()
function uses Maths.wmul()
, which automatically rounds the multiplication result up or down. For example, Maths.wmul(1, 0.5 * 1e18) = 1
(rounding up) while Maths.wmul(1, 0.49 * 1e18) = 0
(rounding down). As a result, rewardClaimed_
can lose precision for small decimal amounts and token holders typically have small fractions of tokens down to 1 wei
. It is uncertain, but the total actual paid rewards could be more than 10% if rounded up, resulting in an insignificant loss of precision in the treasury. However, if rewardClaimed_
is deducted from fundsAvailable
, it could lead to an integer underflow revert if fundsAvailable - totalClaimed - totalTokensRequested = 100% - 10.xx% - 90%
, which exceeds 100%.
Math
#0 - c4-judge
2023-05-18T09:56:06Z
Picodes marked the issue as duplicate of #263
#1 - c4-judge
2023-05-30T18:09:18Z
Picodes marked the issue as duplicate of #263
#2 - c4-judge
2023-05-30T18:10:44Z
Picodes changed the severity to 3 (High Risk)
#3 - c4-judge
2023-05-30T18:13:31Z
Picodes marked the issue as satisfactory
#4 - c4-judge
2023-05-30T18:15:10Z
Picodes marked the issue as selected for report
🌟 Selected for report: JCN
Also found by: 0x73696d616f, 0xSmartContract, 0xnev, Audit_Avengers, Aymen0909, Blckhv, Eurovickk, K42, Kenshin, Rageur, Raihan, ReyAdmirado, SAAJ, SAQ, Shubham, Tomio, Walter, ayden, codeslide, descharre, dicethedev, hunter_w3b, j4ld1na, kaveyjoe, okolicodes, patitonar, petrichor, pontifex, yongskiws
179.8397 USDC - $179.84
The benchmark used Foundry's default optimizer setting. The ExtraordinaryFunding.sol
contract was tested using all functions in the ExtraordinaryFunding.t.sol
contract, excluding the testFuzzExtraordinaryFunding()
function. The StandardFunding.sol
contract was tested using only the testDistributionPeriodEndToEnd()
function.
Running command:
forge test --gas-report --match-contract ExtraordinaryFundingGrantFundTest --no-match-test testFuzzExtraordinaryFunding
forge test --gas-report --match-test testDistributionPeriodEndToEnd
The overall average gas savings are summarized in the table below.
Contract | Function Name | Before | After | Gas Savings |
---|---|---|---|---|
ExtraordinaryFunding | executeExtraordinary | 31595 | 29790 | 1805 |
ExtraordinaryFunding | hashProposal | 3985 | 2256 | 1729 |
ExtraordinaryFunding | proposeExtraordinary | 55381 | 51113 | 4268 |
ExtraordinaryFunding | voteExtraordinary | 26705 | 26245 | 460 |
Subtotal: | 8262 | |||
StandardFunding | claimDelegateReward | 33591 | 33741 | -150 |
StandardFunding | executeStandard | 3561 | 21737 | 1824 |
StandardFunding | fundingVote | 121127 | 120444 | 683 |
StandardFunding | proposeStandard | 83311 | 77480 | 5831 |
StandardFunding | screeningVote | 47128 | 46065 | 1063 |
StandardFunding | updateSlate | 27106 | 23427 | 3679 |
Subtotal: | 12930 | |||
Total gas saved: | 21192 |
# | Topic | Instances |
---|---|---|
G-01 | Replace address[] targets_ with constant variable and remove unused uint[] values_ | 4 |
G-02 | Unmodified external parameter can be declared as calldata | 1 |
G-03 | Remove unused event variable | 3 |
G-04 | Unneccessary revert check | 1 |
G-05 | Don't cache variable only used once | 2 |
G-06 | Reverts check should be located at the beginning of the function if possible | 1 |
G-07 | Delete old slate when replacing a new one will receive gas refund | 1 |
G-08 | Storage variables used more than once should be cached to local variables | 2 |
G-09 | Reading msg.sender is cheaper than caching it to a local address variable | 1 |
G-10 | Merge multiple loops that iterate through the same array | 1 |
address[] targets_
with constant variable and remove unused uint[] values_
Number of instances: 4
The input target
must hold AJNA token address that already has been hardcoded in the contract code. The contract can use to the hardcoded constant address instead of re-checking that user provide the exact address. The input value
can only be 0
, thus, there is no need to declare this variable in the first place.
1. ExtraordinaryFunding.executeExtraordinary | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 7604 | 31595 | 12339 | 94100 | 4 |
After | 5850 | 29812 | 10585 | 92228 | 4 |
Gas Savings | 1754 | 1783 | 1754 | 1872 | - |
File: ajna-grants/src/grants/base/Funding.sol 115: if (targets_[i] != ajnaTokenAddress || values_[i] != 0) revert InvalidProposal();
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol @@ -54,12 +54,10 @@ /// @inheritdoc IExtraordinaryFunding function executeExtraordinary( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) external nonReentrant override returns (uint256 proposalId_) { - proposalId_ = _hashProposal(targets_, values_, calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_EXTRAORDINARY, descriptionHash_))); + proposalId_ = _hashProposal(calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_EXTRAORDINARY, descriptionHash_))); ExtraordinaryFundingProposal storage proposal = _extraordinaryFundingProposals[proposalId_]; @@ -78,7 +76,7 @@ treasury -= tokensRequested; // execute proposal's calldata - _execute(proposalId_, targets_, values_, calldatas_); + _execute(proposalId_, calldatas_); }
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol @@ -44,15 +44,11 @@ /** * @notice Execute an extraordinary funding proposal if it has passed its' requisite vote threshold. - * @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 string. * @return proposalId_ The ID of the executed proposal. */ function executeExtraordinary( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) external returns (uint256 proposalId_);
This requires modifying Funding.sol for related functions as follows:
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol after/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol @@ -45,22 +45,18 @@ /** * @notice Execute the calldata of a passed proposal. - * @param targets_ The list of smart contract targets for the calldata execution. Should be the Ajna token address. - * @param values_ Unused. Should be 0 since all calldata is executed on the Ajna token's transfer method. * @param calldatas_ The list of calldatas to execute. */ function _execute( uint256 proposalId_, - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_ ) internal { // use common event name to maintain consistency with tally emit ProposalExecuted(proposalId_); string memory errorMessage = "Governor: call reverted without message"; - for (uint256 i = 0; i < targets_.length; ++i) { - (bool success, bytes memory returndata) = targets_[i].call{value: values_[i]}(calldatas_[i]); + for (uint256 i = 0; i < calldatas_.length; ++i) { + (bool success, bytes memory returndata) = ajnaTokenAddress.call(calldatas_[i]); Address.verifyCallResult(success, returndata, errorMessage); } } @@ -143,18 +139,14 @@ /** * @notice Create a proposalId from a hash of proposal's targets, values, and calldatas arrays, and a description hash. * @dev Consistent with proposalId generation methods used in OpenZeppelin Governor. - * @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 string. Generated by keccak256(bytes(description))). * @return proposalId_ The hashed proposalId created from the provided params. */ function _hashProposal( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) internal pure returns (uint256 proposalId_) { - proposalId_ = uint256(keccak256(abi.encode(targets_, values_, calldatas_, descriptionHash_))); + proposalId_ = uint256(keccak256(abi.encode(calldatas_, descriptionHash_))); } }
2. ExtraordinaryFunding.proposeExtraordinary | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 4960 | 55381 | 86947 | 86947 | 10 |
After | 3220 | 51148 | 81153 | 81153 | 10 |
Gas Savings | 1740 | 4233 | 5794 | 5794 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol /// @inheritdoc IExtraordinaryFunding function proposeExtraordinary( uint256 endBlock_, - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, string memory description_) external override returns (uint256 proposalId_) { - proposalId_ = _hashProposal(targets_, values_, calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_EXTRAORDINARY, keccak256(bytes(description_))))); + proposalId_ = _hashProposal(calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_EXTRAORDINARY, keccak256(bytes(description_))))); ExtraordinaryFundingProposal storage newProposal = _extraordinaryFundingProposals[proposalId_]; @@ -99,7 +95,7 @@ // check proposal length is within limits of 1 month maximum if (block.number + MAX_EFM_PROPOSAL_LENGTH < endBlock_) revert InvalidProposal(); - uint128 totalTokensRequested = _validateCallDatas(targets_, values_, calldatas_); + uint128 totalTokensRequested = _validateCallDatas(calldatas_); // check tokens requested are available for claiming from the treasury if (uint256(totalTokensRequested) > _getSliceOfTreasury(Maths.WAD - _getMinimumThresholdPercentage())) revert InvalidProposal(); @@ -113,9 +109,6 @@ emit ProposalCreated( proposalId_, msg.sender, - targets_, - values_, - new string[](targets_.length), calldatas_, block.number, endBlock_,
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IExtraordinaryFunding.sol @@ -60,16 +56,12 @@ /** * @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. */ function proposeExtraordinary( uint256 endBlock_, - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, string memory description_ ) external returns (uint256 proposalId_);
This requires modifying Funding.sol and IFunding.sol for related functions as follows:
--- before/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol @@ -95,25 +91,15 @@ /** * @notice Verifies proposal's targets, values, and calldatas match specifications. * @dev Counters incremented in an unchecked block due to being bounded by array length. - * @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. * @return tokensRequested_ The amount of tokens requested in the calldata. */ function _validateCallDatas( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_ ) internal view returns (uint128 tokensRequested_) { - // check params have matching lengths - if (targets_.length == 0 || targets_.length != values_.length || targets_.length != calldatas_.length) revert InvalidProposal(); + for (uint256 i = 0; i < calldatas_.length;) { - for (uint256 i = 0; i < targets_.length;) { - - // check targets and values params are valid - if (targets_[i] != ajnaTokenAddress || values_[i] != 0) revert InvalidProposal(); - // check calldata function selector is transfer() bytes memory selDataWithSig = calldatas_[i]; @@ -143,18 +129,14 @@ /** * @notice Create a proposalId from a hash of proposal's targets, values, and calldatas arrays, and a description hash. * @dev Consistent with proposalId generation methods used in OpenZeppelin Governor. - * @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 string. Generated by keccak256(bytes(description))). * @return proposalId_ The hashed proposalId created from the provided params. */ function _hashProposal( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) internal pure returns (uint256 proposalId_) { - proposalId_ = uint256(keccak256(abi.encode(targets_, values_, calldatas_, descriptionHash_))); + proposalId_ = uint256(keccak256(abi.encode(calldatas_, descriptionHash_))); } }
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol @@ -54,9 +54,6 @@ event ProposalCreated( uint256 proposalId, address proposer, - address[] targets, - uint256[] values, - string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock,
3. StandardFunding.executeStandard | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 9220 | 23561 | 9811 | 48114 | 5 |
After | 7451 | 21745 | 8042 | 46228 | 5 |
Gas Savings | 1769 | 1816 | 1769 | 1886 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -341,12 +341,10 @@ /// @inheritdoc IStandardFunding function executeStandard( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) external nonReentrant override returns (uint256 proposalId_) { - proposalId_ = _hashProposal(targets_, values_, calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_STANDARD, descriptionHash_))); + proposalId_ = _hashProposal(calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_STANDARD, descriptionHash_))); Proposal storage proposal = _standardFundingProposals[proposalId_]; uint24 distributionId = proposal.distributionId; @@ -359,7 +357,7 @@ proposal.executed = true; - _execute(proposalId_, targets_, values_, calldatas_); + _execute(proposalId_, calldatas_); }
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol @@ -187,15 +187,11 @@ * @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(). - * @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. * @param descriptionHash_ Hash of proposal's description string. * @return proposalId_ The id of the executed proposal. */ function executeStandard( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) external returns (uint256 proposalId_);
This requires modifying Funding.sol for related functions as follows:
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol after/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol @@ -45,22 +45,18 @@ /** * @notice Execute the calldata of a passed proposal. - * @param targets_ The list of smart contract targets for the calldata execution. Should be the Ajna token address. - * @param values_ Unused. Should be 0 since all calldata is executed on the Ajna token's transfer method. * @param calldatas_ The list of calldatas to execute. */ function _execute( uint256 proposalId_, - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_ ) internal { // use common event name to maintain consistency with tally emit ProposalExecuted(proposalId_); string memory errorMessage = "Governor: call reverted without message"; - for (uint256 i = 0; i < targets_.length; ++i) { - (bool success, bytes memory returndata) = targets_[i].call{value: values_[i]}(calldatas_[i]); + for (uint256 i = 0; i < calldatas_.length; ++i) { + (bool success, bytes memory returndata) = ajnaTokenAddress.call(calldatas_[i]); Address.verifyCallResult(success, returndata, errorMessage); } } @@ -143,18 +139,14 @@ /** * @notice Create a proposalId from a hash of proposal's targets, values, and calldatas arrays, and a description hash. * @dev Consistent with proposalId generation methods used in OpenZeppelin Governor. - * @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 string. Generated by keccak256(bytes(description))). * @return proposalId_ The hashed proposalId created from the provided params. */ function _hashProposal( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) internal pure returns (uint256 proposalId_) { - proposalId_ = uint256(keccak256(abi.encode(targets_, values_, calldatas_, descriptionHash_))); + proposalId_ = uint256(keccak256(abi.encode(calldatas_, descriptionHash_))); } }
4. StandardFunding.proposeStandard | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 83311 | 83311 | 83311 | 83311 | 7 |
After | 77598 | 77598 | 77598 | 77598 | 7 |
Gas Savings | 5713 | 5713 | 5713 | 5713 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol /// @inheritdoc IStandardFunding function proposeStandard( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, string memory description_ ) external override returns (uint256 proposalId_) { - proposalId_ = _hashProposal(targets_, values_, calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_STANDARD, keccak256(bytes(description_))))); + proposalId_ = _hashProposal(calldatas_, keccak256(abi.encode(DESCRIPTION_PREFIX_HASH_STANDARD, keccak256(bytes(description_))))); Proposal storage newProposal = _standardFundingProposals[proposalId_]; @@ -385,7 +381,7 @@ // store new proposal information newProposal.proposalId = proposalId_; newProposal.distributionId = currentDistribution.id; - newProposal.tokensRequested = _validateCallDatas(targets_, values_, calldatas_); // check proposal parameters are valid and update tokensRequested + newProposal.tokensRequested = _validateCallDatas(calldatas_); // check proposal parameters are valid and update tokensRequested // revert if proposal requested more tokens than are available in the distribution period if (newProposal.tokensRequested > (currentDistribution.fundsAvailable * 9 / 10)) revert InvalidProposal(); @@ -393,9 +389,6 @@ emit ProposalCreated( proposalId_, msg.sender, - targets_, - values_, - new string[](targets_.length), calldatas_, block.number, currentDistribution.endBlock,
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IStandardFunding.sol @@ -203,15 +199,11 @@ /** * @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. * @param description_ Proposal's description string. * @return proposalId_ The id of the newly created proposal. */ function proposeStandard( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, string memory description_ ) external returns (uint256 proposalId_); This requires modifying [Funding.sol](https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/Funding.sol) and [IFunding.sol](https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/interfaces/IFunding.sol) for related functions as follows: ```diff --- before/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/Funding.sol @@ -95,25 +91,15 @@ /** * @notice Verifies proposal's targets, values, and calldatas match specifications. * @dev Counters incremented in an unchecked block due to being bounded by array length. - * @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. * @return tokensRequested_ The amount of tokens requested in the calldata. */ function _validateCallDatas( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_ ) internal view returns (uint128 tokensRequested_) { - // check params have matching lengths - if (targets_.length == 0 || targets_.length != values_.length || targets_.length != calldatas_.length) revert InvalidProposal(); + for (uint256 i = 0; i < calldatas_.length;) { - for (uint256 i = 0; i < targets_.length;) { - - // check targets and values params are valid - if (targets_[i] != ajnaTokenAddress || values_[i] != 0) revert InvalidProposal(); - // check calldata function selector is transfer() bytes memory selDataWithSig = calldatas_[i]; @@ -143,18 +129,14 @@ /** * @notice Create a proposalId from a hash of proposal's targets, values, and calldatas arrays, and a description hash. * @dev Consistent with proposalId generation methods used in OpenZeppelin Governor. - * @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 string. Generated by keccak256(bytes(description))). * @return proposalId_ The hashed proposalId created from the provided params. */ function _hashProposal( - address[] memory targets_, - uint256[] memory values_, bytes[] memory calldatas_, bytes32 descriptionHash_ ) internal pure returns (uint256 proposalId_) { - proposalId_ = uint256(keccak256(abi.encode(targets_, values_, calldatas_, descriptionHash_))); + proposalId_ = uint256(keccak256(abi.encode(calldatas_, descriptionHash_))); } }
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol @@ -54,9 +54,6 @@ event ProposalCreated( uint256 proposalId, address proposer, - address[] targets, - uint256[] values, - string[] signatures, bytes[] calldatas, uint256 startBlock, uint256 endBlock,
calldata
Number of instances: 1
For external variable that is for read-only purpose can be declared as calldata
instead.
1. StandardFunding.screeningVote | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1950 | 47128 | 58419 | 78738 | 11 |
After | 1481 | 46648 | 57937 | 78257 | 11 |
Gas Savings | 469 | 480 | 482 | 481 | - |
--- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -570,7 +570,7 @@ /// @inheritdoc IStandardFunding function screeningVote( - ScreeningVoteParams[] memory voteParams_ + ScreeningVoteParams[] calldata voteParams_ ) external override returns (uint256 votesCast_) { QuarterlyDistribution memory currentDistribution = _distributions[_currentDistributionId];
Number of instances: 3
Every functions that emit VoteCast()
sending an empty string for reason
variable. The variable should be removed if it has no usage.
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol after/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/interfaces/IFunding.sol @@ -66,7 +66,7 @@ /** * @dev Emitted when votes are cast on a proposal. */ - event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); + event VoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight); /***************/ /*** Structs ***/
1. ExtraordinaryFunding.voteExtraordinary | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 571 | 26705 | 29518 | 31565 | 33 |
After | 571 | 26203 | 28961 | 31008 | 33 |
Gas Savings | 0 | 502 | 557 | 557 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol @@ -151,8 +151,7 @@ msg.sender, proposalId_, 1, - votesCast_, - "" + votesCast_ ); }
2. StandardFunding.fundingVote | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1669 | 121127 | 105988 | 410840 | 10 |
After | 1669 | 120466 | 105434 | 407544 | 10 |
Gas Savings | 0 | 661 | 554 | 3296 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -684,8 +684,7 @@ account_, proposalId, support, - incrementalVotesUsed_, - "" + incrementalVotesUsed_ ); }
3. StandardFunding.screeningVote | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1950 | 47128 | 58419 | 78738 | 11 |
After | 1950 | 46624 | 57864 | 78184 | 11 |
Gas Savings | 0 | 504 | 555 | 554 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -747,8 +746,7 @@ account_, proposalId, 1, - votes_, - "" + votes_ ); }
Number of instances: 1
The function will be reverted by uninitailized endBlock
or integer underflow any way. The following line can be considered unnecessary revert check. It can be removed to cost less gas.
File: ajna-grants/src/grants/base/ExtraordinaryFunding.sol 246: function _getVotesExtraordinary(address account_, uint256 proposalId_) internal view returns (uint256 votes_) { 247: if (proposalId_ == 0) revert ExtraordinaryFundingProposalInactive();
1. ExtraordinaryFunding.voteExtraordinary | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 571 | 26705 | 29518 | 31565 | 30 |
After | 571 | 26681 | 29492 | 31539 | 30 |
Gas Savings | 0 | 24 | 26 | 26 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/ExtraordinaryFunding.sol @@ -244,8 +244,6 @@ * @return votes_ The number of votes available to be cast in voteExtraordinary. */ function _getVotesExtraordinary(address account_, uint256 proposalId_) internal view returns (uint256 votes_) { - if (proposalId_ == 0) revert ExtraordinaryFundingProposalInactive(); - uint256 startBlock = _extraordinaryFundingProposals[proposalId_].startBlock; votes_ = _getVotesAtSnapshotBlocks(
Number of instances: 2
Any storage values that only used once should not be cached to a local variable becuase it will cost more gas by declaring.
A special test function was written to benchmark this optimization. Please append the following code to ajna-grants/test/unit/StandardFunding.t.sol
if you wish to try it yourself.
function testUpdateTreasury() external { for (uint i; i < 50; i++) { _grantFund.startNewDistributionPeriod(); vm.roll(block.number + (100 days / 12)); } }
1. StandardFunding.startNewDistributionPeriod | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 52227 | 52321 | 52227 | 55223 | 50 |
After | 52210 | 52304 | 52210 | 52206 | 50 |
Gas Savings | 17 | 17 | 17 | 3017 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -224,8 +224,8 @@ * @dev Increments the previous Id nonce by 1. * @return newId_ The new distribution period Id. */ - function _setNewDistributionId() private returns (uint24 newId_) { - newId_ = _currentDistributionId += 1; + function _setNewDistributionId() private returns (uint24) { + return ++_currentDistributionId; } /************************************
2. StandardFunding.updateSlate | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1002 | 27106 | 8874 | 78805 | 10 |
After | 1002 | 27103 | 8870 | 78800 | 10 |
Gas Savings | 0 | 3 | 4 | 5 | - |
--- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -427,7 +427,6 @@ // check that the slate has no duplicates if (_hasDuplicates(proposalIds_)) revert InvalidProposalSlate(); - uint256 gbc = distributionPeriodFundsAvailable_; uint256 totalTokensRequested = 0; // check each proposal in the slate is valid @@ -445,7 +444,7 @@ totalTokensRequested += proposal.tokensRequested; // check if slate of proposals exceeded budget constraint ( 90% of GBC ) - if (totalTokensRequested > (gbc * 9 / 10)) { + if (totalTokensRequested > (distributionPeriodFundsAvailable_ * 9 / 10)) { revert InvalidProposalSlate(); }
Number of instances: 1
Reverts check should be re-located to the beginning of the function if it can be executed immidiately. This helps reverted transactions cost less gas.
1. StandardFunding.claimDelegateReward | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1459 | 33591 | 36744 | 63575 | 9 |
After | 961 | 33758 | 36744 | 63575 | 9 |
Gas Savings | 498 | -168 | 0 | 0 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -238,15 +238,15 @@ ) external override returns(uint256 rewardClaimed_) { // Revert if delegatee didn't vote in screening stage if(screeningVotesCast[distributionId_][msg.sender] == 0) revert DelegateRewardInvalid(); + + // check rewards haven't already been claimed + if(hasClaimedReward[distributionId_][msg.sender]) revert RewardAlreadyClaimed(); QuarterlyDistribution memory currentDistribution = _distributions[distributionId_]; // Check if Challenge Period is still active if(block.number < _getChallengeStageEndBlock(currentDistribution.endBlock)) revert ChallengePeriodNotEnded(); - // check rewards haven't already been claimed - if(hasClaimedReward[distributionId_][msg.sender]) revert RewardAlreadyClaimed(); - QuadraticVoter memory voter = _quadraticVoters[distributionId_][msg.sender]; // calculate rewards earned for voting
Number of instances: 1
If user submit a new slate that has more cumulative votes and utilized the treasury fund more optimal it will replace the old one. However when the old one got repleced, its data was not deleted. Unused mapping storage can be removed to get gas refund.
1. StandardFunding.updateSlate | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1002 | 27106 | 8874 | 78805 | 10 |
After | 1002 | 24308 | 8874 | 73621 | 10 |
Gas Savings | 0 | 2798 | 0 | 5184 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -331,6 +331,7 @@ // update hash to point to the new leading slate of proposals currentDistribution.fundedSlateHash = newSlateHash; + delete _fundedProposalSlates[currentSlateHash]; emit FundedSlateUpdated( distributionId_,
Number of instances: 2
Storage variables that are used multiple times should be cacahed to local variables as local variable costs less gas.
1. StandardFunding.proposeStandard | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 83311 | 83311 | 83311 | 83311 | 7 |
After | 83221 | 83221 | 83221 | 83221 | 7 |
Gas Savings | 90 | 90 | 90 | 90 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -385,11 +385,13 @@ // store new proposal information newProposal.proposalId = proposalId_; newProposal.distributionId = currentDistribution.id; - newProposal.tokensRequested = _validateCallDatas(targets_, values_, calldatas_); // check proposal parameters are valid and update tokensRequested + uint128 tokensRequested = _validateCallDatas(calldatas_); // check proposal parameters are valid and update tokensRequested // revert if proposal requested more tokens than are available in the distribution period - if (newProposal.tokensRequested > (currentDistribution.fundsAvailable * 9 / 10)) revert InvalidProposal(); + if (tokensRequested > (currentDistribution.fundsAvailable * 9 / 10)) revert InvalidProposal(); + newProposal.tokensRequested = tokensRequested; + emit ProposalCreated( proposalId_, msg.sender,
2. StandardFunding.updateSlate | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1002 | 27106 | 8874 | 76605 | 10 |
After | 1002 | 26221 | 7487 | 77343 | 10 |
Gas Savings | 0 | 885 | 1387 | -738 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -429,13 +429,14 @@ uint256 gbc = distributionPeriodFundsAvailable_; uint256 totalTokensRequested = 0; + uint[] memory _topTen = _topTenProposals[distributionId_]; // check each proposal in the slate is valid for (uint i = 0; i < numProposalsInSlate_; ) { Proposal memory proposal = _standardFundingProposals[proposalIds_[i]]; // check if Proposal is in the topTenProposals list - if (_findProposalIndex(proposalIds_[i], _topTenProposals[distributionId_]) == -1) revert InvalidProposalSlate(); + if (_findProposalIndex(proposalIds_[i], _topTen) == -1) revert InvalidProposalSlate(); // account for fundingVotesReceived possibly being negative if (proposal.fundingVotesReceived < 0) revert InvalidProposalSlate();
msg.sender
is cheaper than caching it to a local address
variableNumber of instances: 1
Using msg.sender
is cheaper than using a local variable as it costs 2 gas (CALLER) while local variable cost starting at 3 (MLOAD/MSTORE). Reference: https://ethereum.org/en/developers/docs/evm/opcodes/.
1. StandardFunding.screeningVote | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1950 | 47128 | 58419 | 78738 | 11 |
After | 1950 | 47072 | 58357 | 78876 | 11 |
Gas Savings | 0 | 56 | 62 | -138 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -589,7 +589,7 @@ // cast each successive vote votesCast_ += votes; - _screeningVote(msg.sender, proposal, votes); + _screeningVote(proposal, votes); unchecked { ++i; } } @@ -691,19 +691,17 @@ /** * @notice Vote on a proposal in the screening stage of the Distribution Period. - * @param account_ The voting account. * @param proposal_ The current proposal being voted upon. * @param votes_ The amount of votes being cast. */ function _screeningVote( - address account_, Proposal storage proposal_, uint256 votes_ ) internal { uint24 distributionId = proposal_.distributionId; // check that the voter has enough voting power to cast the vote - if (screeningVotesCast[distributionId][account_] + votes_ > _getVotesScreening(distributionId, account_)) revert InsufficientVotingPower(); + if (screeningVotesCast[distributionId][msg.sender] + votes_ > _getVotesScreening(distributionId, msg.sender)) revert InsufficientVotingPower(); uint256[] storage currentTopTenProposals = _topTenProposals[distributionId]; uint256 proposalId = proposal_.proposalId; @@ -740,11 +738,11 @@ } // record voters vote - screeningVotesCast[proposal_.distributionId][account_] += votes_; + screeningVotesCast[proposal_.distributionId][msg.sender] += votes_; // emit VoteCast instead of VoteCastWithParams to maintain compatibility with Tally emit VoteCast( - account_, + msg.sender, proposalId, 1, votes_,
Number of instances: 1
The _validateSlate()
function iterates over an array twice: first, to check for duplicate elements, and second, to perform a deeper validation. These two iterations can be merged into a single loop for improved efficiency.
1. StandardFunding.updateSlate | Min | Avg | Median | Max | # calls |
---|---|---|---|---|---|
Before | 1002 | 27106 | 8874 | 78805 | 10 |
After | 1002 | 26940 | 8686 | 78617 | 10 |
Gas Savings | 0 | 166 | 188 | 188 | - |
run: diff -u before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol --- before/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol +++ after/2023-05-ajna/ajna-grants/src/grants/base/StandardFunding.sol @@ -423,15 +423,19 @@ if (block.number <= endBlock || block.number > _getChallengeStageEndBlock(endBlock)) { revert InvalidProposalSlate(); } - - // check that the slate has no duplicates - if (_hasDuplicates(proposalIds_)) revert InvalidProposalSlate(); uint256 gbc = distributionPeriodFundsAvailable_; uint256 totalTokensRequested = 0; // check each proposal in the slate is valid for (uint i = 0; i < numProposalsInSlate_; ) { + // @audit-info from _hasDuplicates() + for (uint j = i + 1; j < numProposalsInSlate_; ) { + if (proposalIds_[i] == proposalIds_[j]) revert InvalidProposalSlate(); + + unchecked { ++j; } + } + Proposal memory proposal = _standardFundingProposals[proposalIds_[i]]; // check if Proposal is in the topTenProposals list @@ -454,31 +458,6 @@ } /** - * @notice Check an array of proposalIds for duplicate IDs. - * @dev Only iterates through a maximum of 10 proposals that made it through the screening round. - * @dev Counters incremented in an unchecked block due to being bounded by array length. - * @param proposalIds_ Array of proposal Ids to check. - * @return Boolean indicating the presence of a duplicate. True if it has a duplicate; false if not. - */ - function _hasDuplicates( - uint256[] calldata proposalIds_ - ) internal pure returns (bool) { - uint256 numProposals = proposalIds_.length; - - for (uint i = 0; i < numProposals; ) { - for (uint j = i + 1; j < numProposals; ) { - if (proposalIds_[i] == proposalIds_[j]) return true; - - unchecked { ++j; } - } - - unchecked { ++i; } - - } - return false; - } - - /** * @notice Calculates the sum of funding votes allocated to a list of proposals. * @dev Only iterates through a maximum of 10 proposals that made it through the screening round. * @dev Counters incremented in an unchecked block due to being bounded by array length of at most 10.
The following gas report was benchmarked from the original codebase before applying any optimizations.
run: forge test --gas-report --match-contract ExtraordinaryFundingGrantFundTest --no-match-test testFuzzExtraordinaryFunding Running 10 tests for test/unit/ExtraordinaryFunding.t.sol:ExtraordinaryFundingGrantFundTest [PASS] testDrainTreasuryThroughExtraordinaryProposal() (gas: 2154984) [PASS] testExtraordinaryProposalFails() (gas: 2256152) [PASS] testGetMinimumThresholdPercentage() (gas: 7792) [PASS] testGetSliceOfNonTreasury() (gas: 15470) [PASS] testGetSliceOfTreasury() (gas: 9307) [PASS] testGetVotingPowerDelegateTokens() (gas: 446130) [PASS] testGetVotingPowerExtraordinary() (gas: 2107320) [PASS] testProposeAndExecuteExtraordinary() (gas: 3301681) [PASS] testProposeExtraordinary() (gas: 2117094) [PASS] testProposeExtraordinaryInvalid() (gas: 2020059) Test result: ok. 10 passed; 0 failed; finished in 1.17s | src/grants/GrantFund.sol:GrantFund contract | | | | | | |---------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | | 3913238 | 19542 | | | | | | Function Name | min | avg | median | max | # calls | | executeExtraordinary | 7604 | 31595 | 12339 | 94100 | 4 | | findMechanismOfProposal | 821 | 821 | 821 | 821 | 1 | | fundTreasury | 44726 | 44726 | 44726 | 44726 | 10 | | getExtraordinaryProposalInfo | 1029 | 1029 | 1029 | 1029 | 3 | | getExtraordinaryProposalSucceeded | 2290 | 2712 | 2788 | 3059 | 3 | | getMinimumThresholdPercentage | 493 | 776 | 493 | 2493 | 8 | | getSliceOfNonTreasury | 1592 | 4842 | 4842 | 8092 | 2 | | getSliceOfTreasury | 761 | 1761 | 1761 | 2761 | 2 | | getVotesExtraordinary | 810 | 7368 | 7694 | 8849 | 33 | | hasVotedExtraordinary | 717 | 1717 | 1717 | 2717 | 2 | | hashProposal | 3985 | 3985 | 3985 | 3985 | 5 | | proposeExtraordinary | 4960 | 55381 | 86947 | 86947 | 10 | | state | 3412 | 5127 | 4014 | 7412 | 8 | | treasury | 396 | 396 | 396 | 396 | 5 | | voteExtraordinary | 571 | 26705 | 29518 | 31565 | 30 |
run: forge test --gas-report --match-test testDistributionPeriodEndToEnd Running 1 test for test/unit/StandardFunding.t.sol:StandardFundingGrantFundTest [PASS] testDistributionPeriodEndToEnd() (gas: 5070756) Test result: ok. 1 passed; 0 failed; finished in 1.22s | src/grants/GrantFund.sol:GrantFund contract | | | | | | |---------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | | 3913238 | 19542 | | | | | | Function Name | min | avg | median | max | # calls | | claimDelegateReward | 1459 | 33591 | 36744 | 63575 | 9 | | executeStandard | 9220 | 23561 | 9811 | 48114 | 5 | | fundTreasury | 44726 | 44726 | 44726 | 44726 | 1 | | fundingVote | 1669 | 121127 | 105988 | 410840 | 10 | | getDelegateReward | 3071 | 3561 | 3071 | 5113 | 5 | | getDistributionId | 351 | 441 | 351 | 2351 | 22 | | getDistributionPeriodInfo | 1059 | 1366 | 1059 | 5059 | 13 | | getFundedProposalSlate | 1133 | 1309 | 1368 | 1368 | 4 | | getFundingPowerVotes | 15731 | 15731 | 15731 | 15731 | 1 | | getProposalInfo | 1032 | 1032 | 1032 | 1032 | 43 | | getSlateHash | 933 | 941 | 945 | 945 | 3 | | getTopTenProposals | 2340 | 2340 | 2340 | 2340 | 6 | | getVoterInfo | 1078 | 1078 | 1078 | 1078 | 5 | | getVotesFunding | 2467 | 2671 | 2671 | 2875 | 2 | | getVotesScreening | 5372 | 5372 | 5372 | 5372 | 11 | | hashProposal | 3985 | 3985 | 3985 | 3985 | 7 | | proposeStandard | 83311 | 83311 | 83311 | 83311 | 7 | | screeningVote | 1950 | 47128 | 58419 | 78738 | 11 | | startNewDistributionPeriod | 53223 | 53223 | 53223 | 53223 | 1 | | state | 2995 | 2995 | 2995 | 2995 | 2 | | updateSlate | 1002 | 27106 | 8874 | 78805 | 10 |
The following gas report was benchmarked after all optimizations have been applied.
run: forge test --gas-report --match-contract ExtraordinaryFundingGrantFundTest --no-match-test testFuzzExtraordinaryFunding Running 10 tests for test/unit/ExtraordinaryFunding.t.sol:ExtraordinaryFundingGrantFundTest [PASS] testDrainTreasuryThroughExtraordinaryProposal() (gas: 2039943) [PASS] testExtraordinaryProposalFails() (gas: 2116515) [PASS] testGetMinimumThresholdPercentage() (gas: 7792) [PASS] testGetSliceOfNonTreasury() (gas: 15470) [PASS] testGetSliceOfTreasury() (gas: 9307) [PASS] testGetVotingPowerDelegateTokens() (gas: 418278) [PASS] testGetVotingPowerExtraordinary() (gas: 1982610) [PASS] testProposeAndExecuteExtraordinary() (gas: 3080485) [PASS] testProposeExtraordinary() (gas: 1988234) [PASS] testProposeExtraordinaryInvalid() (gas: 1904445) Test result: ok. 10 passed; 0 failed; finished in 1.18s | src/grants/GrantFund.sol:GrantFund contract | | | | | | |---------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | | 3686548 | 18410 | | | | | | Function Name | min | avg | median | max | # calls | | executeExtraordinary | 5828 | 29790 | 10563 | 92207 | 4 | | findMechanismOfProposal | 799 | 799 | 799 | 799 | 1 | | fundTreasury | 44758 | 44758 | 44758 | 44758 | 10 | | getExtraordinaryProposalInfo | 1074 | 1074 | 1074 | 1074 | 3 | | getExtraordinaryProposalSucceeded | 2356 | 2778 | 2854 | 3125 | 3 | | getMinimumThresholdPercentage | 493 | 776 | 493 | 2493 | 8 | | getSliceOfNonTreasury | 1592 | 4842 | 4842 | 8092 | 2 | | getSliceOfTreasury | 761 | 1761 | 1761 | 2761 | 2 | | getVotesExtraordinary | 766 | 7367 | 7624 | 8779 | 33 | | hasVotedExtraordinary | 717 | 1717 | 1717 | 2717 | 2 | | hashProposal | 2256 | 2256 | 2256 | 2256 | 5 | | proposeExtraordinary | 3190 | 51113 | 81115 | 81115 | 10 | | state | 3478 | 5193 | 4080 | 7478 | 8 | | treasury | 352 | 352 | 352 | 352 | 5 | | voteExtraordinary | 636 | 26245 | 29000 | 31047 | 30 |
run: forge test --gas-report --match-test testDistributionPeriodEndToEnd [PASS] testDistributionPeriodEndToEnd() (gas: 4643634) Test result: ok. 1 passed; 0 failed; finished in 1.12s | src/grants/GrantFund.sol:GrantFund contract | | | | | | |---------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | | 3686548 | 18410 | | | | | | Function Name | min | avg | median | max | # calls | | claimDelegateReward | 917 | 33741 | 36740 | 63571 | 9 | | executeStandard | 7443 | 21737 | 8034 | 46220 | 5 | | fundTreasury | 44758 | 44758 | 44758 | 44758 | 1 | | fundingVote | 1647 | 120444 | 105412 | 407522 | 10 | | getDelegateReward | 3071 | 3561 | 3071 | 5113 | 5 | | getDistributionId | 440 | 440 | 440 | 440 | 21 | | getDistributionPeriodInfo | 1082 | 1389 | 1082 | 5082 | 13 | | getFundedProposalSlate | 1143 | 1319 | 1378 | 1378 | 4 | | getFundingPowerVotes | 15819 | 15819 | 15819 | 15819 | 1 | | getProposalInfo | 988 | 988 | 988 | 988 | 43 | | getSlateHash | 889 | 897 | 901 | 901 | 3 | | getTopTenProposals | 2330 | 2330 | 2330 | 2330 | 6 | | getVoterInfo | 1056 | 1056 | 1056 | 1056 | 5 | | getVotesFunding | 2423 | 2627 | 2627 | 2831 | 2 | | getVotesScreening | 5417 | 5417 | 5417 | 5417 | 11 | | hashProposal | 2256 | 2256 | 2256 | 2256 | 7 | | proposeStandard | 77480 | 77480 | 77480 | 77480 | 7 | | screeningVote | 1459 | 46065 | 57299 | 77619 | 11 | | startNewDistributionPeriod | 55140 | 55140 | 55140 | 55140 | 1 | | state | 3061 | 3061 | 3061 | 3061 | 2 | | updateSlate | 1002 | 23427 | 7240 | 73373 | 10 |
#0 - c4-judge
2023-05-17T10:45:35Z
Picodes marked the issue as grade-a