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.
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:
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
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
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.
The underlying contract this proxy is standing in front of.
Initialises the inherited
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.
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.
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
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.
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
See the official Solidity documentation for more discussion. The exact behaviour of the abi encoding functions is defined here.
_emit(bytes callData, uint256 numTopics, bytes32 topic1, bytes32 topic2, bytes32 topic3, bytes32 topic4)
Sets the address this proxy forwards its calls to.
setTarget(contract Proxyable _target)
Reverts the transaction if
msg.sender is not the
The proxy's target contract was changed.
TargetUpdated(contract Proxyable newTarget)
Specific descriptions of the behaviour of the
DELEGATECALLEVM instructions can be found in the Ethereum Yellow Paper. ↩