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];
if (selector == bytes4(0)) {
revert Errors.SelectorIsZero();
}
if (self.selectorToBranch[selector] != branch) {
revert Errors.CannotRemoveFromOtherBranch(branch, selector);
}
delete self.selectorToBranch[selector];
self.branchSelectors[branch].remove(selector);
}
if (self.branchSelectors[branch].length() == 0) {
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 });
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));
branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Add);
vm.expectRevert({
revertData: abi.encodeWithSelector(
Errors.UnsupportedFunction.selector, UpgradeBranch.upgrade.selector
)
});
perpsEngine.upgrade(branchUpgrades, new address[](0), new bytes[](0));
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);
vm.expectRevert({
revertData: abi.encodeWithSelector(
Errors.UnsupportedFunction.selector, UpgradeBranch.upgrade.selector
)
});
perpsEngine.upgrade(branchUpgrades, new address[](0), new bytes[](0));
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);
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);
}
}