Summary
The current implementation of the UpgradeBranch
contract contains a critical vulnerability where the intended protection mechanism fails to function as expected. This issue arises because address(this)
refers to the proxy address rather than the implementation address, leading to the UpgradeBranch
remaining mutable and vulnerable to accidental modifications.
Vulnerability Details
The root cause of the vulnerability is that the condition branch == address(this)
always evaluates to false
because address(this)
refers to the proxy address rather than the UpgradeBranch
implementation address.
137: function replaceBranch(Data storage self, address branch, bytes4[] memory selectors) internal {
...
148: revert Errors.SelectorIsZero();
149: }
150: if (oldBranch == address(this)) {
151: revert Errors.ImmutableBranch();
152: }
153: if (oldBranch == branch) {
154: revert Errors.FunctionFromSameBranch(selector);
...
175: }
177: function removeBranch(Data storage self, address branch, bytes4[] memory selectors) internal {
178: if (branch == address(this)) {
179: revert Errors.ImmutableBranch();
180: }
181:
182: uint256 cachedSelectorsLength = selectors.length;
...
204: }
POC
The test_removeUpgradeBranch
execution result of PASS will confirm UpgradeBranch
and its corresponding upgrade
selector were removed from the RootProxy
.
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { UpgradeBranch } from "@zaros/tree-proxy/branches/UpgradeBranch.sol";
import { IPerpsEngine } from "@zaros/perpetuals/PerpsEngine.sol";
import { RootProxy } from "@zaros/tree-proxy/RootProxy.sol";
import { console } from "forge-std/console.sol";
contract Upgrade_Removal_Test is Base_Test {
function setUp() public override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
}
function test_removeUpgradeBranch() external {
UpgradeBranch upgradeBranchImpl = UpgradeBranch(IPerpsEngine(address(perpsEngine)).branchAddress(UpgradeBranch.upgrade.selector));
RootProxy.BranchUpgrade[] memory branchUpgrades = new RootProxy.BranchUpgrade[](1);
address[] memory initializables = new address[](0);
bytes[] memory initializePayloads = new bytes[](0);
bytes4[] memory upgradeBranchSelectors = new bytes4[](1);
upgradeBranchSelectors[0] = UpgradeBranch.upgrade.selector;
branchUpgrades[0] = RootProxy.BranchUpgrade({ branch: address(upgradeBranchImpl), action: RootProxy.BranchUpgradeAction.Remove, selectors: upgradeBranchSelectors });
IPerpsEngine(address(perpsEngine)).upgrade(branchUpgrades, initializables, initializePayloads);
address upgradeBranchByUpgradeSelector = IPerpsEngine(address(perpsEngine)).branchAddress(UpgradeBranch.upgrade.selector);
console.log("upgradeBranch lookup via upgrade.selector", upgradeBranchByUpgradeSelector);
assertEq(upgradeBranchByUpgradeSelector, address(0), "upgradeBranch not removed");
bytes4[] memory selectorsByUpgradeBranch = IPerpsEngine(address(perpsEngine)).branchFunctionSelectors(address(upgradeBranchImpl));
assertEq(selectorsByUpgradeBranch.length, 0, "upgradeBranch selectors not removed");
}
}
Impact
Even though it requires an admin/eDao mistake to occur, the severity should be Medium because:
1/ Had it occured, the impact is Critical as it would cause significant security risks and potentially rendering the proxy pattern inoperable.
2/ There exists an intended protection mechanism for the UpgradeBranch
. Breaking the ImmutableBranch
invariant could be considered a valid Medium.
Tools Used
Foundry test
Recommendations
To address this issue, modify the protection mechanism to correctly compare the implementation address rather than the proxy address. Ensure that the implementation address is appropriately protected to prevent accidental or malicious modifications.