Platform: Code4rena
Start Date: 12/04/2023
Pot Size: $60,500 USDC
Total HM: 21
Participants: 199
Period: 7 days
Judge: hansfriese
Total Solo HM: 5
Id: 231
League: ETH
Rank: 84/199
Findings: 2
Award: $43.63
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: juancito
Also found by: 0xAgro, 0xNorman, 0xSmartContract, 0xStalin, 0xTheC0der, 0xWaitress, 0xhacksmithh, 0xnev, 3dgeville, 8olidity, Arz, Aymen0909, BGSecurity, BRONZEDISC, Bauchibred, Bauer, BenRai, ChainHunters, ChrisTina, CodeFoxInc, DedOhWale, DishWasher, EloiManuel, IceBear, Inspex, Jorgect, Kaysoft, LeoGold, LewisBroadhurst, Madalad, MiloTruck, MohammedRizwan, Nyx, Polaris_tow, RaymondFam, SaharDevep, SanketKogekar, Sathish9098, SolidityATL, Udsen, W0RR1O, aria, ayden, berlin-101, bin2chen, catellatech, codeslide, crc32, decade, descharre, evmboi32, eyexploit, fatherOfBlocks, georgits, giovannidisiena, joestakey, karanctf, kodyvim, ltyu, lukris02, m9800, matrix_0wl, mov, mrpathfindr, nadin, niser93, p0wd3r, parlayan_yildizlar_takimi, pavankv, pontifex, qpzm, ravikiranweb3, rbserver, santipu_, shealtielanz, slvDev, tnevler, wonjun, xmxanuel, yixxas
22.6007 USDC - $22.60
The check minters[_minter] != 0
is not necessary. if the minters
timestamp is set, it will be greater 0.
/** * Returns true if the address is an approved minter. */ function isMinter(address _minter) override public view returns (bool){ return minters[_minter] != 0 && block.timestamp >= minters[_minter]; }
There is a general lack of events in the codebase.
function registerPosition(address _position) override external { if (!isMinter(msg.sender)) revert NotMinter(); positions[_position] = msg.sender; + emit NewPosition(position, msg.sender); }
A standard pattern in Solidity is to define the storage variables and events at the beginning of the contract.
In Frankencoin some defintions are not at the beginning like
// Frankencoin Error `NotMinter` is defined between functions
A common pattern in the Frankencoin codebase is the following:
if (!logicalCondition) revert;
However, this if not
revert are more complicated to read then.
require(logicalCondition);
Example from Frankencoin.sol
modifier minterOnly() { if (!isMinter(msg.sender) && !isMinter(positions[msg.sender])) revert NotMinter(); _; }
Cleaner Way
modifier minterOnly() { require(isMinter(msg.sender) || isMinter(positions[msg.sender]), "NotMinter"); _; }
The revert pattern with custom error types is cheaper from the gas perspective but makes the logical conditions harder to understand.
#0 - c4-judge
2023-05-15T15:44:45Z
hansfriese marked the issue as grade-c
#1 - c4-judge
2023-05-18T06:10:34Z
hansfriese marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0xDACA, 0xRB, 0xSmartContract, 0xhacksmithh, 0xnev, Aymen0909, BenRai, Breeje, DishWasher, Erko, EvanW, JCN, MohammedRizwan, NoamYakov, Polaris_tow, Proxy, Rageur, Raihan, RaymondFam, ReyAdmirado, SAAJ, Sathish9098, Satyam_Sharma, Udsen, __141345__, aria, codeslide, decade, fatherOfBlocks, hunter_w3b, karanctf, matrix_0wl, nadin, naman1778, niser93, pavankv, petrichor, pfapostol, sebghatullah, slvDev, trysam2003, xmxanuel
21.0255 USDC - $21.03
Currently, a second loop is required to check if no duplicates exist. Since this would count voting power twice.
function votes(address sender, address[] calldata helpers) public view returns (uint256) { uint256 _votes = votes(sender); for (uint i=0; i<helpers.length; i++){ address current = helpers[i]; require(current != sender); require(canVoteFor(sender, current)); for (uint j=i+1; j<helpers.length; j++){ require(current != helpers[j]); // ensure helper unique } _votes += votes(current); } return _votes; }
However, this check can be performed in O(n)
without additional memory or storage. The contract only needs to require the helpers
address array to be sorted. This would easily allow to identify duplicates.
function checkDuplicatesAndSorted(address[] calldata helpers) external pure returns (bool ok) { if (helpers.length <= 1) { return true; } address prevAddress = helpers[0]; for (uint i = 1; i < helpers.length; i++) { if (helpers[i] <= prevAddress) { return false; } prevAddress = helpers[i]; } return true; }
In case the bid
transaction reverts the gas still needs to be paid by the user.
It would be more gas efficient to check the require
as first step in the bid function.
Together, with the the transferFrom
of ZHF
tokens.
A forgotten approval might be the most common revert reason.
function bid(uint256 _challengeNumber, uint256 _bidAmountZCHF, uint256 expectedSize) external { Challenge storage challenge = challenges[_challengeNumber]; if (block.timestamp >= challenge.end) revert TooLate(); if (expectedSize != challenge.size) revert UnexpectedSize(); + zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF); + if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge)); if (challenge.bid > 0) { zchf.transfer(challenge.bidder, challenge.bid); // return old bid } emit NewBid(_challengeNumber, _bidAmountZCHF, msg.sender); // ask position if the bid was high enough to avert the challenge if (challenge.position.tryAvertChallenge(challenge.size, _bidAmountZCHF)) { // bid was high enough, let bidder buy collateral from challenger zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF); challenge.position.collateral().transfer(msg.sender, challenge.size); emit ChallengeAverted(address(challenge.position), _challengeNumber); delete challenges[_challengeNumber]; } else { // challenge is not averted, update bid - if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge)); uint256 earliestEnd = block.timestamp + 30 minutes; if (earliestEnd >= challenge.end) { // bump remaining time like ebay does when last minute bids come in // An attacker trying to postpone the challenge forever must increase the bid by 0.5% // every 30 minutes, or double it every three days, making the attack hard to sustain // for a prolonged period of time. challenge.end = earliestEnd; } - zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF); challenge.bid = _bidAmountZCHF; challenge.bidder = msg.sender; } }
#0 - c4-pre-sort
2023-04-27T13:52:42Z
0xA5DF marked the issue as high quality report
#1 - 0xA5DF
2023-04-27T13:53:08Z
#1 is significant and pretty unique
Note: #822 also mentions this with calculation of gas saved
#2 - luziusmeisser
2023-04-30T00:29:16Z
1 is good. 2 does not work as the funds are transferred to different destinations depending on whether the challenge is averted or not.
#3 - c4-sponsor
2023-04-30T00:29:29Z
luziusmeisser marked the issue as sponsor confirmed
#4 - c4-judge
2023-05-16T07:52:41Z
hansfriese marked the issue as grade-b