Skip to content

Proxy

Description

General Ethereum Proxy Architecture

The typical smart contact Proxy pattern is discussed in depth here and here. This implementation has its own architecture, however, and is not identical to most other proxy contracts.

The Synthetix proxy sits in front of an underlying target contract. Any calls made to the proxy are forwarded to that target contract, so it appears as if the target was called. This is designed to allow a contract to be upgraded without altering its address. In Synthetix, this proxy typically operates in tandem with a Proxyable instance as its target. In this configuration, events are always emitted at the proxy, not at the target, even if the target is called directly.

The Synthetix, Synth, and FeePool contracts all exist behind proxies, which has allowed their behaviour to be substantially altered over time.

This proxy provides two different operation modes,1 which can be switched between at any point.

  • DELEGATECALL: Execution of the target's code occurs in the proxy's context, which preserves the message sender and writes state updates to the storage of the proxy itself. This is the standard proxy style used across most Ethereum projects.
  • CALL: Execution occurs in the target's context, so the storage of the proxy is never touched, but function call and event data, as well as the message sender, must be explicitly passed between the proxy and target contracts. This is the style mainly used in Synthetix.

The motivation for the CALL style was to allow complete decoupling of the storage structure from the proxy, except what's required for the proxy's own functionality. This means there's no necessity for the proxy to be concerned in advance with the storage architecture of the target contract. We can avoid using elaborate or unstructured storage solutions for state variables, and there are no constraints on the use of (possibly nested) mapping or reference types.

Instead of executing the target code in its own context, the CALL-style proxy forwards function call data and ether to the target contract that defines the application logic, which then in turn relays information back to the proxy to be returned to the original caller, or to be emitted from the proxy as events. Some state can be kept on the underlying contract if it can be discarded or it is easy to migrate during contract upgrades. This means that the contract's state is conveniently inspected on block explorers such as Etherscan after the underlying contract code is verified. More elaborate data is kept in separate storage contracts that persist across multiple versions.

This allows the proxy's target contract to be largely disposable. This structure looks something like the following:

Proxy architecture graph

In this way the main contract defining the logic can be swapped out without replacing the proxy or state contracts. The user only ever communicates with the proxy and need not know any implementation details. This architecture also allows multiple proxies with differing interfaces to be used simultaneously for a single underlying contract, though events will usually be emitted only from one of them. This feature is currently used by ProxyERC20, which operates atop the Synthetix contract.

There are some tradeoffs to this approach. There is potentially a little more communication overhead for event emission, though there may be some savings available elsewhere depending on system and storage architecture and the particular application.

At the code level, a CALL proxy is not entirely transparent. Target contracts must inherit Proxyable so that they can read the message sender which would otherwise be the proxy itself rather than the proxy's caller. Additionally, events are a bit different; they must be encoded within the underlying contract and then passed back to the proxy to be emitted. The nuts and bolts of event emission are discussed in the _emit section.

Finally, if the target contract needs to transfer ether around, then it will be remitted from the target address rather than the proxy address, though this is a quirk which it would be straightforward to remedy.

Source: contracts/Proxy.sol

Architecture

Variables

target

Source

The underlying contract this proxy is standing in front of.

Type: contract Proxyable

Constructor

constructor

Source

Initialises the inherited Owned instance.

Details

Signature

constructor(address _owner)

Visibility

public

State Mutability

``

Restricted Functions

_emit

Source

When operating in the CALL style, this function allows the proxy's underlying contract (and only that contract) to emit events from the proxy's address.

Usage

Assuming our event signature is MyEvent(A indexed indexedArg, B data1, C data2), invocation in an underlying contract looks something like the following:

proxy._emit(abi.encode(data1, data2), 2, keccak256('MyEvent(A,B,C)'), bytes32(indexedArg), 0, 0);

In the implementation, such expressions are typically wrapped in convenience functions like emitMyEvent(A indexedArg, B data1, C data2) internal whose signature mirrors that of the event itself.

In Solidity, indexed arguments are published as log topics, while non-indexed ones are abi-encoded together in order and included as data. The keccak-256 hash of the Solidity event signature is always included as the first topic. The format of this signature is EventName(type1,...,typeN), with no spaces between the argument types, omitting the indexed keyword and the argument name. For more information, see the official Solidity documentation here and here.

This function takes 4 arguments for log topics. How many of these are consumed is determined by the numTopics argument, which can take the values from 0 to 4, corresponding to the EVM LOG0 to LOG4 instructions. In the case that an event has fewer than 3 indexed arguments, the remaining slots can be provided with 0. Any excess topics are simply ignored. Note that 0 is a valid argument for numTopics, which produces LOG0, an "event" that only has data and no signature.

Caution

If this proxy contract were to be rewritten with Solidity v0.5.0 or above, it would be necessary to slightly simplify the calls to abi.encode with abi.encodeWithSignature.

See the official Solidity documentation for more discussion. The exact behaviour of the abi encoding functions is defined here.

Details

Signature

_emit(bytes callData, uint256 numTopics, bytes32 topic1, bytes32 topic2, bytes32 topic3, bytes32 topic4)

Visibility

external

State Mutability

``

Modifiers

setTarget

Source

Sets the address this proxy forwards its calls to.

Details

Signature

setTarget(contract Proxyable _target)

Visibility

external

State Mutability

``

Modifiers

Emits

Modifiers

onlyTarget

Source

Reverts the transaction if msg.sender is not the target contract.

Events

TargetUpdated

Source

The proxy's target contract was changed.

Signature: TargetUpdated(contract Proxyable newTarget)


  1. Specific descriptions of the behaviour of the CALL and DELEGATECALL EVM instructions can be found in the Ethereum Yellow Paper