14

I was looking at the recent FE badger DAO exploit and this Twitter thread in it https://twitter.com/CryptoCatVC/status/1466380960648380419?s=20

One piece of advice the author gives before signing a metamask transaction is to inspect the etherscan address of the contract and see if it is a proxy contract.

What exactly is a proxy contract? How does one check if a contract is a proxy contract? What are the risks involved with interacting with a proxy contract?

3 Answers 3

19

A proxy contract is a contract which delegates calls to another contract. To interact with the actual contract you have to go through the proxy, and the proxy knows which contract to delegate the call to (the target).

A proxy pattern is used when you want upgradability for your contracts. This way the proxy contract stays immutable, but you can deploy a new contract behind the proxy contract - simply change the target address inside the proxy contract.

Therefore it's a bit dangerous to use a proxy contract, since there are no guarantees that the underlying (target) contract hasn't been changed to a malicious one. There is no strict definition on how to detect a proxy contract, but basically it's anything that delegates the functionality to another contract. You have to analyze the source code to be able to decide.

2
  • 1
    You can see if a contract uses a proxy by looking on EtherScan. See my answer. Commented Jun 16, 2022 at 23:32
  • You can use a proxy because you just want to clone a contract many times. You might not want to upgrade the contract, but you save gas by keeping the functionality in the same contract while the state variables live in the clones. This is what Uniswap does, for example. Look at the OpenZeppelin's Clones library.
    – Jose4Linux
    Commented Aug 22, 2022 at 11:48
6

Proxies (or "diamonds", which is just a name for a specific proxy system specified as an EIP) allow part or all of a contract to be swapped out at any time, without changing the address of the primary entry point for the contract. The entry point simply stores the address of the latest implementation (which may be changed by the contract owner), and then it forwards any function calls to the address of the latest implementation. Proxies were developed because contract deployment is irreversible: the code of a deployed contract can never be changed. But contract storage can be changed. So at the deployment address, the proxy code is deployed, along with the forwarding address of the actual contract you want users to call. Then any function call to the proxy address will just be forwarded to the actual target contract. There are a couple more functions in the proxy contract that allow the owner of the contract to update the forwarding address. This is how the proxied "real" contract can be updated.

Proxies are less safe non-proxied contracts if you don't trust the contract owners (since the owners can swap out the implementation at any time, screwing with their users however they want), but they are more safe than non-proxied contracts if you do trust the contract owners (since the owners can fix bugs at any time).

