Ajna Protocol - DadeKuma'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: 61/114

Findings: 2

Award: $70.26

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

3 (High Risk)
partial-50
upgraded by judge
duplicate-488

Awards

34.0183 USDC - $34.02

External Links

Judge has assessed an item in Issue #467 as 2 risk. The relevant finding follows:

[L-03] Anyone can memorialize other users' position if the owner approves PositionManager There isn't a check to ensure that the caller is the actual owner of the position, so anyone can memorialize a position if the original owner approves PositionManager.

Modify ajna-core/tests/forge/unit/PositionManager.t.sol with the following and the test will not fail:

File: ajna-core\tests\forge\unit\PositionManager.t.sol

  • 176: vm.expectEmit(true, true, true, true);
  • 177: emit MemorializePosition(testAddress, tokenId, indexes);
  • 176: vm.stopPrank();
  • 177: vm.prank(address(123456)); //not owner 178: _positionManager.memorializePositions(memorializeParams); There is 1 instance of this issue.

File: ajna-grants/src/grants/base/StandardFunding.sol

170: /** 171: * @notice Get the block number at which this distribution period's challenge stage ends. 172: * @param endBlock_ The end block of quarterly distribution to get the challenge stage end block for. 173: * @return The block number at which this distribution period's challenge stage ends. 174: / 175: function getChallengeStageEndBlock( 176: uint256 endBlock 177: ) internal pure returns (uint256) { 178: return endBlock_ + CHALLENGE_PERIOD_LENGTH; 179: } 180: 181: /* 182: * @notice Get the block number at which this distribution period's screening stage ends. 183: * @param endBlock_ The end block of quarterly distribution to get the screening stage end block for. 184: * @return The block number at which this distribution period's screening stage ends. 185: / 186: function getScreeningStageEndBlock( 187: uint256 endBlock 188: ) internal pure returns (uint256) { 189: return endBlock_ - FUNDING_PERIOD_LENGTH; 190: } 191: 192: /* 193: * @notice Updates Treasury with surplus funds from distribution. 194: * @dev Counters incremented in an unchecked block due to being bounded by array length of at most 10. 195: * @param distributionId_ distribution Id of updating distribution 196: */ 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

ajna-grants/src/grants/base/StandardFunding.sol#L170-L216

#0 - c4-judge

2023-05-18T18:33:39Z

Picodes marked the issue as duplicate of #356

#1 - c4-judge

2023-05-30T21:47:16Z

Picodes marked the issue as duplicate of #488

#2 - c4-judge

2023-05-30T21:47:49Z

Picodes marked the issue as satisfactory

#3 - c4-judge

2023-05-30T21:47:53Z

Picodes marked the issue as partial-50

#4 - c4-judge

2023-05-30T21:48:18Z

Picodes changed the severity to 3 (High Risk)

Summary


Low Risk Issues

IdTitleInstances
[L-01]Revert by undeflow when extraordinary proposals exceeds the maximum amount1
[L-02]Wasteful/redundant operations when calculating the votes squared1
[L-03]Anyone can memorialize other users' position if the owner approves PositionManager1

Total: 3 instances over 3 issues.

Non Critical Issues

IdTitleInstances
[NC-01]newTopSlate_ condition can be simplified1

Total: 1 instances over 1 issues.

Low Risk Issues


[L-01] Revert by undeflow when extraordinary proposals exceeds the maximum amount

Add a custom revert when proposals exceed the maximum, as it's possible to send only 9 proposals, to avoid an underflow error. Check if _getMinimumThresholdPercentage() >= Maths.WAD, and revert if that's the case.

There is 1 instance of this issue.

File: ajna-grants/src/grants/base/ExtraordinaryFunding.sol

105: 		        if (uint256(totalTokensRequested) > _getSliceOfTreasury(Maths.WAD - _getMinimumThresholdPercentage())) revert InvalidProposal();

ajna-grants/src/grants/base/ExtraordinaryFunding.sol#L105


[L-02] Wasteful/redundant operations when calculating the votes squared

The power of n with an exponent of 2 is always positive, regardless of n (it can be either positive or negative), so calculating the absolute value is wasteful (Maths.abs)

There is 1 instance of this issue.

File: ajna-grants/src/grants/base/StandardFunding.sol

849: 		            votesCastSumSquared_ += Maths.wpow(SafeCast.toUint256(Maths.abs(votesCast_[i].votesUsed)), 2);

ajna-grants/src/grants/base/StandardFunding.sol#L849


[L-03] Anyone can memorialize other users' position if the owner approves PositionManager

There isn't a check to ensure that the caller is the actual owner of the position, so anyone can memorialize a position if the original owner approves PositionManager.

Modify ajna-core/tests/forge/unit/PositionManager.t.sol with the following and the test will not fail:

