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: 37/133
Findings: 2
Award: $170.13
🌟 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
42.5475 USDC - $42.55
 | Issue |
---|---|
1 | Zero-check is not performed for address |
2 | Anyone can withdraw funds for Builder on his behalf (without permission) |
3 | Value of APR isnt checked to be within range |
In the HomeFi.sol
, general address changes are performed only after a zero check. Which is done with the nonZero modifier. But in the setTrustedForwarder function, the new address, _newForwarder
is not checked for zero value.
Mitigation would be to add the nonZero modifier in the function as shown below:
function setTrustedForwarder(address _newForwarder) external override onlyAdmin nonZero(_newForwarder) //mitigation applied noChange(trustedForwarder, _newForwarder) { trustedForwarder = _newForwarder; }
In Project.sol
, the recoverTokens function, sends remaining funds to the builder. But this can be initiated by anyone on his behalf without prior permission. This can be confusing from the Builder's perspective.
Mitigation would be to check if the msg.sender is the builder himself.
In Community
contract, the value of APR
isnt checked to be within a certain range in the publishProject function. This can lead to a large interest to be calculated on the debt amount.
Mitigation would be to check if APR is within a particular range before publishing the project. At least a maximum value should be specified.
 | Issue |
---|---|
1 | Variable name should indicate what it represents |
2 | Use a newer version of Solidity |
In Project.sol
, in the raiseDispute function, the input data
is decoded as shown below:
(address _project, uint256 _task, , , ) = abi.decode( // @audit QA could be renamed to taskID _data, (address, uint256, uint8, bytes, bytes) );
The variable _task
here represents the taskID. And it should be named as such for better readability. In other parts of the code its done well.
The codebase uses Solidity version 0.8.6 which was released in June 2021. Though its not possible to keep up with the version changes of Solidity, its advisable to use a relatively newer version. The current Solidity version is 0.8.15 which was released in June 2022 (one year later than the current version used by the codebase).
Newer versions like 0.8.10 will skip contract existence checks for external calls, if the external call has a return value. This will reduce gas costs.
#0 - jack-the-pug
2022-08-28T03:01:38Z
Re L-01: Zero-check is not performed for address
What if we need to remove the trustedForwarder
?
🌟 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
127.5757 USDC - $127.58
 | Contract | Method | Original Avg Gas Cost | Optimized Avg Gas Cost | Avg Gas Saved |
---|---|---|---|---|---|
1 | HomeFi | createProject | 339543 | 339198 | 345 |
2 | Community | createCommunity | 176854 | 176660 | 194 |
3 | Community | lendToProject | 295225 | 294412 | 813 |
4 | Project | allocateFunds | 63493 | 63200 | 293 |
5 | Project | inviteContractor | 69560 | 69458 | 102 |
Reduction In Deployment Costs:
 | Contract | Original Cost | Optimized Cost | Gas Saved |
---|---|---|---|---|
1 | HomeFi | 2230658 | 2226554 | 4104 |
2 | Community | 3425844 | 3370700 | 55144 |
3 | Project | 3141495 | 3138883 | 2612 |
projectCount
was read 3 times. By using a cached copy, we could save at least 200 gas.//Original Code: // Update project related mappings projects[projectCount] = _project; projectTokenId[_project] = projectCount; emit ProjectAdded(projectCount, _project, _sender, _currency, _hash); //Can be optimized into: uint256 projectCountCached = projectCount; // cache the state variable. projects[projectCountCached] = _project; projectTokenId[_project] = projectCountCached;
createProject
calls an internal function called mintNFT, which again uses projectCount
5 times. This could be optimized as follows as well to save around 400 gas.//Original Code: projectCount += 1; // Mints NFT and set token URI _mint(_to, projectCount); _setTokenURI(projectCount, _tokenURI); emit NftCreated(projectCount, _to); return projectCount; //Can be optimized into: uint256 projectCountCached = ++projectCount; // @audit gas saving // Mints NFT and set token URI _mint(_to, projectCountCached); _setTokenURI(projectCountCached, _tokenURI); emit NftCreated(projectCountCached, _to); return projectCountCached;
Comparison of Hardhat gas reports, before and after the above changes, showed an average gas saving of 345 gas.
communityCount
was read 3 times. By using a cached copy, we could save at least 200 gas.//Original Code: communityCount++; CommunityStruct storage _community = _communities[communityCount]; emit CommunityAdded(communityCount, _sender, _currency, _hash); //Can be optimized into: uint256 communityCountCached = ++communityCount; // cache the state variable. CommunityStruct storage _community = _communities[communityCountCached]; emit CommunityAdded(communityCountCached, _sender, _currency, _hash);
Comparison of Hardhat gas reports, before and after the above changes, showed a gas saving of 194 gas.
projectDetails
from the community mapping 6 times. If a value inside a mapping/array is accessed more than once, gas can be saved by caching the mapping's value in local storage. The gas saving comes from not needing to recalculate the key's keccak256.See the mitigation below:
//Cache in local storage to avoid hash calculations ProjectDetails storage _communityProject = _communities[_communityID].projectDetails[ _project]; //The following lines are modified to use the above variable. require( // Line 400 _amountToProject <= _communityProject .lendingNeeded - _communityProject .totalLent, "Community::lending>needed" ); // Update total lent by lender _communityProject // Line 421 .totalLent += _amountToProject; // First claim interest if principal lent > 0 _communityProject.lentAmount > 0 // Line 427 // Increment lent principal _communityProject // Line 433 .lentAmount += _lendingAmount; // Update lastTimestamp _communityProject // Line 438 .lastTimestamp = block.timestamp;
Comparison of Hardhat gas reports, before and after the above changes, showed an average gas saving of 813 gas.
_changeOrderedTask
is read multiple times. This can be cached to save gas.
b. The state variable taskCount
was being read inside a for loop
. This should be cached.See the mitigation below and affected lines:
//Mitigation: uint256 len = _changeOrderedTask.length; // cache the length of state variable uint256 taskCountCached = taskCount; // cache the state variable //The following lines change as follows: taskCount - j + len - i //Line 592 if (len > 0) { //Line 601 for (; i < len; i++) { //Line 603 if (i == len) { //Line 635 if (j < taskCountCached) { //Line 648 for (++j; j <= taskCountCached; j++) { //Line 650 if (j > taskCountCached) { //Line 681 lastAllocatedTask = taskCountCached; //Line 682
Comparison of Hardhat gas reports, before and after the above changes, showed an average gas saving of 293 gas.
In the inviteContractor function, the emit statement is emitting a state variable called contractor
, which was just assigned from a memory variable called _contractor
. The following mitigation will save around 100 gas.
// change emit ContractorInvited(contractor); // to emit ContractorInvited(_contractor);
Comparison of Hardhat gas reports, before and after the above changes, showed an average gas saving of 102 gas.
Another major area in which we could save deployment cost would be in converting the revert
strings into custom errors. If the function does revert, you can also save on dynamic gas cost. See this example implementation to understand the dynamics of custom errors. It shows that each require
string converted into a custom error saves you around 11000 gas.
To make sure that it is indeed the case, I changed all the require error messages
in the Disputes.sol file into custom errors.
There were 6 of them. And it reduced the deployment cost from 1,340,217 to 1,284,195 ( a total saving 56,022 gas). So we can give it a conservative estimate of 9000 saved gas per custom error.
See below how I changed the existing errors into Custom errors. You can see that, custom errors doesn't always look ugly as many developers feel.
error Disputes_NotResolvable(); error Disputes_NotActionType(); error Disputes_NotMember(); error Disputes_0address(); error Disputes_NotAdmin(); error Disputes_NotProject(); require(_address != address(0), "Disputes::0 address"); //became... if(_address == address(0)) revert Disputes_0address(); require(homeFi.admin() == _msgSender(), "Disputes::!Admin"); //became... if(homeFi.admin() != _msgSender()) revert Disputes_NotAdmin(); require(homeFi.isProjectExist(_msgSender()), "Disputes::!Project"); //became... if(!homeFi.isProjectExist(_msgSender())) revert Disputes_NotProject(); require( _disputeID < disputeCount && disputes[_disputeID].status == Status.Active, "Disputes::!Resolvable" ); //became... if(!(_disputeID < disputeCount && disputes[_disputeID].status == Status.Active)) revert Disputes_NotResolvable(); require( _actionType > 0 && _actionType <= uint8(ActionType.TaskPay), "Disputes::!ActionType" ); //became... if(!(_actionType > 0 && _actionType <= uint8(ActionType.TaskPay))) revert Disputes_NotActionType(); require(_result, "Disputes::!Member"); //became... if(!_result) revert Disputes_NotMember();
Listing out all the require strings which could be converted into custom errors:
File: contracts/Disputes.sol --------------------------------- Line 039: require(_address != address(0), "Disputes::0 address"); Line 046: require(homeFi.admin() == _msgSender(), "Disputes::!Admin"); Line 052: require(homeFi.isProjectExist(_msgSender()), "Disputes::!Project"); Line 061: require( _disputeID < disputeCount && disputes[_disputeID].status == Status.Active, "Disputes::!Resolvable" ); Line 106: require( _actionType > 0 && _actionType <= uint8(ActionType.TaskPay), "Disputes::!ActionType" ); Line 183: require(_result, "Disputes::!Member"); File: contracts/Project.sol --------------------------------- Line 123: require(!contractorConfirmed, "Project::GC accepted"); Line 132: require(_projectAddress == address(this), "Project::!projectAddress"); Line 135: require(_contractor != address(0), "Project::0 address"); Line 150: require(_msgSender() == builder, "Project::!B"); Line 153: require(contractor != address(0), "Project::0 address"); Line 176: require(_nonce == hashChangeNonce, "Project::!Nonce"); Line 189: require( _sender == builder || _sender == homeFi.communityContract(), "Project::!Builder&&!Community" ); Line 195: require(_cost > 0, "Project::!value>0"); Line 199: require( projectCost() >= uint256(_newTotalLent), "Project::value>required" ); Line 238: require(_taskCount == taskCount, "Project::!taskCount"); Line 241: require(_projectAddress == address(this), "Project::!projectAddress"); Line 245: require(_length == _taskCosts.length, "Project::Lengths !match"); Line 277: require(_nonce == hashChangeNonce, "Project::!Nonce"); Line 301: require( _msgSender() == builder || _msgSender() == contractor, "Project::!Builder||!GC" ); Line 308: require(_length == _scList.length, "Project::Lengths !match"); Line 341: require(_projectAddress == address(this), "Project::!Project"); Line 369: require(tasks[_taskID].getState() == 3, "Project::!Complete"); Line 406: require(_project == address(this), "Project::!projectAddress"); Line 511: require(_project == address(this), "Project::!projectAddress"); Line 515: require( signer == builder || signer == contractor, "Project::!(GC||Builder)" ); Line 521: require( signer == builder || signer == contractor || signer == tasks[_task].subcontractor, "Project::!(GC||Builder||SC)" ); Line 530: require(getAlerts(_task)[2], "Project::!SCConfirmed"); Line 753: require(_sc != address(0), "Project::0 address"); Line 886: require( _recoveredSignature == _address || approvedHashes[_address][_hash], "Project::invalid signature" ); Line 906: require( ((_amount / 1000) * 1000) == _amount, "Project::Precision>=1000" ); File: contracts/Community.sol ----------------------------------- Line 069: require(_address != address(0), "Community::0 address"); Line 075: require(_msgSender() == homeFi.admin(), "Community::!admin"); Line 081: require( projectPublished[_project] == _communityID, "Community::!published" ); Line 090: require( _msgSender() == IProject(_project).builder(), "Community::!Builder" ); Line 131: require( !restrictedToAdmin || _sender == homeFi.admin(), "Community::!admin" ); Line 159: require( _communities[_communityID].owner == _msgSender(), "Community::!owner" ); Line 191: require( !_community.isMember[_newMemberAddr], "Community::Member Exists" ); Line 235: require( _publishNonce == _community.publishNonce, "Community::invalid publishNonce" ); Line 241: require(homeFi.isProjectExist(_project), "Community::Project !Exists"); Line 248: require(_community.isMember[_builder], "Community::!Member"); Line 251: require( _projectInstance.currency() == _community.currency, "Community::!Currency" ); Line 312: require( !_communityProject.publishFeePaid, "Community::publish fee paid" ); Line 347: require( _communityProject.publishFeePaid, "Community::publish fee !paid" ); Line 353: require( _lendingNeeded >= _communityProject.totalLent && _lendingNeeded <= IProject(_project).projectCost(), "Community::invalid lending" ); Line 384: require( _sender == _communities[_communityID].owner, "Community::!owner" ); Line 400: require( _amountToProject <= _communities[_communityID] .projectDetails[_project] .lendingNeeded - _communities[_communityID] .projectDetails[_project] .totalLent, "Community::lending>needed" ); Line 491: require( _msgSender() == _communities[_communityID].owner, "Community::!Owner" ); Line 536: require(_builder == _projectInstance.builder(), "Community::!Builder"); Line 539: require( _lender == _communities[_communityID].owner, "Community::!Owner" ); Line 557: require(!restrictedToAdmin, "Community::restricted"); Line 568: require(restrictedToAdmin, "Community::!restricted"); Line 764: require(_repayAmount > 0, "Community::!repay"); Line 792: require(_lentAndInterest >= _repayAmount, "Community::!Liquid"); Line 886: require( _recoveredSignature == _address || approvedHashes[_address][_hash], "Community::invalid signature" ); File: contracts/HomeFi.sol -------------------------------- Line 073: require(admin == _msgSender(), "HomeFi::!Admin"); Line 078: require(_address != address(0), "HomeFi::0 address"); Line 084: require(_oldAddress != _newAddress, "HomeFi::!Change"); Line 142: require(!addrSet, "HomeFi::Set"); Line 191: require(lenderFee != _newLenderFee, "HomeFi::!Change"); Line 255: require( _currency == tokenCurrency1 || _currency == tokenCurrency2 || _currency == tokenCurrency3, "HomeFi::!Currency" ); File: contracts/ProjectFactory.sol --------------------------------------- Line 036: require(_address != address(0), "PF::0 address"); Line 064: require( _msgSender() == IHomeFi(homeFi).admin(), "ProjectFactory::!Owner" ); Line 084: require(_msgSender() == homeFi, "PF::!HomeFiContract"); File: contracts/DebtToken.sol ----------------------------------- Line 031: require( communityContract == _msgSender(), "DebtToken::!CommunityContract" ); Line 050: require(_communityContract != address(0), "DebtToken::0 address"); File: contracts/HomeFiProxy.sol ------------------------------------ Line 041: require(_address != address(0), "Proxy::0 address"); Line 081: require(_length == _implementations.length, "Proxy::Lengths !match"); Line 105: require( contractAddress[_contractName] == address(0), "Proxy::Name !OK" ); Line 133: require(_length == _contractAddresses.length, "Proxy::Lengths !match");
There are a total of 6 + 25 + 24 + 6 + 3 + 2 + 4 = 70 require
strings spread out in 7 files.
Converting all of them into custom errors can save us around 70 * 9000 = 630,000 gas when deploying.
The above changes reduced the deployment cost by 691,860. And Dynamic cost was reduced by â€1,747‬.
#0 - zgorizzo69
2022-08-11T08:10:50Z
very nice summary :1st_place_medal: for the header 's table