Platform: Code4rena
Start Date: 01/08/2022
Pot Size: $50,000 USDC
Total HM: 26
Participants: 133
Period: 5 days
Judge: Jack the Pug
Total Solo HM: 6
Id: 151
League: ETH
Rank: 70/133
Findings: 2
Award: $124.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Lambda
Also found by: 0x1f8b, 0x52, 0xA5DF, 0xNazgul, 0xNineDec, 0xSmartContract, 0xSolus, 0xf15ers, 0xkatana, 0xsolstars, 8olidity, Aymen0909, Bahurum, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, Extropy, Funen, GalloDaSballo, Guardian, IllIllI, JC, Jujic, MEP, Noah3o6, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, SooYa, Soosh, Throne6g, TomJ, Tomio, TrungOre, Waze, Yiko, _Adam, __141345__, a12jmx, ajtra, ak1, arcoun, asutorufos, ayeslick, benbaessler, berndartmueller, bin2chen, bobirichman, brgltd, bulej93, byndooa, c3phas, codexploder, cryptonue, cryptphi, defsec, delfin454000, dipp, djxploit, erictee, exd0tpy, fatherOfBlocks, gogo, hake, hansfriese, horsefacts, hyh, ignacio, indijanc, joestakey, kaden, mics, minhquanym, neumo, obront, oyc_109, p_crypt0, pfapostol, poirots, rbserver, robee, rokinot, rotcivegaf, sach1r0, saian, samruna, saneryee, scaraven, sikorico, simon135, sseefried, supernova
40.692 USDC - $40.69
In the checkPrecision()
function the natSpec comment reads:
/** * @dev Check if precision is greater than 1000, if so, it reverts
However the internal pure
function only returns true when the amount needed to be checked for precision is a multiple of 1000, or even when 0, i.e. must _amount muat be 0 or in 1000 increments.
* @param _amount amount needed to be checked for precision. */ function checkPrecision(uint256 _amount) internal pure { // Divide and multiply amount with 1000 should be equal to amount. // This ensures the amount is not too precise. require( ((_amount / 1000) * 1000) == _amount, "Project::Precision>=1000" ); }
For example, if _amount
was 1999it would return false, because
1999 / 1000 * 1000` equals 1000, and not 1999 (precision loss).
If _amount passed in parameter was 0, then 0 would be equal to _amount and the check would pass.
checkPrecision is called in:
allowing to store a 0 cost for tasks in both functions, in changeOrder for example it modifies a task's price created in addTasks:
// Store new cost for the task tasks[_taskID].cost = _newCost;
In changeOrder, it might trigger a revert in the following condition, if _newCost
is 0, which is allowed, due to an underflow occurring by subtracting from 0.
else if (totalLent - _totalAllocated >= _newCost - _taskCost) { // Increase the difference of new cost and old cost to total allocated. totalAllocated += _newCost - _taskCost; }
To minimize division errors, refactor your arithmetic to perform division last.
The output of an integer division operation is truncated to remove any non-zero remainder. Any arithmetic operations (other than addition and subtraction) that occur after a division operation will exacerbate this truncation error. To minimize this kind of error, refactor your arithmetic operations to perform division operations last. At a minimum, do not perform any multiplication or exponentiation after a division that may have non-zero truncation errors. Note however that ordering division last may increase the possibility of overflow. Example
In this example we compute (21/5)*7 in two different ways. Note that the true value is 29.4.
import "@openzeppelin/contracts/math/SafeMath.sol"; contract Test { using SafeMath for uint256; function bigError() public pure returns (uint256) { return uint256(21).div(5).mul(7); // Returns 28. Total error: 1.4. } function smallError() public pure returns (uint256) { return uint256(21).mul(7).div(5); // Returns 29. Total error: 0.4. } }
The bigError() function performs the division first. The resulting intermediate value of uint256(21).div(5) is 4, which has a truncation error of 0.2 (because the real value of 21/5 is 4.2). This error increases 7-fold with the proceeding multiplication, yielding a total error of 1.4. The smallError() function performs the division last, and results in a smaller error. This is the recommended approach.
Is the following line 891 in function checkSignatureValidity() intentional?
// delete from approvedHash delete approvedHashes[_address][_hash];
From the natspec comments there is nothing indicating that the hash should be deleted, nor it is implied by the function name checkSignatureValidity
.
/** * @dev Internal function for checking signature validity * @dev Checks if the signature is approved or recovered * @dev Reverts if not * @param _address address - address checked for validity * @param _hash bytes32 - hash for which the signature is recovered
The internal functions checkSignature
and checkSignatureTask
(in turn calling checkSignatureValidity
) can be called by the following external functions:
Whenever any of the above functions gets called (and if all required conditions are met) the hashes are deleted, these can be approved by anyone; but can they also be deleted by anyone via one of the external functions listed above?
/// @notice Returns mapping to keep track of all hashes (message or transaction) that have been approved by ANYONE function approvedHashes(address \_signer, bytes32 \_hash) external view returns (bool);
The full function is shown below, checkSignatureValidity. Notice the require statement _recoveredSignature == _address || approvedHashes[_address][_hash]
, it uses an ||
condition, not an &&
; from this we can conclude that even if approvedHashes[_address][_hash]
was used for access control reasons and deleted in order to avoid replay attacks, this condition would not even execute if the first part of ||
is true
. Since the signature and hash can be retrieved from transaction data, it can be considered public information.
function checkSignatureValidity( address _address, bytes32 _hash, bytes memory _signature, uint256 _signatureIndex ) internal { address _recoveredSignature = SignatureDecoder.recoverKey( _hash, _signature, _signatureIndex ); require( _recoveredSignature == _address || approvedHashes[_address][_hash], "Project::invalid signature" ); // delete from approvedHash delete approvedHashes[_address][_hash]; }
The above function is in turn called by another internal function:
function checkSignature(bytes calldata _data, bytes calldata _signature) internal { // Calculate hash from bytes bytes32 _hash = keccak256(_data); // When there is no contractor if (contractor == address(0)) { // Check for builder's signature checkSignatureValidity(builder, _hash, _signature, 0); } // When there is a contractor else { // When builder has delegated his rights to contractor if (contractorDelegated) { // Check contractor's signature checkSignatureValidity(contractor, _hash, _signature, 0); } // When builder has not delegated rights to contractor else { // Check for both B and GC signatures checkSignatureValidity(builder, _hash, _signature, 0); checkSignatureValidity(contractor, _hash, _signature, 1); } } }
builder
and contractor
are public addresses, therefore the security of this function relies on the secrecy of the original bytes calldata _data
, which would require an attacker to find a hash collision using an order with different parameters:
// Calculate hash from bytes bytes32 _hash = keccak256(_data);
The same line is implemented in the checkSignatureTask
function.
function checkSignatureTask( bytes calldata _data, bytes calldata _signature, uint256 _taskID ) internal { // Calculate hash from bytes bytes32 _hash = keccak256(_data); ...
Conventional MitM attacks such as XSS or clickjacking can be used in order to steal raw transaction bytes data.
🌟 Selected for report: c3phas
Also found by: 0x040, 0x1f8b, 0xA5DF, 0xNazgul, 0xSmartContract, 0xSolus, 0xc0ffEE, 0xkatana, 0xsam, 8olidity, Aymen0909, Bnke0x0, CertoraInc, Chinmay, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, Extropy, Fitraldys, Funen, GalloDaSballo, Guardian, IllIllI, JC, Lambda, MEP, Metatron, MiloTruck, Noah3o6, NoamYakov, PaludoX0, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, SooYa, TomJ, Tomio, Waze, _Adam, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, ballx, benbaessler, bharg4v, bobirichman, brgltd, cryptonue, defsec, delfin454000, dharma09, djxploit, durianSausage, eierina, erictee, fatherOfBlocks, gerdusx, gogo, hake, hyh, ignacio, jag, kaden, kyteg, lucacez, mics, minhquanym, oyc_109, pfapostol, rbserver, ret2basic, robee, rokinot, sach1r0, saian, samruna, scaraven, sikorico, simon135, supernova, teddav, tofunmi, zeesaw
21.7223 USDC - $21.72
Recommendation : Change storage to memory
Recommendation : Change code order
Line can be moved before line 184
Recommendation : Remove unnecessary variable
_community.memberCount
can be used directly in lines 198 and 199
Recommendation : Remove repeated require block
This syntax gets used four times so it could be a modifier in all instances except in the escrow()
:
require( _msgSender() == _communities[_communityID].owner, "Community::!Owner" );
Likewise on line 386it is not capitalised unlike the others
"Community::!owner"
Recommendation : Remove unecessary return declaration
type name of sender not necessary line 903
Recommendation : Remove unecessary asignment
Can be set by default to true instea of doing so explicitely
Recommendation : Shorten shorten ipfs (CID v1) hash to 32 bytes line 284
mintNFT function, parameter string memory _tokenURI can be shortened to create a bytes32 type, rather than a string that stores 128 bytes for IPFS CIDs version 0.