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: 43/133
Findings: 2
Award: $137.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
94.8726 USDC - $94.87
https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Project.sol#L330-L359 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Project.sol#L705-L713 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Project.sol#L386-L490 https://github.com/code-423n4/2022-08-rigor/blob/main/test/utils/projectTests.ts#L1110
In Project.sol, once a task is set as completed (by calling function setComplete), the contract pays the subcontractor. Once in this state, in should not be possible to change the task state back to ACTIVE/INACTIVE, because then the same task could be set as completed again and payed out multiple times. Furthermore, function projectCost would not return the real cost, because it loops through all tasks of a project adding up the cost of each task, but the real cost of a completed task payed out multiple times would be the cost of the task times the number of times it has been set as completed.
A call to function changeOrder can unapprove a completed task just by passing a _newCost greater than _taskCost and provided that (totalLent - _totalAllocated < _newCost - _taskCost) holds. It can also be unapproved by passing in a new subcontractor to changeOrder.
It could be possible for a malicious contractor and subcontractor to (almost) drain the total lent to the project by setting as complete, changing order to unnaprove task, then setting again as complete, multiple times.
I added a new test in file projectTests.ts, substituting test 'should be able to complete a task' with the following test:
it('should be able to changeOrder after complete a task and new state should be INACTIVE', async () => { const taskID = 1; const _taskCost = 2 * taskCost; const taskSC = signers[3]; const data = { types: ['uint256', 'address'], values: [taskID, project.address], }; const [encodedData, signature] = await multisig(data, [ signers[0], signers[1], taskSC, ]); await mockDAIContract.mock.transfer .withArgs(taskSC.address, _taskCost) .returns(true); await mockDAIContract.mock.transfer .withArgs(await homeFiContract.treasury(), _taskCost / 1e3) .returns(true); const tx = await project.setComplete(encodedData, signature); tx.wait(); const taskDetails = await project.getTask(taskID); expect(taskDetails.state).to.equal(3); const { subcontractor, cost } = await project.getTask(taskID); expect(subcontractor).to.equal(taskSC.address); const newCost = taskCost * 100; const newSC = subcontractor; const projectAddress = project.address; const data2 = { types: ['uint256', 'address', 'uint256', 'address'], values: [taskID, newSC, newCost, projectAddress], }; const [encodedData2, signature2] = await multisig(data2, [ signers[0], signers[1], taskSC, ]); const tx2 = await project.changeOrder(encodedData2, signature2); tx2.wait(); const { state } = await project.getTask(taskID); expect(state).to.equal(3); });
It first sets as complete a task, and then it call changeOrder with a greater cost and same subcontractor. The test fails, because it expects the task state to be 3 (COMPLETE), but it is 1 (INACTIVE) instead.
Hardhat
There are various things that can be done to mitigate this issue, but I would:
#0 - 0xA5DF
2022-08-11T15:34:47Z
Subset of #95 (not reusing signature, requires contractor to be an accomplice)
#1 - jack-the-pug
2022-08-27T04:45:04Z
Duplicate of #95
🌟 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