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:
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¶
Related Contracts¶
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.
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
.
_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)
-
Specific descriptions of the behaviour of the
CALL
andDELEGATECALL
EVM instructions can be found in the BNB Yellow Paper. ↩