Ajna Protocol - Kenshin's results

A peer to peer, oracleless, permissionless lending protocol with no governance, accepting both fungible and non fungible tokens as collateral.

General Information

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

Ajna Protocol

Findings Distribution

Researcher Performance

Rank: 23/114

Findings: 2

Award: $423.24

Gas:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Kenshin

Also found by: 0xRobocop, Dug, REACH, Ruhum, hyh, nobody2018, rbserver

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
upgraded by judge
H-04

Awards

243.4033 USDC - $243.40

External Links

Lines of code

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

Vulnerability details

Impact

Each period reserves a reward for granting up to 3% (GBC: Global Budget Constraint). The GBC is split into two parts:

  1. 90% for proposal granting. Any proposal requesting more than 90% will revert. The total amount requested across winning proposals must not exceed this percentage.
  2. 10% for voters who have participated in that distribution period as an incentive.

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:

  1. The treasury has a total of 1000 AJNA. 3% is reserved for this period, resulting in a GBC of 30 AJNA. The treasury is updated to 1000 - 30 = 970 AJNA.
  2. 90% is for proposals (27 AJNA) and 10% is for voters (3 AJNA).
  3. Assume all 27 AJNA are fully granted among winning proposals.
  4. Assume 10 voters in total, all fully voted and have equal voting power. Each voter receives 0.3 AJNA, totaling 3 AJNA.
  5. The treasury has spent 27 AJNA + 3 AJNA, leaving an actual balance of 970 AJNA.
  6. This round has ended and the treasury updates its balance before starting a new one using this logic. 970 += (30 - 27) = 973.
  7. The treasury accounts for 973 AJNA while having only 970 AJNA in actuality.

More detailed analysis

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.

Proof of Concept

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

Tools Used

  • Manual review
  • Foundry

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);      

Remark

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%.

Assessed type

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

Awards

179.8397 USDC - $179.84

Labels

bug
G (Gas Optimization)
grade-a
G-08

External Links

Gas Optimization Report

Summary

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.

ContractFunction NameBeforeAfterGas Savings
ExtraordinaryFundingexecuteExtraordinary31595297901805
ExtraordinaryFundinghashProposal398522561729
ExtraordinaryFundingproposeExtraordinary55381511134268
ExtraordinaryFundingvoteExtraordinary2670526245460
Subtotal:8262
StandardFundingclaimDelegateReward3359133741-150
StandardFundingexecuteStandard3561217371824
StandardFundingfundingVote121127120444683
StandardFundingproposeStandard83311774805831
StandardFundingscreeningVote47128460651063
StandardFundingupdateSlate27106234273679
Subtotal:12930
Total gas saved:21192

Optimization details

#TopicInstances
G-01Replace address[] targets_ with constant variable and remove unused uint[] values_4
G-02Unmodified external parameter can be declared as calldata1
G-03Remove unused event variable3
G-04Unneccessary revert check1
G-05Don't cache variable only used once2
G-06Reverts check should be located at the beginning of the function if possible1
G-07Delete old slate when replacing a new one will receive gas refund1
G-08Storage variables used more than once should be cached to local variables2
G-09Reading msg.sender is cheaper than caching it to a local address variable1
G-10Merge multiple loops that iterate through the same array1

G-01 Replace 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.executeExtraordinaryMinAvgMedianMax# calls
Before76043159512339941004
After58502981210585922284
Gas Savings1754178317541872-
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.proposeExtraordinaryMinAvgMedianMax# calls
Before496055381869478694710
After322051148811538115310
Gas Savings1740423357945794-
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.executeStandardMinAvgMedianMax# calls
Before9220235619811481145
After7451217458042462285
Gas Savings1769181617691886-
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.proposeStandardMinAvgMedianMax# calls
Before833118331183311833117
After775987759877598775987
Gas Savings5713571357135713-
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,

G-02 Unmodified external parameter can be declared as calldata

Number of instances: 1

For external variable that is for read-only purpose can be declared as calldata instead.

1. StandardFunding.screeningVoteMinAvgMedianMax# calls
Before195047128584197873811
After148146648579377825711
Gas Savings469480482481-
--- 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];

G-03 Remove unused event variable

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.voteExtraordinaryMinAvgMedianMax# calls
Before57126705295183156533
After57126203289613100833
Gas Savings0502557557-
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.fundingVoteMinAvgMedianMax# calls
Before166912112710598841084010
After166912046610543440754410
Gas Savings06615543296-
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.screeningVoteMinAvgMedianMax# calls
Before195047128584197873811
After195046624578647818411
Gas Savings0504555554-
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_
         );
     }

G-04 Unneccessary revert check

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.voteExtraordinaryMinAvgMedianMax# calls
Before57126705295183156530
After57126681294923153930
Gas Savings0242626-
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(

G-05 Don't cache variable only used once

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.startNewDistributionPeriodMinAvgMedianMax# calls
Before5222752321522275522350
After5221052304522105220650
Gas Savings1717173017-
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.updateSlateMinAvgMedianMax# calls
Before10022710688747880510
After10022710388707880010
Gas Savings0345-
--- 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();
             }

G-06 Reverts check should be located at the beginning of the function if possible

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.claimDelegateRewardMinAvgMedianMax# calls
Before14593359136744635759
After9613375836744635759
Gas Savings498-16800-
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

G-07 Delete old slate when replacing a new one will receive gas refund

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.updateSlateMinAvgMedianMax# calls
Before10022710688747880510
After10022430888747362110
Gas Savings0279805184-
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_,

G-08 Storage variables used more than once should be cached to local variables

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.proposeStandardMinAvgMedianMax# calls
Before833118331183311833117
After832218322183221832217
Gas Savings90909090-
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.updateSlateMinAvgMedianMax# calls
Before10022710688747660510
After10022622174877734310
Gas Savings08851387-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();

G-09 Reading msg.sender is cheaper than caching it to a local address variable

Number 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.screeningVoteMinAvgMedianMax# calls
Before195047128584197873811
After195047072583577887611
Gas Savings05662-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_,

G-10 Merge multiple loops that iterate through the same array

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.updateSlateMinAvgMedianMax# calls
Before10022710688747880510
After10022694086867861710
Gas Savings0166188188-
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.

Gas reports output

Before

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      |

After

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter