Ajna Protocol - K42'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: 101/114

Findings: 1

Award: $22.28

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

22.2767 USDC - $22.28

Labels

bug
G (Gas Optimization)
grade-b
G-28

External Links

Gas (or mana(as of new eip)) Optimization Report by K42

Possible Optimizations in GrantFund.sol

Scope = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/GrantFund.sol

Possible Optimization 1 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/GrantFund.sol#L58

-- You could create a state variable for the IERC20 instance in fundTreasury = IERC20 token = IERC20(ajnaTokenAddress); to avoid initializing it every time fundTreasury is called, like so IERC20 public immutable ajnaToken; constructor(address ajnaTokenAddress) { ajnaToken = IERC20(ajnaTokenAddress); } Then you can modify the function like so treasury += fundingAmount_; emit FundTreasury(fundingAmount_, treasury); ajnaToken.safeTransferFrom(msg.sender, address(this), fundingAmount_); This will some save gas.

Possible Optimizations in PositionManager.sol

Scope = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/PositionManager.sol

Possible Optimization 1 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/PositionManager.sol#LL45C1-L45C35 -- As you are already importing IERC20, change using SafeERC20 for ERC20; to using SafeERC20 for IERC20;, then change all instances of ERC20 with IERC20. This will import the minimal interface required for the contract, and therefore reduce contract size and lower gas costs.

Possible Optimization 2 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/PositionManager.sol#LL146C3-L146C3 -- You could use temporary variable for params_.tokenId in burn and you can consider implementing this change in other functions as it can avoid multiple SLOAD operations. This will save gas, as SLOAD is relatively expensive, do this like so uint256 tokenId = params_.tokenId; if (positionIndexes[tokenId].length() != 0) revert LiquidityNotRemoved(); delete nonces[tokenId]; delete poolKey[tokenId]; _burn(tokenId); emit Burn(msg.sender, tokenId);

Possible Optimization in RewardsManager.sol

Scope = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/RewardsManager.sol Possible Optimization 1 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-core/src/RewardsManager.sol#L426 -- You can reduce state variable reads by using local variables more, in multiple places, state variables like stakes,positionManager,ajnaPool, and others are read multiple times within the same function. Reading state variables consumes more gas than reading local variables. Therefore you can reduce gas usage by storing the state variable values, in a local variable and then use the local variable throughout the function use cases. For example = in the _calculateNextEpochRewards(), you can create local variables for stakes[tokenId_] and positionManager, like this StakeInfo storage stakeInfo = stakes[tokenId_]; IPositionManager positionMgr = positionManager; Then you can replace all instances of stakes[tokenId_] with stakeInfo and positionManager with positionMgr. This will further optimize the contract and save gas.

Possible Optimizations in Funding.sol

Scope = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/Funding.sol Possible Optimization 1 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/Funding.sol#L123

-- In _validateCallDatas you can use calldatas_[i].length instead of mload(add(selDataWithSig, 0x20)) Instead of using assembly to get the length of calldatas_[i], you can use calldatas_[i].length, this will save gas.

Possible Optimization 2 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/Funding.sol#L112 -- You can better optimize the for loop in _validateCallDatas, by incrementing i directly in the for loop, like so for (uint256 i = 0; i < targets_.length; i++) This will save you some gas.

Possible Optimizations in ExtraordinaryFunding.sol

Scope = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/ExtraordinaryFunding.sol Possible Optimization 1 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L246 -- In _getVotesExtraordinary(), you could remove if (proposalId_ == 0) revert ExtraordinaryFundingProposalInactive();, because the proposalId_ is never zero when the function is called from an external function, therefore the condition will never be true when the function is called from an external function, so you can remove the unnecessary check and save gas, like so function _getVotesExtraordinary(address account_, uint256 proposalId_) internal view returns (uint256 votes_) { uint256 startBlock = _extraordinaryFundingProposals[proposalId_].startBlock; votes_ = _getVotesAtSnapshotBlocks( account_, startBlock - VOTING_POWER_SNAPSHOT_DELAY, startBlock ); }