File: ajna-core\tests\forge\unit\PositionManager.t.sol

-   176:         vm.expectEmit(true, true, true, true);
-   177:         emit MemorializePosition(testAddress, tokenId, indexes);
+   176:         vm.stopPrank();
+   177:         vm.prank(address(123456)); //not owner
    178:         _positionManager.memorializePositions(memorializeParams);

There is 1 instance of this issue.

File: ajna-grants/src/grants/base/StandardFunding.sol

170: 		    /**
171: 		     * @notice Get the block number at which this distribution period's challenge stage ends.
172: 		     * @param  endBlock_ The end block of quarterly distribution to get the challenge stage end block for.
173: 		     * @return The block number at which this distribution period's challenge stage ends.
174: 		    */
175: 		    function _getChallengeStageEndBlock(
176: 		        uint256 endBlock_
177: 		    ) internal pure returns (uint256) {
178: 		        return endBlock_ + CHALLENGE_PERIOD_LENGTH;
179: 		    }
180: 		
181: 		    /**
182: 		     * @notice Get the block number at which this distribution period's screening stage ends.
183: 		     * @param  endBlock_ The end block of quarterly distribution to get the screening stage end block for.
184: 		     * @return The block number at which this distribution period's screening stage ends.
185: 		    */
186: 		    function _getScreeningStageEndBlock(
187: 		        uint256 endBlock_
188: 		    ) internal pure returns (uint256) {
189: 		        return endBlock_ - FUNDING_PERIOD_LENGTH;
190: 		    }
191: 		
192: 		    /**
193: 		     * @notice Updates Treasury with surplus funds from distribution.
194: 		     * @dev    Counters incremented in an unchecked block due to being bounded by array length of at most 10.
195: 		     * @param distributionId_ distribution Id of updating distribution 
196: 		     */
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

ajna-grants/src/grants/base/StandardFunding.sol#L170-L216


Non Critical Issues


[NC-01] newTopSlate_ condition can be simplified

currentSlateHash!= 0 can be safely removed in the second part of the OR condition, as the condition would short circuit on the first expression (currentSlateHash == 0) if that isn't the case.

There is 1 instance of this issue.

File: ajna-grants/src/grants/base/StandardFunding.sol

317: 		        newTopSlate_ = currentSlateHash == 0 ||
318: 		            (currentSlateHash!= 0 && sum > _sumProposalFundingVotes(_fundedProposalSlates[currentSlateHash]));

ajna-grants/src/grants/base/StandardFunding.sol#L317-L318


#0 - DadeKuma

2023-05-11T21:25:05Z

Whops, I copied the wrong code for L-03, this is the correct one:

https://github.com/code-423n4/2023-05-ajna/blob/a51de1f0119a8175a5656a2ff9d48bbbcb4436e7/ajna-core/src/PositionManager.sol#L170-L216

File: ajna-core/src/PositionManager.sol

170:     function memorializePositions(
171:         MemorializePositionsParams calldata params_
172:     ) external override {
173:         EnumerableSet.UintSet storage positionIndex = positionIndexes[params_.tokenId];
174: 
175:         IPool   pool  = IPool(poolKey[params_.tokenId]);
176:         address owner = ownerOf(params_.tokenId);
177: 
178:         uint256 indexesLength = params_.indexes.length;
179:         uint256 index;
180: 
181:         for (uint256 i = 0; i < indexesLength; ) {
182:             index = params_.indexes[i];
183: 
184:             // record bucket index at which a position has added liquidity
185:             // slither-disable-next-line unused-return
186:             positionIndex.add(index);
187: 
188:             (uint256 lpBalance, uint256 depositTime) = pool.lenderInfo(index, owner);
189: 
190:             Position memory position = positions[params_.tokenId][index];
191: 
192:             // check for previous deposits
193:             if (position.depositTime != 0) {
194:                 // check that bucket didn't go bankrupt after prior memorialization
195:                 if (_bucketBankruptAfterDeposit(pool, index, position.depositTime)) {
196:                     // if bucket did go bankrupt, zero out the LP tracked by position manager
197:                     position.lps = 0;
198:                 }
199:             }
200: 
201:             // update token position LP
202:             position.lps += lpBalance;
203:             // set token's position deposit time to the original lender's deposit time
204:             position.depositTime = depositTime;
205: 
206:             // save position in storage
207:             positions[params_.tokenId][index] = position;
208: 
209:             unchecked { ++i; }
210:         }
211: 
212:         // update pool LP accounting and transfer ownership of LP to PositionManager contract
213:         pool.transferLP(owner, address(this), params_.indexes);
214: 
215:         emit MemorializePosition(owner, params_.tokenId, params_.indexes);
216:     }

#1 - c4-judge

2023-05-18T18:33:12Z

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