Common proxy vulnerabilities in Solidity part 1

Source: OpenZeppelin

Proxies are used to implement upgradeability in Solidity smart contracts. They serve as a middleman between a contract and its users. They are employed to change a contract’s logic without altering its address. A master copy contract and a proxy contract that refers to the master copy are how proxies are typically set up. A lot has already been said about how proxies are built, if you are not familiar with it in the last paragraph you can find references to materials about how to create proxies and their types – it is recommended to understand proxies at least on a foundational level first. In this article we will focus solely on potential security issues that are typically found in upgradable smart contracts.

Uninitialized proxy

Proxies do not have a constructor, instead, they usually have the initialize() function which sets the key parameters – the reason for this is described here in details. Normally, the initialization should be an one-time operation, similarly to the constructor. However it might happen, that for some reason function initialize() is not called and the contract is deployed on the blockchain with the initialize() function possible to be called by anyone! As you probably imagine, this may lead to serious consequences, since usually the initializers sets the owner to the caller, which means, any attacker may call the function and take over the vulnerable contract. The exploitation scenario usually may involve updating the contract implementation to attacker-controlled one, which contains a malicious function that executes selfdestruct. If the proxy utilizes delegatecall, once selfdestruct is called, the destruction operation will be performed on the vulnerable contract’s storage (as this is how delegatecall works).

References:

https://medium.com/immunefi/wormhole-uninitialized-proxy-bugfix-review-90250c41a43a

https://medium.com/immunefi/harvest-finance-uninitialized-proxies-bug-fix-postmortem-ea5c0f7af96b

https://forum.openzeppelin.com/t/security-advisory-initialize-uups-implementation-contracts/15301

Unable to upgrade

Understanding the proxy upgrade process should be key point when auditing an upgradeable contract, especially, if the contract contains altered logic taken from original, well-known solidity libraries.

pragma solidity ^0.8.0;

contract ImplementationV1 {
    function doSomething() public pure {
        // code that does something
    }
}

contract Upgradable {
    address public implementation;
    bool public initialized;

    constructor() public {
        implementation = address(new ImplementationV1());
        initialized = true;
    }

    function upgradeImplementation(address _newImplementation) public {
        require(!initialized);
        implementation = _newImplementation;
    }
}

In above code, which is intentionally vulnerable, there is a logic issue. As you can see, the require statement in function upgradeImplementation() will always be false after the contract variable initialized is set to true (which will be always the case as it’s being set in the constructor). This way, the contract cannot be upgraded, so one of core utilities is broken. As a result, the effort required to re-deploy the contract might be extremely high, especially, if the contract is a critical part of a DeFi with a high TVL.

Multiple initializations

While current OpenZeppelin proxy implementations make use of modifiers such as initializer or disableinitializers it is possible that if there are custom tweaks in proxy implementations, the fact of initialization will not be notified in the smart contract, which might lead to a scenario equal to an uninitialized proxy, or in slightly more secure case, that the initialization can be done multiple times, but its only possible for contract admin.

pragma solidity ^0.8.0;

contract Upgradable {
    address public implementation;
    bool public initialized;

    function initialize() public {
        require(!initialized);
        implementation = address(new ImplementationV1());
        owner = msg.sender;
        initialized = false;
        uint256 totalBalance = 0; //assume its some key variable holding user's balances
    }

    function upgradeImplementation(address _newImplementation) public onlyOwner {
        implementation = _newImplementation;
    }


}

In above scenario, the initializer fails to mark that initialization has been done, which means, it can be done multiple times. In this case, initialize() can be at any time called by an attacker to take over the contract as in uninitialized proxy scenario mentioned previously. Moreover, even if that’s mitigated e.g. by checking the ownership, consider that function still stays open for the owner, and it’s accidentally called, clearing all users’ balances.

Frontrunning initialization

When initializing an upgradable contract, you should be aware that there is still a possibility to get front run. Since initialization is usually public function, even if its one-time, it should be checked after deployment if the transaction succeeded from proper account and if it was not frontrun. It would be a pity to discover on a late stage of a project, that the deployer is in fact someone else. Of course, there is a possibility to implement in-code check, for example, if the initialization has been called twice or more in a short time and in such case revert, however, simply examining the contract after deployment if the owner is who it was meant to be should be the quickest and most effective way of ensuring no frontrunning took place.

Proxy resources

If you want to learn more about the proxies itself to better understand how they are built, you might want to take a look at following resources:

Youtube Smart Contract Programmer channel, videos dedicated to building a proxy:

Also resources from CoinMonks:

In case you want to read also the second part of this article, it’s available here.

Article, News & Post

More Post

A guide to reentrancy: abusing the external calls for fun and profit

The reentrancy vulnerability is one of most serious ones that can be found in solidity smart contracts. Apart from the “classic” reentrancy like the one from the infamous DAO hack, there are other types of this vulnerability. In this article, we describe them along with some popular strategies on how

Ethereum signatures for hackers and auditors 101

In real world you can sign documents using your personal signature, which is assumed to be unique and proves that you support, acknowledge or commit something. The same can be done on ethereum blockchain and in solidity smart contracts – but using cryptography. In this article, we will briefly explain

Common proxy vulnerabilities in Solidity – part 2

In the previous part, we explained some of typical proxy issues related to initialization, lack of state update or frontrunning. In this part, we would like to talk a bit about function and storage conflicts and also about decentralization. Proxy function clashing This vulnerability is unlikely to be found unless

Automated auditing part 2 – usage of AI for Smart Contracts testing

Introduction Creating a project or solution from scratch is a difficult and time-consuming process. A business concept must first be developed, then it must be translated into a high-level solution architecture, and finally the software development stage takes place. Because we will be focusing on hypothetical smart contracts in this

Common proxy vulnerabilities in Solidity part 1

Proxies are used to implement upgradeability in Solidity smart contracts. They serve as a middleman between a contract and its users. They are employed to change a contract’s logic without altering its address. A master copy contract and a proxy contract that refers to the master copy are how proxies

Echidna - Wikipedia

Automated auditing part 1 – fuzzing with Echidna

What is Echidna? In this part, we will cover the very basics of Echidna usage. Echidna is an animal, but it is also the name of a Solidity fuzzer. This tool is really worth mastering since a skilled user can be able to test a smart contract with it in