Skip to content

Proxy

Description

General BNB 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 Oikos 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 Oikos, 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 Oikos, 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 BNB 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 Oikos.

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 Oikos 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: Proxy.sol

Architecture


Inheritance Graph

Proxy inheritance graph



Variables


target

The underlying contract this proxy is standing in front of.

Type: Proxyable public


useDELEGATECALL

This toggle controls whether the proxy is in CALL or DELEGATECALL mode. The contract is in DELEGATECALL mode iff useDELEGATECALL is true.

Type: bool public


Functions


constructor

Initialises the inherited Owned instance.

Details

Signature

constructor(address _owner) public

Superconstructors


setTarget

Sets the address this proxy forwards its calls to.

Details

Signature

setTarget(Proxyable _target) external

Modifiers

Emits


setUseDELEGATECALL

Selects which call style to use by setting useDELEGATECALL.

Details

Signature

  • setUseDELEGATECALL(bool value) external

Modifiers


_emit

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, uint numTopics, bytes32 topic1, bytes32 topic2, bytes32 topic3, bytes32 topic4) external

Modifiers

Emits

This function can emit any possible event.


() (fallback function)

If none of the above functions is hit, then the function call data and gas is forwarded to the target contract. The result of that invocation is returned to the message sender.

If the proxy is in DELEGATECALL style, it operates like most other proxies. If it is in CALL mode, then it first calls target.setMessageSender(msg.sender) to initialise the messageSender variable in the underlying Proxyable instance. In addition it forwards any ether included in the transaction to its target.

Details

Signature

() external payable


Modifiers


onlyTarget

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


Events


TargetUpdated

The proxy's target contract was changed.

Signature: TargetUpdated(Proxyable newTarget)


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