Uninitialized Proxy
This vulnerability occurs when the initialize()
function of a proxy is unprotected, allowing an attacker to freely call it and obtain higher privileges within the contract. The initialize()
function is meant to only be called once and is restricted from being called again, so if left uncalled could lead to detrimental damages to the protocol.
Vulnerability Detail
OpenZeppelin UUPS Uninitialized Proxy bug
When UUPS proxy contracts are deployed, the constructor is just an initialize() function. Usually this function initializes other variables, including important and crucial ones like owners of the implementation contract.
As the owner, you gain access to set other upgrades in the future by using the upgradeToAndCall()
function, which calls the upgraded implementation contract instead of going through a proxy. This function doesn't only change the implementation address to a new one, but also executes any migration/initialization function using DELEGATECALL
and the passed along data. If the initialization function of this new implementation contract executes SELFDESTRUCT
, the DELEGATECALL
caller will be destructed.
Examples
Wormhole Uninitialized Proxy[1]
Root Cause
Wormhole has a cryptographically secured method in proving identify through a mechanism known as "Guardian Set". This mechanism prevents unauthorized users calling admin sensitive functions, which is crucial for security.
The insecure initialize function from Wormhole provided a way to supply new guardians:
function initialize(
address[] memory initialGuardians,
uint16 chainId,
uint16 governanceChainId,
bytes32 governanceContract) external
Assuming an attacker controlled the guardian set, they could upgrade the contract, as the logic below from Wormhole indicates:
function submitContractUpgrade(bytes memory _vm) public {
Structs.VM memory vm = parseVM(_vm);
// The attacker has a valid signature
(bool isValid, string memory reason) = verifyGovernanceVM(vm);
require(isValid, reason);
GovernanceStructs.ContractUpgrade memory upgrade = parseContractUpgrade(vm.payload);
require(upgrade.module == module, "Invalid Module");
require(upgrade.chain == chainId(), "Invalid Chain");
setGovernanceActionConsumed(vm.hash);
// Upgrade to contract attacker wants
upgradeImplementation(upgrade.newContract);
)
With the attacker controlling which contract Wormhole has to upgrade to, they could deploy a malicious contract that self destructs. With the proxy pointing to a destructed contract, the proxy is now rendered useless and all funds are stuck since it can no longer update it's implementation.
Fix
Wormhole team fixed the issue in the following transaction.
The transaction called initialize()
on the implementation contract and set the Guardians, not allowing anyone else to call it.
Harvest Finance Uninitialized Proxy[2]
Root Cause
Harvest Finance had 3 uninitialized proxy implementation on these addresses:
- https://contract-library.com/contracts/Ethereum/392A5C02635DCDBD4C945785CE530A9A69DDA6C2
- https://contract-library.com/contracts/Ethereum/47228860C3EBEB999AE6CC43286FD94DF264A143
- https://contract-library.com/contracts/Ethereum/E41E27CD5C99BF93466FCE3F797CF038EFC3C37D
These contracts were not verified nor publicly available, the reporters, Dedaub, managed to decompile the contracts and perform their vulnerability analysis and found unprotected and uncalled initialize()
functions.
function initialize(uint256 daysLong, address _wrappedToken, address _coreGlobals, address _preWrapEthPair) public payable {
Harvest Finance had a function named doHardWork()
for yield farming, which DELEGATECALL
s the main contract for various tasks. More about that function can be found at line 495[3] of the decompiled contract.
If initialize()
was called by an attacker, they could set the hard-worker storage field to a contract that calls SELFDESTRUCT
, and after calling doHardWork()
, destroys the contract and bricks the proxy.
These contract had significant holdings, with around a total of $6.4M.
Fix
Changing the proxies to direct to implementation contracts that cannot SELFDESTRUCT
from untrusted callers.
Mitigation
Making sure the initialize()
function is well protected from unauthorized callers.
Call initialize()
once contract is deployed