The window to exchange $HAI for Hacken Equity Shares ($HES) is now open > Claim your spot today

  • Hacken
  • Blog
  • Discover
  • Upgradeable Smart Contracts (USCs): Exploring The Concept And Security Risks

Upgradeable Smart Contracts (USCs): Exploring The Concept And Security Risks

By Palamarchuk RomanandMalanii Oleh

Share via:

Upgradeable smart contracts are bringing flexibility to the otherwise immutable code. Many companies, such as Compound Finance and OpenSea widely adopt them as the future of smart contract development. However, security problems are also emerging. Critics say upgradeability weakens blockchain’s immutability, while supporters believe it enhances safety by addressing bugs and preventing exploits. Let’s break it down.

What Are Upgradeable Smart Contracts?

Typically, smart contracts are unchangeable once deployed. This immutability builds trust among DeFi parties since even the contract’s creator can’t alter it. However, this also means they can’t be updated, posing risks if security or other issues arise.

Upgradeable Smart Contracts (USC) solve these issues by allowing updates without the need to migrate all activity to a new address.

The earliest and most intuitive method of updating smart contracts with no state migrations is the data-separation pattern. Here, we separate a smart contract into logic and storage contracts, storing and changing the state respectively. The problem with this method is evident: constant calls between logic and storage require gas.

That’s why modern USCs rely on proxies. A fixed proxy contract stores the system’s state and a changeable logic address. Unlike normal contracts, users communicate with the proxy, which forwards their calls to the business logic contract. In proxy-based upgradeability, logic contracts don’t store any user data. Hence, the upgrade is easy: a new logic contract is deployed, and the only call needed is to replace the old address in the proxy contract.

Learn more: Bodell et al. (2023). “Proxy Contracts

Benefits Of Upgradeable Smart Contracts

  • Enhanced Security: Developers can fix undetected smart contract vulnerabilities within the same contract.
  • Feature Addition: Teams can introduce new features or adapt without creating a new contract.
  • Cost Efficiency: Upgrades can save on gas fees compared to deploying new contracts.
  • Data Consistency: Data, like user balances, remains unchanged during upgrades, eliminating migration hassles.
  • User-Friendly: The contract address remains constant, making interactions simpler.
  • Less Fragmentation: A single contract address minimizes confusion.

Mechanics Of Upgradeable Smart Contracts

Transparent Proxy

Transparent proxies handle access control and upgradability within the proxy. Both admins and users are limited to logic within their access range.

ProsCons
Simple and easy to understand.More expensive in terms of gas cost due to storage layout requirements.
Most tools and libraries support it.Contains specific storage slots that are reserved, which can lead to confusion.
Pros and cons of transparent proxy pattern

Transparent Proxy Pattern is a prevalent design for upgradeable smart contracts. At its core, it utilizes `delegatecall` to delegate the execution of function calls to an implementation contract.


The mechanism involves two contracts: Proxy and Implementation. When you send a transaction to Proxy Contract, it forwards it to Implementation Contract using `delegatecal`, thereby preserving the context (e.g., msg.sender and msg.value) of the calling transaction.

Example of transparent proxy smart contracts: 

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Proxy contract
contract TransparentProxy {
    address public implementation;
    address public admin;

    constructor(address _implementation, address _admin) {
        implementation = _implementation;
        admin = _admin;
    }

    fallback() external payable {
        address _impl = implementation;
        require(_impl != address(0));
        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize())
            let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
            let size := returndatasize()
            returndatacopy(ptr, 0, size)
            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
    }
}

// Sample implementation contract
contract MyContract {
    uint public value;

    function setValue(uint _value) external {
        value = _value;
    }
}

Universal Upgrade Proxy Standard (UUPS)

ProsCons
More gas-efficient for upgrades.Requires strict storage management to avoid storage clashes.
Allows for more flexibility in contract design.Not as common as the transparent proxy, but gaining traction.
Pros and cons of UUPS

UUPS is a more gas-efficient way of achieving upgradability. Instead of defining storage in the proxy contract, it uses the same storage layout as its implementation contract. This design makes the upgrade cheaper but requires more care in terms of not rearranging the storage layout in upgraded contracts.

Nowadays, smart contract developers opt for UUPS. They offer the same versatility as transparent proxies but are more cost-effective to launch because the upgrade logic isn’t housed in the proxy.

Example of UUPS:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// UUPS Proxy contract
contract UUPSProxy {
    address public implementation;
    address public admin;

    constructor(address _implementation, address _admin) {
        implementation = _implementation;
        admin = _admin;
    }

    function _delegate(address _impl) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    fallback() external payable {
        _delegate(implementation);
    }
}

// Sample implementation contract
contract MyContract {
    uint public value;

    function setValue(uint _value) external {
        value = _value;
    }
}

Diamonds

Diamonds introduce a cascade mechanism that directs function requests to their corresponding implementation contracts through a mapping system. This system breaks down logic, including the upgrade interface, into specialized implementation contracts, known as facets, each having its unique address recorded in the mapping.

Overall, diamonds reduce potential pitfalls associated with the UUPS upgrade approach:

  1. Diamond Cut Functionality: Post-upgrade, an external function call can be executed, separating the initialization logic from both the proxy and implementation contracts. 
  2. Single Transaction Execution: Diamond Cut encapsulates both upgrade execution and initialization into one transaction, ensuring that new state variables are initialized and secure.
  3. Isolated Initialization Logic: Diamond separates its initialization logic from both the proxy and implementation contracts, safeguarding against potential external threats to either.
  4. DiamondMultiInit: Streamlines the upgrade process, allowing for multiple initializations of all state variables.

In essence, the Diamond structure offers a comprehensive, flexible, and secure upgrade mechanism, enhancing the overall solidity and efficiency. But this is the least tried and tested method because of its novelty.

Implementing Upgradeable Smart Contracts

There’s no single standard for implementing USCs as many EIPs and 3rd-party libraries exist. We have collected relevant documentation for each standard. 

EIPs

  • EIP-1538: Transparent contract standard
  • EIP-1822: Universal upgradeable proxy standard (UUPS)
  • EIP-1967: Proxy storage slots
  • EIP-2535: Diamonds, multi-facet proxy.

Third-party library

OpenZeppelin’s Upgrade Plugins is a good solution that supports both Transparent Proxy and UUPS patterns.

Pros

  • Auto-checks storage layout for upgrades
  • Seamlessly works with Truffle & Hardhat.
  • Regularly updated and audited by the OpenZeppelin team

Cons:

  • Requires understanding of OpenZeppelin’s approach
  • Adds dependencies and tooling to the dev environment.

Implementation instructions:

Example of OpenZeppelin Upgrade:

const { deployProxy, upgradeProxy } = require('@openzeppelin/hardhat-upgrades');

async function main() {
    const MyContract = await ethers.getContractFactory('MyContract');
    const instance = await deployProxy(MyContract, [/* constructor arguments */]);
    console.log('Deployed at:', instance.address);
    
    // ... later ...

    const MyContractV2 = await ethers.getContractFactory('MyContractV2');
    const upgradedInstance = await upgradeProxy(instance.address, MyContractV2);
    console.log('Upgraded to:', upgradedInstance.address);
}

main();

Potential Security Risks 

There’s no single standard for implementing upgradeable smart contracts correctly, which increases the risks of producing serious security weaknesses.

Missing Call: There’s a big risk of a missing call to initialize a contract and its dependencies. Normally, the constructor is run on deployment to initialize the contract and dependencies like Ownable. But it’s not the case in USCs. Developers may forget to initialize a contract and dependencies, leading to serious consequences, as in the case of Wormhole’s uninitialized proxy.

Storage Collisions: Modifying the storage layout during an upgrade might lead to storage collisions between implementation versions. Storage collision is when two distinct variables point to the same storage location, leading to unexpected and unintended results. This is one of the most important security issues.

Unauthorized Upgrades: The mechanism that allows the contract to be upgraded must be well-protected. If malicious actors gain control, they could replace the contract with a malicious version, compromising user funds or data.

Denial of Service (DoS) After Upgrade: If an upgrade is not properly tested, it might introduce vulnerabilities that could be exploited for DoS attacks.

Unprotected Initialization: Initialization functions should be protected so they can’t be called multiple times. If they aren’t, attackers could reset the contract state.

Initializable Implementations: Some implementations can be initialized more than once, allowing attackers to seize control or reset them.

If unchecked, those vulnerabilities can snowball into costly exploits. We strongly recommend undergoing a smart contract audit to ensure your USCs don’t contain any errors.

Conclusions

Technically speaking, upgradeability doesn’t alter the immutability of smart contracts; it simply introduces a new contract to replace the old one. However, there are different methods of implementing USCs.

The most widely used approach is a proxy contract. This method manages state variables and refers to an implementation contract for logic, permitting upgrades without affecting data.

Follow @hackenclub on 𝕏 (Twitter)


References:

https://www.usenix.org/system/files/usenixsecurity23-bodell.pdf

https://ethereum.org/en/developers/docs/smart-contracts/upgrading/
https://docs.openzeppelin.com/learn/upgrading-smart-contracts

https://www.quicknode.com/guides/ethereum-development/smart-contracts/an-introduction-to-upgradeable-smart-contracts

subscribe image
promotion image
IMPORTANT

Subscribe to our newsletter

Enter your email address to subscribe to Hacken Reseach and receive notifications of new posts by email.

Read next:

More related
  • Blog image
    DISCOVER
    Bitcoin Layer 2 Uncovered: Exploring Its Growth & Key Projects To Watch In 2024 Malanii O.
  • Blog image
  • Blog image

Get our latest updates and expert insights on Web3 security