If a security bug is found in a contract implementation, and it was not deployed to work through a proxy, then the only way to fix the security bug is to:

  1. Freeze all activity on the buggy contract, if your API even has a call to accomplish that (but adding that sort of API will reduce user confidence in your contract if they don't trust you).
  2. Fix the bug to create a new bug-free version of the contract.
  3. Make a complete copy of the data structures of the old contract, reversing any damaging changes to account balances etc. that were made by hackers, if the vulnerability was actively exploited. (Note that this may cause a ripple effect of bugs on other contracts, such as exchanges, since they expect that the "rug won't be pulled out from under them", i.e. that account balances etc. will not suddenly change in unexpected ways. This issue is true even for proxied contracts, if you try to fix any damage done, by overriding behavior directly.)
  4. Deploy the fixed data structures along with the new bug-free version of the code (this can be very expensive if the old contract has grown a lot in its storage requirements over time).
  5. Instruct your users to use the new copy of the contract, at the new deployment address (who knows if you will even be able to reach all users who need to know).
  6. Ask your old contract to SELFDESTRUCT, if you even have an API to do that (but adding that sort of API will also reduce user confidence in your contract if they don't trust you).
  7. Hope that all other contracts that need to call your contract (such as exchanges) see your notice that your contract address has changed.
  8. Hope that people trust that the newly deployed contract is the official replacement for the buggy old contract (scammers could probably impersonate the authors of the buggy contract to claim there was a new bugfixed version, and take over the ecosystem).

So you can probably see, proxied contracts are the way to go if you are deploying a contract as a trustworthy/trusted individual or organization, because the above alternative is pretty terrible.

On the flip side, whether or not you are not a trustworthy/trusted individual or organization, if you deploy without a proxy, the chance of an exploitable vulnerability being found in the contract is fairly high, and that may undermine users' confidence in your contract. Therefore not deploying with a proxy may be more anxiety-inducing for users than deploying with a proxy!

If you are not a trustworthy/trusted individual or organization, and you want to avoid a dubious response to deploying your contract via a proxy, then you need to invest in an unbiased security audit of your contract before deploying it. But these can run anywhere from tens to hundreds of thousands of dollars.

If you do get a security audit, Etherscan has a way of submitting the URL to the audit results, which can then be officially associated with your contract, so that people can see it's trustworthy. Although even if you deploy an audited contract, but you deploy with a proxy, there's nothing stopping you from coming back later and swapping out the implementation with one that contains some nefarious bug. (You will lose your Etherscan source verification if you do that though, since the proxied contract will no longer have the same hashcode as the old one you verified with source, unless you submit the nefarious source for verification.)

EtherScan can detect whether a contract is proxied, in many circumstances:

https://medium.com/etherscan-blog/and-finally-proxy-contract-support-on-etherscan-693e3da0714b

https://etherscan.io/proxyContractChecker

When viewing a contract on EtherScan, under the "Contract" tab, if Etherscan knows the contract is proxied, then you will have not just "Read Contract" and "Write Contract", but also "Read Contract via Proxy" and "Write Contract via Proxy" or similar.

If the proxied code has been updated, Etherscan will show you the contract address for the previous version(s) of the code, so that users can examine all versions for changes.

0

Proxy, ERC1967Proxy, Transparent and UUPS in-depth explanation

PS: This is more like a detailed explanation of https://docs.openzeppelin.com/contracts/5.x/api/proxy with multiple examples and implementations. Make sure to understand what a proxy is before reading this.

  • This is our example contract we're going to be using for all the test cases.
contract Bank {

    address owner;
    mapping(address => uint) balance;
   
    constructor() {
        owner = msg.sender;
    }

    function deposit() public payable {
        balance[msg.sender] = msg.value;
    }

    function changeOwner(address _addr) public {
        require(msg.sender == owner);
        owner = _addr;
    }

    function withdraw() public payable {
        msg.sender.call{value: balance[msg.sender]}("");
    }

    function getOwner() public view returns(address) {
        return owner;
    }
    
}
  • Say the bank contract is deployed and a lot of users are using it right now but imagine there come's a scenario where we found a critical vulnerability in the contract or suppose we want to upgrade our contract and add some functionalities; What are we gonna do about it?

Nothing really, smart contracts are immutable by design; meaning after we deploy it there's pretty much nothing we can do.

So there are multiple workarounds for our above scenario.

From: https://ethereum.org/en/developers/docs/smart-contracts/upgrading/

We can see that "Upgrade mechanism #1: Contract migration" is the best we could do rn. Aka creating a new fresh upgraded contract and migrating the entire storage which is obviously going to be a huge and tedious task if the contract has a high user interaction.


To overcome these issues we can use something named a proxy.

From https://docs.openzeppelin.com/contracts/5.x/api/proxy , we can see that there are Proxy, ERC1967Proxy and it's two upgradeability mechanisms transparent and UUPS.

Let's start with a basic Proxy.

Proxy: Abstract contract implementing the core delegation functionality.

Proxy.sol from Openzeppelin:

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Proxy.sol)

pragma solidity ^0.8.20;

abstract contract Proxy {

    function _delegate(address implementation) internal virtual {
        assembly {

            calldatacopy(0, 0, calldatasize())

            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

            returndatacopy(0, 0, returndatasize())

            switch result

            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }


    function _implementation() internal view virtual returns (address);


    function _fallback() internal virtual {
        _delegate(_implementation());
    }


    fallback() external payable virtual {
        _fallback();
    }
}

As we can see that this contract has a fallback function which delegates our call to the logic contract (The bank contract), so all the state variables is going to be stored in this contract and we're going to just use the logic contract only for the code.

Below is the implementation of this:

pragma solidity 0.8;

import "../helpers/Proxy.sol";

contract MyProxy is Proxy {

    address current;

    constructor(address _addr) {
        current = _addr;
    }

    function _implementation() internal view override returns (address) {
        return current;
    }

    receive() external payable { }

}
  • First we deploy our bank contract and deploy the proxy contract with constructor data as the address of the bank contract.

  • The delegatecall function runs the code of the logic contract as it's own code meaning msg.sender and msg.value is going to be the same.

  • So we set our logic contract as our bank contract and give the users this proxy to interact with it. We set the abi of this proxy contract as the one of bank contract.

  • Say if we call deposit() function to the proxy contract. Since there's no function named deposit() in the proxy contract, it directly goes to the fallback function of the proxy contract and from there delegatecall is executed with calldata as deposit(), The proxy contract uses the Bank contract just for the logic but the storage is updated in the proxy contract itself.

But guess what, the above code is actually really really insecure, can you guess why? Yes! Storage collision.

The address current and address owner points the same slot which is slot0.

If we call the changeOwner(address) function the current implementation is going to be replaced by this new address which is completely going to break our contract. So to prevent this something needs to be done.

Can you think of a way? Yes! Store this implementation address in a really random slot which is never going to be overwritten.


EIP1967 Standard

PS: We are going to update the contructor of our logic contract to an init function. You'll see why later.

In order to avoid clashes with the storage variables of the implementation contract behind a proxy, we use EIP1967 storage slots.

The slot of the implementation is stored at:

bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

Which is "The keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1."

Implementation of the contract:

pragma solidity 0.8;

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract MyProxy is ERC1967Proxy {

    constructor(address implementation, bytes memory _data) 
            ERC1967Proxy(implementation, _data) {

    }

}

By default the above contract is not upgradeable. Here's how it works.

  • First the implementation address and the init data is passed to the constructor of the ERC1967 proxy function.
constructor(address implementation, bytes memory _data) payable {
        ERC1967Utils.upgradeToAndCall(implementation, _data);
    }
  • Then the value in slot is updated with the new implementation address.
function _setImplementation(address newImplementation) private {
        if (newImplementation.code.length == 0) {
            revert ERC1967InvalidImplementation(newImplementation);
        }
        StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
    }

    function upgradeToAndCall(address newImplementation, bytes memory data) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);

        if (data.length > 0) {
            Address.functionDelegateCall(newImplementation, data);
        } else {
            _checkNonPayable();
        }
    }
  • Finally the init function is called.
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata);
    }

