DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: low
Invalid

M-01. Function `RootUpgrade::removeBranch` allows to remove Upgrade Branch, resulting in the Protocol loses upgradability

https://github.com/Cyfrin/2024-07-zaros/blob/d687fe96bb7ace8652778797052a38763fbcbb1b/src/tree-proxy/leaves/RootUpgrade.sol#L177-L204

Summary

If the owner proceeds to upgrade the UpgradeBranch::upgrade function via the RootUpgrade::removeBranch function, it would render the addition of a new UpgradeBranch::upgrade function to the Protocol through the RootUpgrade::addBranch function impossible. Without the presence of the UpgradeBranch::upgrade function the Protocol loses upgradability.

Vulnerability Details

The UpgradeBranch uses for upgrade the Protocol. To do that, owner can call UpgradeBranch::upgrade function with next params Link to code:

FILE: src/tree-proxy/branches/UpgradeBranch.sol
function upgrade(
RootProxy.BranchUpgrade[] memory branchUpgrades,
address[] memory initializables,
bytes[] memory initializePayloads
) { ... }
FILE: src/tree-proxy/RootProxy.sol
enum BranchUpgradeAction {
Add,
Replace,
Remove
}
...
struct BranchUpgrade {
address branch;
BranchUpgradeAction action;
bytes4[] selectors;
}

In the current realization owner has two ways to upgrade any function:

  • First: call upgrade function with BranchUpgradeAction.Replace param

  • Second: call upgrade function with BranchUpgradeAction.Remove param and than call upgrade function with BranchUpgradeAction.Add param

RootUpgrade::removeBranch function link to code is internal function that provides the logic for removing branch selectors when BranchUpgradeAction.Remove param was choosen. There is one check for branch in this function: if (branch == address(this)) { revert Errors.ImmutableBranch(); }.
Since this function calls trought the Proxy, address(this) is address of the Proxy, i.e. address of PerpsEngine contract.

There is two checks for branch selector in RootUpgrade::removeBranch function: selector != bytes4(0) and self.selectorToBranch[selector] == branch.
So, owner can remove functions from any branch incude UpgradeBranch::upgrade.

FILE: src/tree-proxy/leaves/RootUpgrade.sol
function removeBranch(Data storage self, address branch, bytes4[] memory selectors) internal {
if (branch == address(this)) {
revert Errors.ImmutableBranch();
}
uint256 cachedSelectorsLength = selectors.length;
for (uint256 i; i < cachedSelectorsLength; i++) {
bytes4 selector = selectors[i];
// also reverts if left side returns zero address
if (selector == bytes4(0)) {
revert Errors.SelectorIsZero();
}
if (self.selectorToBranch[selector] != branch) {
revert Errors.CannotRemoveFromOtherBranch(branch, selector);
}
delete self.selectorToBranch[selector];
// slither-disable-next-line unused-return
self.branchSelectors[branch].remove(selector);
}
// if no more selectors in branch, remove branch address
if (self.branchSelectors[branch].length() == 0) {
// slither-disable-next-line unused-return
self.branches.remove(branch);
}
}

If owner chooses second way to upgrade UpgradeBranch::upgrade function itself, he will find themselves unable to call upgrade function with BranchUpgradeAction.Add param following its removal. Since the upgrade function will be removed from the list of functions available for calling. Owner will no longer be able to call the upgrade function and update the protocol.

Proof of Concept

Let's add a new test to the upgrade.t.sol test file. We remove an upgrade function from the Protocol. Then we try to upgrade the protocol in several ways.

Also, add import { UpgradeBranch } from "@zaros/tree-proxy/branches/UpgradeBranch.sol"; to this file for corretly testing.

