-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New subcontractor can be set for a SCConfirmed task without current subcontractor consent #378
Comments
When a SC accepts an invitation the task is marked as active https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/Tasks.sol#L128
|
Yes, you are right, in general case // If tasks are already allocated with old cost.
if (tasks[_taskID].alerts[1]) {
// If new task cost is less than old task cost.
if (_newCost < _taskCost) {
// Find the difference between old - new.
uint256 _withdrawDifference = _taskCost - _newCost;
// Reduce this difference from total cost allocated.
// As the same task is now allocated with lesser cost.
totalAllocated -= _withdrawDifference;
// Withdraw the difference back to builder's account.
// As this additional amount may not be required by the project.
autoWithdraw(_withdrawDifference);
}
// If new cost is more than task cost but total lent is enough to cover for it.
else if (totalLent - _totalAllocated >= _newCost - _taskCost) {
// Increase the difference of new cost and old cost to total allocated.
totalAllocated += _newCost - _taskCost;
}
// If new cost is more than task cost and totalLent is not enough.
else {
// Un-confirm SC, mark task as inactive, mark allocated as false, mark lifecycle as None
// Mark task as inactive by unapproving subcontractor.
// As subcontractor can only be approved if task is allocated
_unapproved = true;
tasks[_taskID].unApprove();
// Mark task as not allocated.
tasks[_taskID].unAllocateFunds();
// Reduce total allocation by old task cost.
// As as needs to go though funding process again.
totalAllocated -= _taskCost;
// Add this task to _changeOrderedTask array. These tasks will be allocated first.
_changeOrderedTask.push(_taskID);
}
} Suppose task is 95% complete, its budget is fully spent, so changeOrder() is called per mutual agreement to add extra I.e. fully removing subcontractor from already funded and started task provides a more specific similar attack surface. By definition /**
* @dev Set a task as un accepted/approved for SC
* @dev modifier onlyActive
* @param _self Task the task being set as funded
*/
function unApprove(Task storage _self) internal {
// State/ lifecycle //
_self.alerts[uint256(Lifecycle.SCConfirmed)] = false;
_self.state = TaskStatus.Inactive;
} But in changeOrder() all the parties already reviewed and accepted the terms: // Decode params from _data
(
uint256 _taskID,
address _newSC,
uint256 _newCost,
address _project
) = abi.decode(_data, (uint256, address, uint256, address));
// If the sender is disputes contract, then do not check for signatures.
if (_msgSender() != disputes) {
// Check for required signatures.
checkSignatureTask(_data, _signature, _taskID);
} // 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);
} So, marking the task as not active and not SCConfirmed doesn't look correct in this case. Straightforward mitigation here is to keep it active, i.e. do partial flag removal, say do function unConfirm(Task storage _self) internal {
// State/ lifecycle //
_self.alerts[uint256(Lifecycle.SCConfirmed)] = false;
} |
A little bit more complex, but more correct (project logic aligned) mitigation is:
function deActivate(Task storage _self) internal {
// State/ lifecycle //
_self.state = TaskStatus.Inactive;
}
/// @dev only allow unconfirmed tasks.
modifier onlyUnconfirmed(Task storage _self) {
require(
!_self.alerts[uint256(Lifecycle.SCConfirmed)],
"Task::SCConfirmed"
);
_;
} function inviteSubcontractor(Task storage _self, address _sc)
internal
- onlyInactive(_self)
+ onlyUnconfirmed(_self)
{
_self.subcontractor = _sc;
} function acceptInvitation(Task storage _self, address _sc)
internal
- onlyInactive(_self)
+ onlyUnconfirmed(_self)
{
// Prerequisites //
require(_self.subcontractor == _sc, "Task::!SC");
// State/ lifecycle //
_self.alerts[uint256(Lifecycle.SCConfirmed)] = true;
_self.state = TaskStatus.Active;
}
/// @dev only allow inactive tasks. Task is inactive if SC is unconfirmed.
modifier onlyInactive(Task storage _self) {
require(_self.state == TaskStatus.Inactive, "Task::active");
_;
}
else {
- // Un-confirm SC, mark task as inactive, mark allocated as false, mark lifecycle as None
- // Mark task as inactive by unapproving subcontractor.
- // As subcontractor can only be approved if task is allocated
- _unapproved = true;
- tasks[_taskID].unApprove();
+ // Mark task as inactive, mark allocated as false, add to the allocation queue
+ // Mark task as inactive
+ tasks[_taskID].deActivate();
// Mark task as not allocated.
tasks[_taskID].unAllocateFunds();
// Reduce total allocation by old task cost.
// As as needs to go though funding process again.
totalAllocated -= _taskCost;
// Add this task to _changeOrderedTask array. These tasks will be allocated first.
_changeOrderedTask.push(_taskID);
} Notice that the mitigation here is to make Active and SCConfirmed states independent (as a general note, it doesn't make much sense to have some fully coinciding states). Active flags whether task is in progress right now, while SCConfirmed flags whether it ever was started, being either Active (work is being done right now) or Inactive (work had started, something was done, now it's paused). The issue basically means that this states are different and moving a task to another SC while it's SCConfirmed should be prohibited as some work was done and some payment to current SC is due |
Agree to the risk, but the severity should be 2. |
This is a good one with a very detailed explanation, but I'm afraid it fits a Medium severity better, as funds are not directly at risk but rather a malfunctioning feature that can indirectly cause damage to certain roles. |
Lines of code
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L295-L316
Vulnerability details
Malicious builder/contractor can change the subcontractor for any task even if all the terms was agreed upon and work was started/finished, but the task wasn't set to completed yet, i.e. it's
SCConfirmed
,getAlerts(_taskID)[2] == true
. This condition is not checked by inviteSC().For example, a contractor can create a subcontractor of her own and front run valid setComplete() call with a sequence of
inviteSC(task, own_subcontractor) -> setComplete()
with a signatory from theown_subcontractor
, stealing the task budget from the subcontractor who did the job. Contractor will not breach any duties with the community as the task will be done, while raiseDispute() will not work for a real subcontractor as the task record will be already changed.Setting the severity to be high as this creates an attack vector to fully steal task budget from the subcontractor as at the moment of any valid setComplete() call the task budget belongs to subcontractor as the job completion is already verified by all the parties.
Proof of Concept
inviteSC() requires either builder or contractor to call for the change and verify nothing else:
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L295-L316
_inviteSC() only checks non-zero address and calls inviteSubcontractor():
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L747-L762
inviteSubcontractor() just sets the new value:
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/Tasks.sol#L106-L111
Task is paid only on completion by setComplete():
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L349-L356
This way the absence of
getAlerts(_taskID)[2]
check and checkSignatureTask() call in inviteSC() provides a way for builder or contractor to steal task budget from a subcontractor.Recommended Mitigation Steps
Consider calling checkSignatureTask() when
getAlerts(_taskID)[2]
is true, schematically:https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L310-L313
This approach is already implemented in changeOrder() where
_newSC
is a part of hash that has to be signed by all the parties:https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L386-L403
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L477-L481
checkSignatureTask() checks all the signatures:
https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L855-L861
The text was updated successfully, but these errors were encountered: