Skip to content
Last updated: December 19, 2024

SCWE-005: Insecure Upgradeable Proxy Design

Content in BETA

This content is in beta and still under active development, so it is subject to change any time (e.g. structure, IDs, content, URLs, etc.).

Send Feedback

Relationships

Description

Insecure upgradeable proxy designs occur when a smart contract implements an upgradeable proxy pattern without properly securing or validating upgrades. This may allow unauthorized actors to change the contract’s logic, possibly introducing malicious behavior. It often happens when the upgrade functionality lacks proper access controls or when there is no timelock to delay the upgrade, giving malicious actors an opportunity to exploit the contract.

This vulnerability can lead to critical failures, including the redirection of contract calls to malicious logic or unauthorized updates that compromise the integrity of the contract.

Remediation

  • Access Control: Ensure only trusted parties (e.g., contract owners, multisig wallets) can perform upgrades.
  • Timelock Mechanism: Implement a timelock to delay upgrades and provide transparency.
  • Transparent Proxy Pattern: Use patterns that prevent unauthorized contract logic changes, such as the Transparent Proxy Pattern.
  • Audit Proxy Logic Regularly: Conduct regular audits to ensure that the upgrade mechanism is secure and follows best practices.

Samples

Vulnerable Proxy Contract

pragma solidity ^0.4.0;

contract VulnerableProxy {
    address public implementation;
    address public owner;

    function setImplementation(address _implementation) public {
        require(msg.sender == owner, "Only owner can set implementation");
        implementation = _implementation;
    }

    function () public payable {
        address _impl = implementation;
        require(_impl != address(0), "Implementation address is zero");
        assembly {
            let result := delegatecall(gas, _impl, add(msg.data, 0x20), mload(msg.data), 0, 0)
            let size := returndatasize
            let ptr := mload(0x40)
            return(ptr, size)
        }
    }
}
In this example, the VulnerableProxy contract allows the owner to update the implementation contract. If the owner is compromised, they can point the proxy to a malicious implementation, allowing the attacker to control the contract's logic.

Fixed Proxy Contract with Secure Upgrade Mechanism

pragma solidity ^0.4.0;

contract SecureProxy {
    address public implementation;
    address public owner;
    uint public lastUpgradeTime;
    uint public upgradeDelay = 1 days;  // 24 hours delay before upgrade

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can perform this action");
        _;
    }

    modifier upgradeable() {
        require(now >= lastUpgradeTime + upgradeDelay, "Upgrade not allowed yet");
        _;
    }

    function setImplementation(address _implementation) public onlyOwner upgradeable {
        implementation = _implementation;
        lastUpgradeTime = now; // Update the last upgrade time
    }

    function () public payable {
        address _impl = implementation;
        require(_impl != address(0), "Implementation address is zero");
        assembly {
            let result := delegatecall(gas, _impl, add(msg.data, 0x20), mload(msg.data), 0, 0)
            let size := returndatasize
            let ptr := mload(0x40)
            return(ptr, size)
        }
    }
}

In the fixed SecureProxy contract, the following changes have been made:

  • Access Control: The onlyOwner modifier ensures that only the contract owner can update the proxy’s implementation.
  • Timelock Mechanism: The upgradeable modifier adds a delay (set to 1 day in this example) to prevent rapid contract upgrades. The contract cannot be upgraded until the specified time has passed since the last upgrade.
  • Secure Upgrade Logic: The proxy logic is updated only after passing the necessary access control and timelock checks.