Possible Optimizations in StandardFunding.sol

Scope = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/StandardFunding.sol Possible Optimization 1 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/StandardFunding.sol#LL274C3-L293C6

-- In _getDelegateReward() Instead of dividing the result of Maths.wdiv() by 10, you can multiply the votingPowerAllocatedByDelegatee directly by 0.1 (as a WAD) to calculate the 10% of GBC distributed as per delegatee voting power allocated. This will save gas by reducing the number of arithmetic operations. Change like so function _getDelegateReward( QuarterlyDistribution memory currentDistribution_, QuadraticVoter memory voter_ ) internal pure returns (uint256 rewards_) { // calculate the total voting power available to the voter that was allocated in the funding stage uint256 votingPowerAllocatedByDelegatee = voter_.votingPower - voter_.remainingVotingPower; // if none of the voter's voting power was allocated, they receive no rewards if (votingPowerAllocatedByDelegatee == 0) return 0; // calculate reward // delegateeReward = 10 % of GBC distributed as per delegatee Voting power allocated rewards_ = Maths.wdiv( Maths.wmul( currentDistribution_.fundsAvailable, Maths.wmul(votingPowerAllocatedByDelegatee, 0.1 * 1e18) // Multiply by 0.1 as a WAD ), currentDistribution_.fundingVotePowerCast); }

Possible Optimization 2 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/StandardFunding.sol#LL300C5-L341C1

-- In updateSlate() Instead of doing currentSlateHash!= 0 && sum > _sumProposalFundingVotes(_fundedProposalSlates[currentSlateHash]) to check if the new slate is better, you can store the sum of the funded votes for the current slate directly, into the QuarterlyDistribution struct. This will save gas by avoiding the function call to _sumProposalFundingVotes(), and also you can use a local variable for existingSlate instead of accessing storage directly. This will save gas by reducing the number of SSTORE operations. Change like so function updateSlate(uint256[] calldata proposalIds_, uint24 distributionId_) external override returns (bool newTopSlate_) { QuarterlyDistribution storage currentDistribution = _distributions[distributionId_]; // store number of proposals for reduced gas cost of iterations uint256 numProposalsInSlate = proposalIds_.length; // check each proposal in the slate is valid, and get the sum of the proposals fundingVotesReceived uint256 sum = _validateSlate(distributionId_, currentDistribution.endBlock, currentDistribution.fundsAvailable, proposalIds_, numProposalsInSlate); // get pointers for comparing proposal slates bytes32 currentSlateHash = currentDistribution.fundedSlateHash; bytes32 newSlateHash = keccak256(abi.encode(proposalIds_)); // check if the slate of proposals is better than the existing slate, and is thus the new top slate newTopSlate_ = currentSlateHash == 0 || (currentSlateHash != 0 && sum > currentDistribution.fundedSlateVoteSum); // if the slate of proposals is a new top slate, update state if (newTopSlate_) { uint256[] storage existingSlate = _fundedProposalSlates[newSlateHash]; uint256[] memory localSlate = new uint256[](numProposalsInSlate); for (uint i = 0; i < numProposalsInSlate; ) { // update list of proposals to fund localSlate[i] = proposalIds_[i]; unchecked { ++i; } } existingSlate = localSlate; // update hash to point to the new leading slate of proposals and update the fundedSlateVoteSum currentDistribution.fundedSlateHash = newSlateHash; currentDistribution.fundedSlateVoteSum = sum; emit FundedSlateUpdated(distributionId_, newSlateHash); }}

Possible Optimization 3 = https://github.com/code-423n4/2023-05-ajna/blob/main/ajna-grants/src/grants/base/StandardFunding.sol#LL843C2-L853C6

-- In _sumSquareOfVotesCast(), you can remove the unchecked block since the function is already safe due to the limited length of votesCast_ array. The compiler will automatically handle the bounds check for you anyway, so removing unchecked { ++i; } can save you some gas. Other than that, looking optimized.

#0 - c4-judge

2023-05-17T12:27:03Z

Picodes marked the issue as grade-b

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