WellUpgradeable is an upgradeable version of the Well contract, inheriting from OpenZeppelin's UUPSUpgradeable and OwnableUpgradeable contracts. According to OpenZeppelin’s documentation for UUPSUpgradeable.sol (here and here), the internal _authorizeUpgrade function must be overridden to include access restriction, typically using the onlyOwner modifier. This must be done to prevent unauthorized users from upgrading the contract to a potentially malicious implementation.
However, in the current implementation the _authorizeUpgrade function is overridden with custom logic but lacks the onlyOwner modifier. As a result, the upgradeTo and upgradeToAndCall methods can be invoked by any address, allowing anyone to upgrade the contract, leading to the deployment of malicious code and compromise the integrity of the contract.
Proof of concept
The following test demonstrates that WellUpgradeable can be upgraded by any address, not just the owner. It is based on testUpgradeToNewImplementation with the difference that a new user address is created and used to call the upgradeTo function, successfully upgrading the contract and exposing the lack of access control.
Paste the following test into WellUpgradeable.t.sol:
function testUpgradeToNewImplementationNotOwner() public {
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = new MockToken("BEAN", "BEAN", 6);
tokens[1] = new MockToken("WETH", "WETH", 18);
Call memory wellFunction = Call(wellFunctionAddress, abi.encode("2"));
Call[] memory pumps = new Call[](1);
pumps[0] = Call(mockPumpAddress, abi.encode("2"));
// create new mock Well Implementation:
address wellImpl = address(new MockWellUpgradeable());
WellUpgradeable well2 =
encodeAndBoreWellUpgradeable(aquifer, wellImpl, tokens, wellFunction, pumps, bytes32(abi.encode("2")));
vm.label(address(well2), "upgradeableWell2");
address user = makeAddr("user");
vm.startPrank(user);
WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress));
proxy.upgradeTo(address(well2));
// verify proxy was upgraded.
assertEq(address(well2), MockWellUpgradeable(proxyAddress).getImplementation());
vm.stopPrank();
}
Recommended mitigation steps
Add the onlyOwner modifier to the _authorizeUpgrade function in WellUpgradeable.sol to restrict upgrade permissions:
+ function _authorizeUpgrade(address newImplmentation) internal view override onlyOwner {
// verify the function is called through a delegatecall.
require(address(this) != ___self, "Function must be called through delegatecall");
// verify the function is called through an active proxy bored by an aquifer.
address aquifer = aquifer();
address activeProxy = IAquifer(aquifer).wellImplementation(_getImplementation());
require(activeProxy == ___self, "Function must be called through active proxy bored by an aquifer");
// verify the new implmentation is a well bored by an aquifier.
require(
IAquifer(aquifer).wellImplementation(newImplmentation) != address(0),
"New implementation must be a well implmentation"
);
// verify the new implmentation is a valid ERC-1967 implmentation.
require(
UUPSUpgradeable(newImplmentation).proxiableUUID() == _IMPLEMENTATION_SLOT,
"New implementation must be a valid ERC-1967 implmentation"
);
}
Lines of code
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L65
Vulnerability details
Impact
WellUpgradeable
is an upgradeable version of theWell
contract, inheriting from OpenZeppelin'sUUPSUpgradeable
andOwnableUpgradeable
contracts. According to OpenZeppelin’s documentation forUUPSUpgradeable.sol
(here and here), the internal_authorizeUpgrade
function must be overridden to include access restriction, typically using theonlyOwner
modifier. This must be done to prevent unauthorized users from upgrading the contract to a potentially malicious implementation.However, in the current implementation the
_authorizeUpgrade
function is overridden with custom logic but lacks theonlyOwner
modifier. As a result, theupgradeTo
andupgradeToAndCall
methods can be invoked by any address, allowing anyone to upgrade the contract, leading to the deployment of malicious code and compromise the integrity of the contract.Proof of concept
The following test demonstrates that
WellUpgradeable
can be upgraded by any address, not just the owner. It is based ontestUpgradeToNewImplementation
with the difference that a new user address is created and used to call theupgradeTo
function, successfully upgrading the contract and exposing the lack of access control.Paste the following test into
WellUpgradeable.t.sol
:Recommended mitigation steps
Add the
onlyOwner
modifier to the_authorizeUpgrade
function inWellUpgradeable.sol
to restrict upgrade permissions:Assessed type
Access Control