Summary
The addBranch
and replaceBranch
functions in the RootUpgrade
library do not check for potential selector collisions. This means that different branches could be assigned the same function selectors, leading to incorrect function calls and unexpected behavior in the proxy contract.
Vulnerability Details
In the addBranch
and replaceBranch
functions, new branches are added or existing branches are replaced with new selectors. However, there is no mechanism to verify that these selectors are not already in use by other branches. If a collision occurs, where the same selector is associated with multiple branches, the proxy contract will not be able to determine which branch's function to call, resulting in unpredictable behavior.
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/tree-proxy/leaves/RootUpgrade.sol#L115-L135
function addBranch(Data storage self, address branch, bytes4[] memory selectors) internal {
self.branches.add(branch);
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] != address(0)) {
revert Errors.FunctionAlreadyExists(selector);
}
self.selectorToBranch[selector] = branch;
self.branchSelectors[branch].add(selector);
}
}
https://github.com/Cyfrin/2024-07-zaros/blob/main/src/tree-proxy/leaves/RootUpgrade.sol#L137-L175
function replaceBranch(Data storage self, address branch, bytes4[] memory selectors) internal {
self.branches.add(branch);
uint256 cachedSelectorsLength = selectors.length;
for (uint256 i; i < cachedSelectorsLength; i++) {
bytes4 selector = selectors[i];
address oldBranch = self.selectorToBranch[selector];
if (selector == bytes4(0)) {
revert Errors.SelectorIsZero();
}
if (oldBranch == address(this)) {
revert Errors.ImmutableBranch();
}
if (oldBranch == branch) {
revert Errors.FunctionFromSameBranch(selector);
}
if (oldBranch == address(0)) {
revert Errors.NonExistingFunction(selector);
}
self.selectorToBranch[selector] = branch;
self.branchSelectors[branch].add(selector);
self.branchSelectors[oldBranch].remove(selector);
if (self.branchSelectors[oldBranch].length() == 0) {
self.branches.remove(oldBranch);
}
}
}
Impact
If a selector collision occurs, the proxy contract may call the wrong function implementation, leading to unexpected and potentially harmful behavior.
Tools Used
Manual Review
Recommendations
Add logic to the addBranch
and replaceBranch
functions to check if the new selectors are already associated with other branches. If a collision is detected, revert the transaction or handle it gracefully.
function addBranch(Data storage self, address branch, bytes4[] memory selectors) internal onlyOwner {
for (uint256 i; i < cachedSelectorsLength; i++) {
bytes4 selector = selectors[i];
if (self.selectorToBranch[selector] != address(0)) {
revert Errors.FunctionAlreadyExists(selector);
}
}
}
function replaceBranch(Data storage self, address branch, bytes4[] memory selectors) internal onlyOwner {
for (uint256 i; i < cachedSelectorsLength; i++) {
bytes4 selector = selectors[i];
if (self.selectorToBranch[selector] != address(0) &&
self.selectorToBranch[selector] != oldBranch) {
revert Errors.FunctionAlreadyExists(selector);
}
}
}