From openzeppelin docs:

This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed behind a proxy. Since proxied contracts do not make use of a constructor, it’s common to move constructor logic to an external initializer function, usually called initialize.

Now let's update our Bank contract.

contract Bank {

    address owner;
    mapping(address => uint) balance;
    bool private initialized;

    function init(address _owner) public {
        require(!initialized, "Contract instance has already been initialized");
        initialized = true;
        owner = _owner;
    }

    function deposit() public payable {
        balance[msg.sender] = msg.value;
    }

    function withdraw() public payable {
        msg.sender.call{value: balance[msg.sender]}("");
    }

    function changeOwner() public {
        owner = msg.sender;
    }

    function getOwner() public view returns(address) {
        return owner;
    }
    
}

As we can see that our contract now has an init function which is initialized when we first set this implementation to our proxy.

Let's deploy our bank contract and initialize it with proxy contract.

ERC1967Proxy(0x1d72840e4D2E0533c41386986D4Ed3405c171783, 0x19ab453c0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4)

Use https://abi.hashex.org/ to encode according to abi specification.

The most important thing to note here is that the init function updates the slot0(owner variable) of the proxy contract not the actual deployed logic contract because the proxy contract delegatecalls to the init function and not a regular call. So no state variables is updated in the implementation contract.

If we check the slot0 of the proxy contract it points the owner variable.


Just like said above, by default the ERC1967 standard is not upgradeable.

From openzeppelin docs:

There are two alternative ways to add upgradeability to an ERC1967 proxy

TransparentUpgradeableProxy and UUPSUpgradeable


TransparentUpgradeableProxy

One another issue we face in the above standard is that proxy selector clashing can occur; meaning imagine if we had a function with same name in our implementation contract and our proxy contract, what is going to happen then? The one in the proxy contract is going to be called and the one in implementation is ignored.

A major use case of transparentupgradeable proxy is to prevent this.

If any account other than the admin calls the proxy, the call will be forwarded to the implementation.

If the admin calls the proxy, it can call the upgradeToAndCall function but any other call won’t be forwarded to the implementation. If the admin tries to call a function on the implementation it will fail with an error indicating the proxy admin cannot fallback to the target implementation.

