Platform: Code4rena
Start Date: 03/05/2023
Pot Size: $60,500 USDC
Total HM: 25
Participants: 114
Period: 8 days
Judge: Picodes
Total Solo HM: 6
Id: 234
League: ETH
Rank: 66/114
Findings: 2
Award: $58.52
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: rbserver
Also found by: 0xnev, ABAIKUNANBAEV, Audit_Avengers, Aymen0909, BGSecurity, BRONZEDISC, Bason, DadeKuma, GG_Security, Jerry0x, Jorgect, MohammedRizwan, REACH, Sathish9098, Shogoki, T1MOH, UniversalCrypto, aviggiano, ayden, berlin-101, bytes032, codeslide, descharre, fatherOfBlocks, hals, kaveyjoe, kodyvim, lfzkoala, lukris02, nadin, naman1778, patitonar, pontifex, sakshamguruji, squeaky_cactus, teawaterwire, wonjun, yjrwkk
36.2377 USDC - $36.24
ID | Finding | Instances |
---|---|---|
L-01 | Treasury can be wrong if funded proposal is not executed | 1 |
L-02 | Proposal parameters are not correctly checked | 1 |
ID | Finding | Instances |
---|---|---|
N-01 | Use a more recent version of solidity | 1 |
N-02 | startNewDistributionPeriod() will likely be called everytime before challenge period ends | 1 |
N-03 | Typos | 3 |
N-04 | Require or if statements that check input arguments should be at the top of the function | 1 |
N-05 | Unnecessary code | 1 |
N-06 | Don't use msg.sender as a parameter for an internal function that's only used once | 2 |
N-07 | Remove block.number from events | 2 |
The function _updateTreasury()
doesn't check if a proposal is executed. It updates it's amount by all the tokens requested from the distributions slate. However when a proposal is gonna be executed it's still possible to revert for various reasons. For example the contract that the proposal want to use reverts on
_beforeTokenTransfer. Or the contract is paused and doesn't allow any tokens to be sent to the contract.
When this proposal will not be executed in the future, the treasury will be forever wrong. Resulting in more tokens in the treasury than the treasury
variable is.
A mitigation for this is to check if a proposal is executed and then decrement the treasury with the tokens requested from said proposal.
The function _validateCallDatas()
checks the parameters of a new proposal.
The line below checks if the target is equal to the address of the ajna token, or if the value is 0. However the target should always be the ajna token and a transfer of value 0 will be pretty stupid. It should be check if both are true or else revert.
- if (targets_[i] != ajnaTokenAddress || values_[i] != 0) revert InvalidProposal(); + if (targets_[i] != ajnaTokenAddress && values_[i] != 0) revert InvalidProposal();
The contracts in ajna-core use solidity version 0.8.14. Later release versions have new features and bug fixes. Consider using a later version.
startNewDistributionPeriod()
will likely be called everytime before challenge period endsThe challenge period is a period of 7 days after the funding stage has ended. However the startNewDistributionPeriod()
can be called by everyone immeditaly after the funding stage has ended. This means the function most likely will not be called after the challenge stage has ended and the first if statement will never be true. This isn't a big problem because it will just be called the next time the distribution gets incremented, so the first if statement to update the treasury can be removed.
StandardFunding.sol L127: { // Check if any last distribution exists and its challenge stage is over - if (currentDistributionId > 0 && (block.number > _getChallengeStageEndBlock(currentDistributionEndBlock))) { - // Add unused funds from last distribution to treasury - _updateTreasury(currentDistributionId); - } // checks if any second last distribution exist and its unused funds are not added into treasury if (currentDistributionId > 1 && !_isSurplusFundsUpdated[currentDistributionId - 1]) { // Add unused funds from second last distribution to treasury _updateTreasury(currentDistributionId - 1); } }
// readd non distributed tokens to the treasury
: readd// check that the voter hasn't already voted on a proposal by seeing if it's already in the votesCast array
: should be this proposalfunction reedemPositions(
: should be redeemStandardFunding.sol#L423-L425: check for challenge period should be in the updateSlate()
function, this also saves an extra function parameter.
StandardFunding.sol#L317-L318:
newTopSlate_ = currentSlateHash == 0 || (currentSlateHash!= 0 && sum > _sumProposalFundingVotes(_fundedProposalSlates[currentSlateHash]));
The statement currentSlateHash!= 0
will always be true when currentSlateHash == 0
is not true. Hereby currentSlateHash!= 0
can be removed.
When an internal function is only used once, it's unnecessary to pass msg.sender as parameter because msg.sender is a global variable and it will always be the same. This counts for _fundingVote()
and _screeningVote
block.number is added to an events information by default, hence why it can be removed from the emit statement.
IFunding: L54: event ProposalCreated( uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, - uint256 startBlock, uint256 endBlock, string description ); StandardFunding: L393: emit ProposalCreated( proposalId_, msg.sender, targets_, values_, new string[](targets_.length), calldatas_, - block.number, currentDistribution.endBlock, description_ );
block.number can also be removed at:
#0 - c4-judge
2023-05-18T18:38:31Z
Picodes marked the issue as grade-b
๐ Selected for report: JCN
Also found by: 0x73696d616f, 0xSmartContract, 0xnev, Audit_Avengers, Aymen0909, Blckhv, Eurovickk, K42, Kenshin, Rageur, Raihan, ReyAdmirado, SAAJ, SAQ, Shubham, Tomio, Walter, ayden, codeslide, descharre, dicethedev, hunter_w3b, j4ld1na, kaveyjoe, okolicodes, patitonar, petrichor, pontifex, yongskiws
22.2767 USDC - $22.28
ID | Finding | Gas saved | Instances |
---|---|---|---|
G-01 | Do budget check at the end of the for loop | - | 1 |
G-02 | Use constants instead of type(uintx).max | 20 | 1 |
G-03 | Remove block.number from events | 20 | 2 |
G-04 | Use double if statement instead of && | 30 | 1 |
G-05 | Make up to 3 fields in an event indexed | 100 | - |
The if statement to check if the slate has exceeded the budget in _validateSlate()
is now in every iteration. Gas can be saved when you put it at the end of the for loop.
StandardFunding.sol#L421-L454
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(); // account for fundingVotesReceived possibly being negative if (proposal.fundingVotesReceived < 0) revert InvalidProposalSlate(); // update counters sum_ += uint128(proposal.fundingVotesReceived); // since we are converting from int128 to uint128, we can safely assume that the value will not overflow totalTokensRequested += proposal.tokensRequested; // check if slate of proposals exceeded budget constraint ( 90% of GBC ) - if (totalTokensRequested > (gbc * 9 / 10)) { - revert InvalidProposalSlate(); - } unchecked { ++i; } } + if (totalTokensRequested > (gbc * 9 / 10)) { + revert InvalidProposalSlate(); + }
It's more gas efficiรซnt to use a constant instead of a max from a type. The constant won't be pretty if it's a high value but it will save around 20 gas everytime the function gets called.
if (sumOfTheSquareOfVotesCast > type(uint128).max) revert InsufficientVotingPower();
block.number is added by default to event information. Around 300 gas can be saved if you remove block.number when emitting events.
IFunding: L54: event ProposalCreated( uint256 proposalId, address proposer, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, - uint256 startBlock, uint256 endBlock, string description ); StandardFunding: L393: emit ProposalCreated( proposalId_, msg.sender, targets_, values_, new string[](targets_.length), calldatas_, - block.number, currentDistribution.endBlock, description_ );
Same optimization can be done at:
When an if or require statement uses &&, it's more gas efficiรซnt to have two if/require statements
if (currentDistributionId > 0 && (block.number > _getChallengeStageEndBlock(currentDistributionEndBlock))) { // Add unused funds from last distribution to treasury _updateTreasury(currentDistributionId); }
Can be changed to:
if (currentDistributionId > 0 ) { if((block.number > _getChallengeStageEndBlock(currentDistributionEndBlock))){ // Add unused funds from last distribution to treasury _updateTreasury(currentDistributionId); } }
Same optimization can be made at:
An event can have up to 3 fields indexed. For each fields that's indexed, around 100 gas can be saved when emitting that event. So for 3 fields extra, this means 300 gas saved. This omptimization can be done at almost every event in the protocol.
#0 - c4-judge
2023-05-17T10:51:08Z
Picodes marked the issue as grade-b