Proxy — Part 3, Transparent Proxy

Tushar Bansal
5 min readAug 8, 2023

--

Transparent Proxies are a type of smart contract proxy that allows for the upgradeability of smart contracts while maintaining the same address and interface. They work by using delegatecall to forward all function calls and data to the implementation contract. The proxy contract itself only serves as a thin layer to redirect the calls, while the logic is stored in a separate implementation contract. This enables developers to upgrade the implementation contract without changing the proxy contract or disrupting the existing functionality.
Overall, This pattern places both the state and the ability to upgrade in a proxy contract. The proxy points to a given implementation contract which holds the logic.

Why need of Transparent Proxy?

To avoid proxy selector clashing, which can potentially be used in an attack. This pattern implies two things that go hand in hand:

  1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if that call matches one of the admin functions exposed by the proxy itself.
  2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the implementation. If the admin tries to call a function on the implementation it will fail with an error that says “admin cannot fallback to proxy target”.

Code Example — Github Link:

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

contract CounterV1 {
uint public count;

function inc() external {
count += 1;
}
}

contract CounterV2 {
uint public count;

function inc() external {
count += 1;
}

function dec() external {
count -= 1;
}
}

contract Proxy {
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint(keccak256("eip1967.proxy.implementation")) - 1);

bytes32 private constant ADMIN_SLOT =
bytes32(uint(keccak256("eip1967.proxy.admin")) - 1);

constructor() {
_setAdmin(msg.sender);
}

modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}

function _getAdmin() private view returns (address) {
return StorageSlot.getAddressSlot(ADMIN_SLOT).value;
}

function _setAdmin(address _admin) private {
require(_admin != address(0), "admin = zero address");
StorageSlot.getAddressSlot(ADMIN_SLOT).value = _admin;
}

function _getImplementation() private view returns (address) {
return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
}

function _setImplementation(address _implementation) private {
require(_implementation.code.length > 0, "implementation is not contract");
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = _implementation;
}

function changeAdmin(address _admin) external ifAdmin {
_setAdmin(_admin);
}

function upgradeTo(address _implementation) external ifAdmin {
_setImplementation(_implementation);
}


function admin() external ifAdmin returns (address) {
return _getAdmin();
}

function implementation() external ifAdmin returns (address) {
return _getImplementation();
}


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 _fallback() private {
_delegate(_getImplementation());
}

fallback() external payable {
_fallback();
}

receive() external payable {
_fallback();
}
}

contract ProxyAdmin {
address public owner;

constructor() {
owner = msg.sender;
}

modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}

function getProxyAdmin(address proxy) external view returns (address) {
(bool ok, bytes memory res) = proxy.staticcall(abi.encodeCall(Proxy.admin, ()));
require(ok, "call failed");
return abi.decode(res, (address));
}

function getProxyImplementation(address proxy) external view returns (address) {
(bool ok, bytes memory res) = proxy.staticcall(
abi.encodeCall(Proxy.implementation, ())
);
require(ok, "call failed");
return abi.decode(res, (address));
}

function changeProxyAdmin(address payable proxy, address admin) external onlyOwner {
Proxy(proxy).changeAdmin(admin);
}

function upgrade(address payable proxy, address implementation) external onlyOwner {
Proxy(proxy).upgradeTo(implementation);
}
}

library StorageSlot {
struct AddressSlot {
address value;
}

function getAddressSlot(
bytes32 slot
) internal pure returns (AddressSlot storage r) {
assembly {
r.slot := slot
}
}
}

CounterV1: It is a contract which contains one function “inc”, increases the value of “count” variable.

CounterV2: It is a contract which contains two function “inc” & “dec”, This is going to be our upgraded contract.

Proxy: This is our proxy smart contract, here we have defined our proxy call with some other important functions like “setAdmin”, “getAdmin”, “setImplementation”, “getImplementation”, “changeAdmin”, “upgradeTo”. upgradeTo” is defined under the proxy smart contract.

Modifier of smart contract takes care if msg.sender is admin then it forwards the call as per the condition, caller who is not admin being forwarded to “_fallback()”

Storage slot to store admin address and implementation address is used. “bytes32(uint(keccak256(“eip1967.proxy.implementation”)) — 1)” for implementation address and “bytes32(uint(keccak256(“eip1967.proxy.admin”)) — 1)” for admin address.

ProxyAdmin: This smart contract is only applicable for admin. Point to note is “upgrade” function is inside the proxy. In case of UUPS proxy, “upgrade” function is being transferred to logic contract.

Note: To get the steps to run the contract, follow the video mentioned into the Readme of Github.

Code Example Open Zeppelin— Github Link:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Box {
uint256 private _value;

// Emitted when the stored value changes
event ValueChanged(uint256 value);

// Stores a new value in the contract
function store(uint256 value) public {
_value = value;
emit ValueChanged(value);
}

// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}

}
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BoxUpgrade {
uint256 private _value;

// Emitted when the stored value changes
event ValueChanged(uint256 value);

// Stores a new value in the contract
function store(uint256 value) public {
_value = value;
emit ValueChanged(value);
}

// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}

function inc() public {
_value = _value+1;
}

}
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MyProxy is TransparentUpgradeableProxy {
constructor(
address _logic,
address admin_,
bytes memory _data
) payable TransparentUpgradeableProxy(_logic, admin_, _data) {}
}
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MyProxyAdmin is ProxyAdmin {}

Box: This smart contract is version 1 smart contract that is being deployed first and then being upgraded with BoxUpgrade smart contract which has extra function “inc”.

MyProxy: This smart contract uses the open zeppelin Transparent Upgradeable library. It is easy to use, just import the library and access the “TransparentUpgradeableProxy” keyword as interface. Make sure to create a constructor to pass “_logic” which is logic contract (Box)’s address, “admin_” which is the address of admin you wants to create, “_data” as the data which is the parameters of constructor fuction of logic contract. If there is no parameters needed to pass then pass empty value.

ProxyAdmin: It is to create the Proxy admin. Just import the open zeppelin proxyAdmin library and use proxyAdmin keyword as interface.

Note: To get the steps to run the contract, follow the video mentioned into the Readme of Github.

Overall, Transparent Proxy comes with many features and makes it easy to work. It is more secure and gas optimised than others but it has its own drawback.

Because the function selectors use a fixed amount of bytes, there will always be the possibility of a clash. This isn’t an issue for day to day development, given that the Solidity compiler will detect a selector clash within a contract, but this becomes exploitable when selectors are used for cross-contract interaction. Clashes can be abused to create a seemingly well-behaved contract that’s actually concealing a backdoor.

Currently to solve that problem, we have separate Admin and User , where the admin can only call the functions of proxy and other than admin any other function call is being forwarded to logic contract. But that too creates the whole process costly. UUPS solves the problem in better way.

--

--