🎓 A1Learner
🔍
0 of 45 lessons completed

Solidity Transparent vs UUPS

A transparent proxy is a proxy that handles admin calls itself and forwards only user calls to the logic contract. UUPS works the other way, by forwarding every call to the logic contract.

A transparent proxy routes admin and user calls differently

The admin upgrades through the proxy itself. A user sets a value through the forwarded logic, while an admin logic call reverts.

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

contract Counter {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}

contract CounterV2 {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
    function increment() public {
        value += 1;
    }
}

contract TransparentProxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
        bytes32 adminSlot = ADMIN_SLOT;
        address deployer = msg.sender;
        assembly { sstore(adminSlot, deployer) }
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function admin() public view returns (address adm) {
        bytes32 slot = ADMIN_SLOT;
        assembly { adm := sload(slot) }
    }
    function upgrade(address impl) public {
        require(msg.sender == admin(), "only admin");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    fallback() external payable {
        require(msg.sender != admin(), "admin cannot call logic");
        address impl = implementation();
        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()) }
        }
    }
}

A UUPS proxy forwards the upgrade call to the logic

The owner upgrades by calling the logic function through the proxy. A stranger upgrade reverts in the logic guard.

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

contract Counter {
    uint256 public value;
    address public owner;
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    function initialize(address admin) public {
        require(owner == address(0), "already initialized");
        owner = admin;
    }
    function setValue(uint256 v) public {
        value = v;
    }
    function proxiableUUID() public pure returns (bytes32) {
        return IMPL_SLOT;
    }
    function upgradeTo(address newImpl) public {
        require(msg.sender == owner, "only owner");
        (bool ok, bytes memory ret) = newImpl.staticcall(abi.encodeWithSignature("proxiableUUID()"));
        require(ok && ret.length == 32 && abi.decode(ret, (bytes32)) == IMPL_SLOT, "not a UUPS contract");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, newImpl) }
    }
}

contract CounterV2 {
    uint256 public value;
    address public owner;
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    function initialize(address admin) public {
        require(owner == address(0), "already initialized");
        owner = admin;
    }
    function setValue(uint256 v) public {
        value = v;
    }
    function increment() public {
        value += 1;
    }
    function proxiableUUID() public pure returns (bytes32) {
        return IMPL_SLOT;
    }
    function upgradeTo(address newImpl) public {
        require(msg.sender == owner, "only owner");
        (bool ok, bytes memory ret) = newImpl.staticcall(abi.encodeWithSignature("proxiableUUID()"));
        require(ok && ret.length == 32 && abi.decode(ret, (bytes32)) == IMPL_SLOT, "not a UUPS contract");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, newImpl) }
    }
}

contract UupsProxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    fallback() external payable {
        address impl = implementation();
        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()) }
        }
    }
}

The same admin call succeeds on UUPS and reverts on transparent

One logic contract sits behind both proxies. The admin sets a value through UUPS, but the same call through the transparent proxy reverts.

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

contract Counter {
    uint256 public value;
    function setValue(uint256 v) public {
        value = v;
    }
}

contract TransparentProxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
        bytes32 adminSlot = ADMIN_SLOT;
        address deployer = msg.sender;
        assembly { sstore(adminSlot, deployer) }
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function admin() public view returns (address adm) {
        bytes32 slot = ADMIN_SLOT;
        assembly { adm := sload(slot) }
    }
    function upgrade(address impl) public {
        require(msg.sender == admin(), "only admin");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    fallback() external payable {
        require(msg.sender != admin(), "admin cannot call logic");
        address impl = implementation();
        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()) }
        }
    }
}

contract UupsProxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    fallback() external payable {
        address impl = implementation();
        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()) }
        }
    }
}

Example

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

contract VaultV1 {
    uint256 public saved;
    address public owner;
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    function initialize(address admin) public {
        require(owner == address(0), "already initialized");
        owner = admin;
    }
    function addSaved(uint256 v) public {
        saved += v;
    }

    function proxiableUUID() public pure returns (bytes32) {
        return IMPL_SLOT;
    }
    function upgradeTo(address newImpl) public {
        require(msg.sender == owner, "only owner");
        (bool ok, bytes memory ret) = newImpl.staticcall(abi.encodeWithSignature("proxiableUUID()"));
        require(ok && ret.length == 32 && abi.decode(ret, (bytes32)) == IMPL_SLOT, "not a UUPS contract");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, newImpl) }
    }
}

contract VaultV2 {
    uint256 public saved;
    address public owner;
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    function initialize(address admin) public {
        require(owner == address(0), "already initialized");
        owner = admin;
    }
    function addSaved(uint256 v) public {
        saved += v;
    }
    function doubleSaved() public {
        saved *= 2;
    }

    function proxiableUUID() public pure returns (bytes32) {
        return IMPL_SLOT;
    }
    function upgradeTo(address newImpl) public {
        require(msg.sender == owner, "only owner");
        (bool ok, bytes memory ret) = newImpl.staticcall(abi.encodeWithSignature("proxiableUUID()"));
        require(ok && ret.length == 32 && abi.decode(ret, (bytes32)) == IMPL_SLOT, "not a UUPS contract");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, newImpl) }
    }
}

contract TransparentProxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
    constructor(address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
        bytes32 adminSlot = ADMIN_SLOT;
        address deployer = msg.sender;
        assembly { sstore(adminSlot, deployer) }
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    function admin() public view returns (address adm) {
        bytes32 slot = ADMIN_SLOT;
        assembly { adm := sload(slot) }
    }
    function upgrade(address impl) public {
        require(msg.sender == admin(), "only admin");
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    fallback() external payable {
        require(msg.sender != admin(), "admin cannot call logic");
        address impl = implementation();
        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()) }
        }
    }
}

contract UupsProxy {
    bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    constructor(address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { sstore(slot, impl) }
    }
    function implementation() public view returns (address impl) {
        bytes32 slot = IMPL_SLOT;
        assembly { impl := sload(slot) }
    }
    fallback() external payable {
        address impl = implementation();
        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()) }
        }
    }
}

Key points

  • Transparent routing: admin calls stay in the proxy, and user calls forward to the logic.
  • Transparent clash rule: an admin calling a logic function through a transparent proxy reverts, so admin and logic functions can never collide.
  • UUPS routing: a UUPS proxy forwards every call, including the upgrade call, to the logic.
  • UUPS trust: UUPS trusts the logic contract to guard its own upgrade function.
  • Proxy size: a transparent proxy carries its own upgrade code, while a UUPS proxy stays smaller because the upgrade code lives in the logic.

In this example, VaultV1 and VaultV2 are UUPS logic contracts, and one vault of each proxy type points at them. The transparent vault gains 30 and the UUPS vault gains 40, then each proxy upgrades its own way, and both vaults keep their values and double them to 60 and 80.

Note

both patterns keep state in the proxy, so an upgrade never moves stored values.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

Which proxy pattern handles admin calls in the proxy itself?