function test_WhenRemovingAUpgradeBranch() external givenTheSenderIsTheOwner {
changePrank({ msgSender: users.owner.account });
// Remove an upgrade function from UpgradeBranch
bytes4 selector = UpgradeBranch.upgrade.selector;
address upgradeBranch = perpsEngine.branchAddress(selector);
address[] memory branches = new address[](1);
branches[0] = upgradeBranch;
bytes4[] memory upgradeBranchSelectors = new bytes4[](1);
upgradeBranchSelectors[0] = selector;
bytes4[][] memory branchesSelectors = new bytes4[][](1);
branchesSelectors[0] = upgradeBranchSelectors;
RootProxy.BranchUpgrade[] memory branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Remove);
perpsEngine.upgrade(branchUpgrades, new address[](0), new bytes[](0));
// Try to add new upgrade function to Upgrade branch
branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Add);
// it should not return the removed branch functions
vm.expectRevert({
revertData: abi.encodeWithSelector(
Errors.UnsupportedFunction.selector, UpgradeBranch.upgrade.selector
)
});
perpsEngine.upgrade(branchUpgrades, new address[](0), new bytes[](0));
// Try to remove another function
Branch.Data[] memory perpsEngineBranches = perpsEngine.branches();
address orderBranchAddress;
for (uint256 i; i < perpsEngineBranches.length; i++) {
if (i == 4) {
orderBranchAddress = perpsEngineBranches[i].branch;
}
}
branches[0] = orderBranchAddress;
bytes4[] memory orderBranchSelectors = new bytes4[](1);
orderBranchSelectors[0] = OrderBranch.getActiveMarketOrder.selector;
branchesSelectors[0] = orderBranchSelectors;
branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Remove);
// we expect this call also revert because upgrade function isn't allow
vm.expectRevert({
revertData: abi.encodeWithSelector(
Errors.UnsupportedFunction.selector, UpgradeBranch.upgrade.selector
)
});
perpsEngine.upgrade(branchUpgrades, new address[](0), new bytes[](0));
// Try to replace some function function
address newOrderBranch = address(new NewOrderBranch());
branches[0] = newOrderBranch;
bytes4[] memory newOrderBranchSelectors = new bytes4[](1);
newOrderBranchSelectors[0] = NewOrderBranch.getName.selector;
branchesSelectors[0] = newOrderBranchSelectors;
branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Replace);
// we expect this call also revert because upgrade function isn't allow
vm.expectRevert({
revertData: abi.encodeWithSelector(
Errors.UnsupportedFunction.selector, UpgradeBranch.upgrade.selector
)
});
perpsEngine.upgrade(branchUpgrades, new address[](0), new bytes[](0));
}

Let's run the test: forge test --mt test_WhenRemovingAUpgradeBranch

2024-07-zaros$ forge test --mt test_WhenRemovingAUpgradeBranch
[⠒] Compiling...
[⠆] Compiling 1 files with Solc 0.8.25
[⠔] Solc 0.8.25 finished in 7.13s
Compiler run successful!
Ran 1 test for test/integration/tree-proxy/upgrade-branch/upgrade/upgrade.t.sol:Upgrade_Integration_Test
[PASS] test_WhenRemovingAUpgradeBranch() (gas: 2775890)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 16.25ms (1.22ms CPU time)
Ran 1 test suite in 16.95ms (16.25ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

We can see the test passing.

Impact

Vulnerability requires owner actions. But the impact of these actions is High. The protocol will completely lose the upgradability. To regain this functionality, it becomes necessary to redeploy the Protocol.

Tools Used

Manual Review

Recommendations

Add the require in the RootUpgrade::removeBranch function to check that branch selector doesn't equal UpgradeBranch::upgrade function selector.

FILE: src/tree-proxy/leaves/RootUpgrade.sol
+ import { UpgradeBranch } from "@zaros/tree-proxy/branches/UpgradeBranch.sol";
...
function removeBranch(Data storage self, address branch, bytes4[] memory selectors) internal {
if (branch == address(this)) {
revert Errors.ImmutableBranch();
}
uint256 cachedSelectorsLength = selectors.length;
for (uint256 i; i < cachedSelectorsLength; i++) {
bytes4 selector = selectors[i];
// also reverts if left side returns zero address
if (selector == bytes4(0)) {
revert Errors.SelectorIsZero();
}
if (self.selectorToBranch[selector] != branch) {
revert Errors.CannotRemoveFromOtherBranch(branch, selector);
}
+ if (selector == UpgradeBranch.upgrade.selector) {
+ revert("Don't remove upgrade selector");
+ }
delete self.selectorToBranch[selector];
// slither-disable-next-line unused-return
self.branchSelectors[branch].remove(selector);
}
// if no more selectors in branch, remove branch address
if (self.branchSelectors[branch].length() == 0) {
// slither-disable-next-line unused-return
self.branches.remove(branch);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Function `RootUpgrade::removeBranch` allows to remove Upgrade Branch, resulting in the Protocol loses upgradability

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

Function `RootUpgrade::removeBranch` allows to remove Upgrade Branch, resulting in the Protocol loses upgradability

Appeal created

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

Function `RootUpgrade::removeBranch` allows to remove Upgrade Branch, resulting in the Protocol loses upgradability

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.