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: 4/133
Findings: 6
Award: $2,623.57
π Selected for report: 1
π Solo Findings: 1
π Selected for report: Lambda
Also found by: 0x1f8b, 0x52, horsefacts, vlad_bochok, wastewa
514.2536 USDC - $514.25
Transaction replays on unrelated functions
abi.decode has a very high level of ambiguity when decoding abi.encode data. Variable types do not follow many of the conversion rules that apply to regular setting. All abi.decode does is take the bytes given to it and unpacks it and stores is at the requested variables. This means that an encoded uint256 could be unpacked as nearly any other type of data. The output is dictated by the abi.decode call not the abi.encode call. To make matters worse, the length of the input data does not need to be exactly the same as the output for abi.decode.
event BroadcastUint256 (uint256 indexed myUint); event BroadcastAddress (address indexed myAddress); function DecodeWrongLengthAndVarType () public { uint256 first = uint256(1); uint256 second = uint256(2); uint256 third = uint256(3); bytes memory data = abi.encode(first, second, third); (address firstDecoded, uint256 secondDecoded) = abi.decode(data, (address, uint256)); emit BroadcastAddress(firstDecoded); emit BroadcastUint256(secondDecoded); }
I've included some sample code above, feel free to plug it into remix and test yourself. Let's use the above decode line as an example. On the right side of the function, abi.decode is decoding data and storing it as an address and uint256. As long as data is at least 64 bytes long (address=32 bytes, uint256=32 bytes) then it will successfully store a value for both firstDecoded and secondDecoded. In the above example data is 96 bytes, abi.decode will read the first 64 bytes, store them as firstDecoded and secondDecoded, then skip the last 32 bytes. This introduces a huge amount of data malleability, because input data does not have to match the outputs of abi.decode in either length or type, the only requirement is that the input data length is at least as long as the output. Given how loose the requirements are, data from a majority of functions in Project.sol and Community.sol can be submit interchangeably and the data will be processed by abi.decode without issue.
checkSignatureValidity(builder, _hash, _signature, 0);
The other point of concern is the method by which signatures are specified. The issue is that the builder is always uses the 0 index. This means that the signature will be valid for the data for every function it is submit with, due to it always being signed with the same signatureIndex.
When a transaction is mined or even added the mempool, all the input data becomes public. This would give malicious actors access to all data and signatures. It would also allow them to find and test every possible replay opportunity. It isn't just limited to project.sol because community.sol uses the same signature/data scheme and a majority of functions that use this pattern can be called by anyone. It's clear that Rigor is a very ambitious product and will continue to expand to become more feature rich. With this expansion will come an even larger number of functions and signed data, which will continually widen the opening for data replay.
This is open ended and there are very large number of potential vectors and combinations to consider. I was not able to find a specific example but given the scope I think it is worth including, so I am submitting this as medium risk.
I strongly recommend implementing a stricter form of permissioning. I believe to fully remove the risk of replay, a majority of functions should be permissioned to the appropriate builder/contractor/subcontractor though the use of modifiers.
#0 - zgorizzo69
2022-08-11T08:08:13Z
#1 - jack-the-pug
2022-08-27T13:45:43Z
Duplicate of #75
π Selected for report: scaraven
Also found by: 0x52, Deivitto, Lambda, TrungOre, auditor0517, hansfriese, rbserver, simon135, smiling_heretic, sseefried
165.6336 USDC - $165.63
Builder reduces their interest by half
uint256 _noOfDays = (block.timestamp - _communityProject.lastTimestamp) / 86400; // 24*60*60
_communityProject.lastTimestamp = block.timestamp;
In Community.sol#returnToLender, the number of days for accumulating interest is rounded down to the nearest day. Then in Community.sol#claimInterest _communityProject.lastTimestamp is updated to block.timestamp. If a builder were to make a small repayment every 1.99 days, only 1 day worth of interest would accumulate every ~2 days. This would allow them to reduce their total paid interest by half.
This vulnerability cannot be used to avoid interest completely because if the accumulated interest == 0 then Community.sol#claimInterest won't update the timestamp.
Change L847 to:
_communityProject.lastTimestamp = (block.timestamp / 86400) * 86400;
#0 - horsefacts
2022-08-06T22:09:45Z
π Selected for report: MiloTruck
Also found by: 0x52, 8olidity, Ruhum, __141345__, cccz, codexploder, cryptonue, hansfriese, sseefried
49.6901 USDC - $49.69
All projects created while fee > 1000 can never receive funding
HomeFi.sol#replaceLenderFee allows fees > 1000 (100%). If set > 1000 all projects created after would never be able to receive funds due to lenderFee causing an underflow error in Community.sol#lendToProject.
Implement a max fee:
require(lenderFee >= 30);
#0 - horsefacts
2022-08-06T21:58:15Z
Duplicate of https://github.com/code-423n4/2022-08-rigor-findings/issues/400 (lender can set a greater than 100% fee that underflows)
π Selected for report: hansfriese
285.6964 USDC - $285.70
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L455-L481 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L484-L506 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L509-L552
Builder forced to pay higher interest
Contract is pausable and functions that reduce debt do not function when contract is paused. Interest continues to accrue during that period of time, forcing the builder to pay more interest because they are unable to repay while it's paused.
Remove whenNotPaused modifer from functions that reduce user debt.
#0 - horsefacts
2022-08-06T22:14:01Z
π Selected for report: 0x52
1567.6074 USDC - $1,567.61
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L501-L506 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L108-L115
User is unable to revoke previously approved hash
If user reconsiders or notices something malicious about the hash after signing, they should be able to revoke the hash. For example the user approves a hash only to find out later that the hash has been spoofed and they weren't approving what they thought they were. To protect themselves the user should be able to revoke approval, otherwise it may lead to loss of funds or access.
Add the following function:
function revokeHash(bytes32 _hash) external virtual { approvedHashes[_msgSender()][_hash] = false; }
#0 - parv3213
2022-08-18T09:58:43Z
Duplicate of #86
#1 - parv3213
2022-09-12T06:39:56Z
Duplicate of #86
This is a separate issue and not a duplicate. I do not find it essential to revoke a hash. As off-chain signatures can never be marked as invalid, adding this feature for on-chain signatures makes no sense.
π 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
The underlying proxy for HomeFi.sol stores the admin address in the EIP1967 compliant location at:
bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
HomeFi.sol implements a secondary non-EIP1967 admin address in L52, which is set in L113 of initialize.
admin should be renamed to something else such as owner to avoid any confusion
Contracts never deal directly with eth, but implement a payable fallback and receive
N/A
Fallback should not be payable and receive should revert.
HomeFi.sol mints an NFT when calling createProject, but never uses any feature associated with it. It consumes a lot of gas to mint an NFT then never use it for anything. Implementing the ERC721 standard increases gas cost to deploy the contract because of the extra code required.
Don't mint NFTs when calling createProject and don't implement ERC721
Current method of storing acceptable currency can only ever support 3 currencies. In order to add more, a new implementation would have to be deployed. If currencies were stored in a mapping it would be easy and convenient to add new currencies without having to deploy a new contract.
Store the approved currencies in a mapping(address => bool) to approve a currency set the bool for that currency address to true. To validate a currency simply check if the mapping returns true for its address.
There is no way to remove a member from a community after they've been added. It could help with bloat if there was a way to remove users.
N/A
Add a function that allows the owner to remove users. Additionally add a nonce to addMember so the transaction data can't be replayed after the user is removed