code-423n4 / 2024-07-basin-validation

0 stars 0 forks source link

`WellUpgradeable` can be upgraded by anyone #65

Closed c4-bot-6 closed 3 months ago

c4-bot-6 commented 3 months ago

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 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"
        );
    }

Assessed type

Access Control