Implementation:

pragma solidity 0.8;

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract ProxyChan is TransparentUpgradeableProxy {

    constructor(address implementation, bytes memory _data) 
        TransparentUpgradeableProxy(implementation, msg.sender, _data) {
    }

    function getOwn() public  returns(address) {
        return super._proxyAdmin();
    }

}

constructor(address _logic, address initialOwner, bytes _data)

The most important thing here to note is that a new contract named Proxy admin is going to be deployed and via this contract the initial owner can upgrade the logic address.

The getOwn() function returns the address of the ProxyAdmin smart contract which has it's owner set as the initalowner.

If we try to upgrade using any other account it returns "OwnableUnauthorizedAccount".

These properties mean that the admin account can only be used for upgrading the proxy, so it’s best if it’s a dedicated account that is not used for anything else. This will avoid headaches due to sudden errors when trying to call a function from the proxy implementation. For this reason, the proxy deploys an instance of ProxyAdmin and allows upgrades only if they come through it. You should think of the ProxyAdmin instance as the administrative interface of the proxy, including the ability to change who can trigger upgrades by transferring ownership.


UUPS proxy

In the case of uups proxy the upgrade mechanism is included in the implementation contract itself but not in the proxy contract. Check out the comparison to find out why.

ERC1967 proxy:

import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract MyProxy is ERC1967Proxy {

    constructor(address implementation, bytes memory _data) 
            ERC1967Proxy(implementation, _data) {
    }

}

Bank contract(Our implementation contract) V1:

import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract BankV1 is UUPSUpgradeable {

    address owner;

    mapping(address => uint) balance;
    bool private initialized;

    function _authorizeUpgrade(address implementation) internal override view  {
        require(msg.sender == owner, "Can't upgrade");
    }

    function init(address _owner) public {
        require(!initialized, "Contract instance has already been initialized");
        initialized = true;
        owner = _owner;
    }

    function _disableInit() internal override  {
        initialized = false;
    }

    function deposit() public payable {
        balance[msg.sender] = msg.value;
    }

    function withdraw() public payable {
        msg.sender.call{value: balance[msg.sender]}("");
    }

    function changeOwner() public {
        owner = msg.sender;
    }

    function getOwner() public view returns(address) {
        return owner;
    }
    
}

The initilized variable is temporarily disalbed when upgrading to the new contract, Make sure to use Initializable.sol from openzeppelin to do it in a much more safe and efficient way.

function _disableInit() internal override  {
        initialized = false;
    }
function _disableInit() internal virtual;

Add this line to UUPSUpgradeable.sol and modify the upgradeToAndCall() as:

function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy {
        _disableInit();
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCallUUPS(newImplementation, data);
    }

DOING THIS IS REALLY INSECURE IN A SORT OF WAY, JUST FOR DEOMSTRATION PURPOSE/TO SHOW YOU GUYS THIS IS DONE , MAKE SURE TO USE INITIALIZABLE.SOL

Now our BankV1 has a function named upgradeToAndCall()

Let's upgrade our contract.

BankV2:

import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract BankV2 is UUPSUpgradeable {

    address owner;

    mapping(address => uint) balance;
    bool private initialized;

    function _authorizeUpgrade(address implementation) internal override view  {
        require(msg.sender == owner, "Can't upgrade");
    }

    function init(address _owner) public {
        require(!initialized, "Contract instance has already been initialized");
        initialized = true;
        owner = _owner;
    }

    function bankName() public pure returns(string memory) {
        return "RandomName";
    }

    function _disableInit() internal override  {
        initialized = false;
    }

    function deposit() public payable {
        balance[msg.sender] = msg.value;
    }

    function withdraw() public payable {
        msg.sender.call{value: balance[msg.sender]}("");
    }

    function changeOwner() public {
        owner = msg.sender;
    }

    function getOwner() public view returns(address) {
        return owner;
    }
    
}

Call upgradeToAndCall() to the proxy contract with the new implementation(Bank V2) and data, we can see that our logic contract has been upgraded and if we can call bankName() to our proxy contract it returns the name.


If you have a strong solid understanding of delegatecall() function then implementing proxies is not that of a big deal.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.