diff --git a/exchange-proxy/exchange-proxy.md b/exchange-proxy/exchange-proxy.md new file mode 100644 index 0000000..40a0a47 --- /dev/null +++ b/exchange-proxy/exchange-proxy.md @@ -0,0 +1,351 @@ +# ZeroEx (Exchange Proxy) Spec + +## Motivation + +Pretty soon after V3 + staking were launched we started seeing pain points from integrations and migrations. Many of these issues required on-chain solutions to sort out. For some, we were able to (mostly) satisfy requirements with extension contracts and ERC20 bridge contracts. But there are still many cases where neither of these approaches provide elegant solutions, and the only real solution would be to amend the Exchange contract itself. However, the Exchange contract is monolithic and not designed to be upgradeable, so changes to one part of the contract would require deploying a new instance and migrating integrators to this new contract. It’s also highly likely that new issues would appear as the ecosystem matured and we’d have to make more changes, leading to more migrations, etc. + +At the same time, we have our mind on governance. Ideally, we’d like governance to be binding on a per-feature basis. But with a monolithic model, we’d always be deploying the full Exchange feature-set in one swoop, so people would technically always be voting on an entire system. This has the effect of reducing participation because it’s very difficult to engage and inform voters on a complex collection of of features vs just a handful of related ones. + +## The Proxy Contract: ZeroEx + +The ZeroEx contract implements a per-function proxy pattern. Every function registered to the proxy contract can have a distinct implementation contract. Implementation contracts are called “features” and can expose multiple, related functions. Since features can be upgraded independently, there will no longer be a collective “version” of the API, defaulting to a rolling release model. + +![exchange-proxy-call-forwarding](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/exchange-proxy-call-forwarding.png) + +The ZeroEx contract’s only responsibility is to route (delegate) calls to per-function implementation contracts through its fallback. + +```solidity +contract ZeroEx { + + // Storage bucket for the proxy. + struct ProxyStorage { + // Mapping of function selector -> function implementation + mapping(bytes4 => address) impls; + } + + // Construct this contract. + constructor() public { + // Temporarily register the `bootstrap()` function. + ProxyStorage.impls[IBootstrap.bootstrap.selector] = + address(new Bootstrap(msg.sender)); + } + + fallback() external payable { + address impl = getFunctionImplementation(msg.data.readBytes4()); + require(impl != address(0)); + + // Forward the call. + (bool success, bytes memory result) = impl.delegatecall(msg.data); + if (!success) { + revertData(result); + } + returnData(result); + } + + receive() external payable {} + + // Get the implementation of a function selector. + function getFunctionImplementation(bytes4 selector) + public view returns (address) + { + return ProxyStorage.impls[msg.data.readBytes4()]; + } +} +``` + +## Bootstrapping + +The ZeroEx contract comes pre-loaded with only one feature: Bootstrap. This exposes a `bootstrap()` function that can only be called by the deployer. `bootstrap()` does a few things: + +1. De-register the `bootstrap()` function, which prevents it being called again. +2. Self-destruct. +3. Delegatecall the bootstrapper target contract and call data. + +This is a stripped down Bootstrap feature contract: + + +```solidity +contract Bootstrap { + // The ZeroEx contract. + address immutable private _deployer; + // The implementation address of this contract. + address immutable private _implementation; + // The allowed caller to `bootstrap()`. + address immutable private _bootstrapCaller; + + constructor(address bootstrapCaller) public { + _deployer = msg.sender; + _implementation = address(this); + _bootstrapCaller = bootstrapCaller; + } + + // Execute a bootstrapper in the context of the proxy. + function bootstrap(address target, bytes callData) external { + // Only the bootstrap caller can call this function. + require(msg.sender == _bootstrapCaller); + // Deregister. + LibProxyStorage.getStorage().impls[this.bootstrap.selector] = address(0); + // Self-destruct. + Bootstrap(_implementation).die(); + // Call the bootstrapper. + target.delegatecall(callData); + } + + function die() external { + require(msg.sender == _deployer); + selfdestruct(msg.sender); + } +} +``` + + +This is the basic execution flow using our deployment migration contract (InitialMigration), which acts as both the deployer and bootstrapper: +![exchange-proxy-bootstrap](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/exchange-proxy-bootstrap.png) +## Function Registry Management + +One of the initial features InitialMigration bootstraps into the ZeroEx contract is the function registry feature, SimpleFunctionRegistry. This feature exposes the following function registry management features: + +* `extend()` - Register a new function (selector) and implementation (address). This also maintains a history of past implementations so we can roll back to one, if needed. +* `rollback()` - Reverts a function implementation to a prior version in its history. + +```solidity +contract SimpleFunctionRegistry { + + // Storage bucket for this feature. + struct SFRStorage { + // Mapping of function selector -> implementation history. + mapping(bytes4 => address[]) implHistory; + } + + // Roll back to the last implementation of a function. + function rollback(bytes4 selector) + external + onlyOwner + { + address[] storage history = SFRStorage.implHistory[selector]; + require(history.length > 0); + ProxyStorage.impls[selector] = history[history.length - 1]; + history.pop(); + } + + // Register or replace a function. + function extend(bytes4 selector, address impl) + external + onlyOwner + { + _extend(selector, impl); + } + + // Register or replace a function. + function _extend(bytes4 selector, address impl) + private + { + address[] storage history = SFRStorage.implHistory[selector]; + history.push(ProxyStorage.impls[selector]); + ProxyStorage.impls[selector] = impl; + } +} +``` + +## Ownership + +Another feature InitialMigration bootstraps into the proxy is the Ownable feature. This exposes ownership management functions: `transferOwner`ship`()` and `getOwner()`. This feature also enables ubiquitous modifiers such as `onlyOwner`, so it is an implicit dependency of nearly every other feature. + + +```solidity +contract Ownable { + + // Storage bucket for this feature. + struct OwnableStorage { + // The owner of this contract. + address owner; + } + + // Change the owner of this contract. + function transferOwnership(address newOwner) + external + onlyOwner + { + OwnableStorage.owner = newOwner; + } + + // Get the owner of this contract. + function getOwner() external view returns (address owner_) { + return OwnableStorage.owner; + } +} +``` + +## Migrations + +Migrations are upgrade logic that run in the context of the proxy contract. To do this, the owner calls the `migrate()` function, provided by the Ownable feature. This follows a similar sequence as the bootstrap process. Notably, it temporarily sets the owner of the contract to itself for the duration of the migration call, which allows the migrator to perform admin-level operations through other features, such as registering or rolling back new functions (see [Function Registry Management](https://0xproject.quip.com/DudoAmHvpLaB#eADACA29a1H)). Before exiting, the owner is set to the `newOwner`, which is passed in to the call. + +One motivation for the existence of this function, as opposed to just having the make individual admin calls, is a shortcoming of the ZeroExGoverner contract, which is designed to perform one operation at a time, with no strict ordering of those operations. + +This is a stripped down `migrate()` feature implementation: + +```solidity +contract Ownable { + + // Execute a migration function in the context of the proxy contract. + function migrate(address target, bytes calldata data, address newOwner) + external + onlyOwner + { + // If the owner is already set to ourselves then we've reentered. + require(OwnableStorage.owner != address(this)); + // Temporarily set the owner to ourselves. + OwnableStorage.owner = address(this); + + // Perform the migration. + target.delegatecall(data); + + // Set the new owner. + OwnableStorage.owner = newOWner; + } +} +``` + + +This is an example sequence of a migration: + +![zero_ex_migrate](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/zero_ex_migrate.png) +## Storage Buckets + +Because feature functions get delegatecalled into, they all share the same execution context and, thus, state space. It’s critical that storage for each feature be compartmentalized from other features to avoid accidentally writing to the same slot. We solve this by strictly adhering to a storage bucket pattern for our feature contracts. This rule also extends to all inherited contracts/mixins. + +Storage buckets are enabled by new language features in solidity 0.6, which allow us to rewrite a storage variable’s slot reference to a globally unique ID. These IDs are stored in an *append-only* enum defined in `LibStorage`, to enforce uniqueness. The true storage slot for a bucket is the feature’s storage ID multiplied by a large constant to prevent overlap between buckets. + +Example: + +```solidity +LibStorage { + enum StorageId { + MyFeature + } + + function getStorageSlot(StorageId id) internal pure returns (uint256) { + return uint256(id) * 1e18; + } +} + +LibMyFeatureStorage { + // Storage layout for this feature. + struct Storage { + mapping(bytes32 => bytes) myData; + } + + // Get the storage bucket for this feature. + function getStorage() internal view returns (Storage storage st) { + uint256 slot = LibStorage.getStorageSlot( + LibStorage.StorageId.MyFeature + ); + assembly { st_slot := slot } + } +} +``` + +With the above pattern, writing to storage is simply: + +```solidity +LibMyFeatureStorage.getStorage().myData[...] = ...; +``` + +## Version Management + +### Inspection + +This is a rolling release model, where every feature/function has its own version. All feature contracts (except Bootstrap because it’s ephemeral), implement the `IFeature` interface: + +```solidity +interface IFeature { + // The name of this feature set. + function FEATURE_NAME() external view returns (string memory name); + + // The version of this feature set. + function FEATURE_VERSION() external view returns (uint256 version); +} +``` + +So, to get the version of a function one could do `IFeature(getFunctionImplementation(foo.selector)).FEATURE_VERSION`. + +### Best Practices + +The registry is intentionally not prescriptive on how features should be migrated. But there are some general best practices we can follow to avoid harming users, and ourselves. + +**Deprecation** + +In general, unless a function has a vulnerability, we should keep it intact for the duration of the deprecation schedule. Afterwards, we can ultimately disable the function by either calling `extend()` with a null implementation or by calling `rollback()` to a null implementation. + +**Patches** + +These include bug-fixes, optimizations, or any other changes that preserve the intended behavior of the function. For these cases, we should upgrade the function in-place, i.e., using the same selector but changing the implementation contract, through `extend()`. + +**Vulnerabilities** + +If a vulnerability is found in a live function, we should call `rollback()` immediately to reset it to a non-vulnerable implementation. Because `rollback()` is a separate function from `extend()`, it can be exempted from timelocks to allow a swift response. + +**Upgrades** + +These involve meaningful behavioral changes, such as new settlement logic, changes to the order format (or its interpretation), etc. These should always be registered under a new selector, which comes free if the arguments also change, to allow users the opportunity to opt-in to new behavior. If the upgrade is intended to replace an existing feature, the old version should follow a deprecation schedule, unless we’re confident no one is using it. + +**Features used by features** + +Not all features are designed to be exclusively consumed by the public. We can have internal features by applying an `onlySelf` modifier to the function. We need to be mindful of another class of user: the contract itself. Avoiding missteps on this will require a combination of diligence and good regression test suites. + +## Known Risks + +The extreme flexibility of this model means we have few built-in guardrails that more conventional architectures enjoy. To avoid pitfalls, we’ve established a few new patterns to follow during development, but the following areas will always need careful scrutiny during code reviews. + +### Extended Attack Surface for Features + +Because features all run in the same execution context, they inherit potential vulnerabilities from other features. Some vulnerabilities may also arise from the interactions of separate features, which may not be obvious without examining the system as a whole. Reviewers will always need to be mindful of these scenarios and features should try to create as much isolation of responsibilities as possible. + +### Storage Layout Risks + +All features registered to the proxy will run in the same storage context as the proxy itself. We employ a pattern of per-feature storage buckets (structs) with globally unique bucket slots to mitigate issues. + +**Slot Overlap** + +Every time we develop a new feature, an entry is appended to the `LibStorage.StorageId` enum, which is the bucket ID for the feature’s storage. This applies to the storage used by the proxy contract itself. When calculating the true slot for the storage bucket, this enum value is offset by `1` and bit shifted by `128`: + + +```solidity +function getStorageSlot(StorageId id) internal pure returns (uint256) { + return (uint256(id) + 1) << 128; +} +``` + + +Given Solidity’s storage layout rules (https://solidity.readthedocs.io/en/v0.6.6/miscellaneous.html), subsequent storage buckets should always be 2^128 slots apart, which means buckets can have 2^128 flattened inline fields before overlapping. While it’s not impossible for buckets to overlap with this pattern, it should be extremely unlikely if we follow it closely. Maps and arrays are not stored sequentially but should also be affected by their base slot value to make collisions unlikely. + +**Inherited Storage** + +A more insidious way to corrupt storage buckets is to have a feature unintentionally inherit from a mixin that has plain (non-bucketed) state variables, because the mixin can potentially read/write to slots shared by other buckets through them. To avoid this: + +1. We prefix all feature-compatible mixins with “Fixin” (“Feature” + “Mixin”) and only allow contract inheritance from these. +2. Storage IDs are offset by 1 before computing the slot value. This means the first real storage bucket will actually start at slot `2^128`, which gives us a safety buffer for these scenarios, since it’s unlikely a mixin would unintentionally access slots beyond `2^128`. + +**Shared Access to Storage** + +There is nothing stopping a feature from reaching into another feature’s storage bucket and reading/modifying it. Generally this pattern is discouraged but may be necessary in some cases, or may be preferable to save gas. This can create an implicit tight coupling between features and we need to take those interactions into account when upgrading the features that own those storage buckets. + +**Restricted Functions and Privilege Escalation** + +We will also be registering functions that have caller restrictions. Functions designed for internal use only will have an `onlySelf` modifier that asserts that `msg.sender == address(this)`. The other class of restricted functions are owner-only functions, which have an `onlyOwner` modifier that asserts that the `msg.sender == LibOwnableStorage.Storage.owner`. + +The check on owner-only functions can be easily circumvented in a feature by directly overwriting `LibOwnableStorage.Storage.owner` with another address. If best practices and patterns are adhered to, doing so would involve deliberate and obvious effort and should be caught in reviews. + +**Self-Destructing Features** + +A feature contract with self-destruct logic must safeguard this code path to only be executed after the feature is deregistered, otherwise its registered functions will fail. In most cases this would just cause the feature to temporarily go dark until we could redeploy it. But it may leave the proxy in an unusable state if this occurs in the contract of a mission-critical feature, e.g., Ownable or SimpleFunctionRegistry (neither of which can self-destruct). + +Features should also be careful that `selfdestruct` is never executed in the context of the proxy to avoid destroying the proxy itself. + +**Allowances** + +Although the proxy will not have access to the V3 asset proxies initially, early features will require taker allowances to be accessible to the proxy somehow. Instead of having the proxy contract itself be the allowance target, we intend on using a separate “Puppet” contract, callable only by the proxy contract. This creates a layer of separation between the proxy contract and allowances, so moving user funds is a much more deliberate action. In the event of a major vulnerability, the owner can simply detach the puppet contract from the proxy. This also avoids the situation where the proxy has lingering allowances if we decide grant it asset proxy authorization. + +**Balances** + +Inevitably, there will be features that will cause the Exchange Proxy to hold temporary balances (e.g., payable functions). Thus, it’s a good idea that no feature should cause the Exchange Proxy to hold a permanent balance of tokens or ether, since these balances can easily get mixed up with temporary balances. diff --git a/exchange-proxy/features/token-spender.md b/exchange-proxy/features/token-spender.md new file mode 100644 index 0000000..b9d0e71 --- /dev/null +++ b/exchange-proxy/features/token-spender.md @@ -0,0 +1,24 @@ +# TokenSpender + +We need a way to transfer funds from the taker into the Exchange proxy temporarily so we can perform transformations on it. We could simply have the taker set an allowance for the Exchange proxy, but this comes with some risk: + +* Lack of separation of complex logic vs access to funds. +* No easy way to detach allowances in case of a critical vulnerability. +* Allowances do not migrate if we ever redeploy the Exchange proxy. +* Dangling allowances if we decide to migrate the Exchange proxy to use the V3 asset proxies for allowances. + +So we opt for something akin to the role of asset proxies in V3, where takers must set their allowance to a separate, specialized spender contract accessible by the Exchange proxy. However, instead of having a single allowance contract for each asset type, we instead have a **single**, universal spender contract. Takers can get the address of this contract with `getAllowanceTarget()`. + +This universal allowance contract has a single `executeCall()` function which blindly `call`s calldata on the caller’s (Exchange Proxy) behalf. This contract is akin the `FlashWallet` used by `transformERC20()` with some notable differences: + +* Implements the `Authorizable` mixin. + * Owner will be set to the governor. + * The `ZeroEx` contract will be the sole authorized address. +* Cannot perform a `delegatecall`. Instead exposes an (authority-only) `executeCall()` function that simply forwards a call. +* Is never intended to hold a balance. + +Through `executeCall()`, the Exchange proxy can call `transferFrom()` in the `AllowanceTarget`’s context to move taker funds. A dedicated feature, `TokenSpender`, will wrap this functionality with convenience functions such as `_spendERC20Tokens(token, from to, amount)`. + +![token-spender](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/token-spender.png) + +The `AllowanceTarget` instance is owned by the governor contract, so it can be detached in an emergency or a migration. diff --git a/exchange-proxy/features/transform-erc20.md b/exchange-proxy/features/transform-erc20.md new file mode 100644 index 0000000..767c3af --- /dev/null +++ b/exchange-proxy/features/transform-erc20.md @@ -0,0 +1,159 @@ +# Arbitrary ERC20 Transformations + +## Background + +Integrations have taught us that there is no one-size-fits-all solution to a swap function. Different integrators will have different needs. Some prefer to interact with ETH over WETH, others want to collect affiliate fees, and we may see some that want to interact using wrapped tokens (cUSDC and the like). + +## The `transformERC20()` Function + +A theme common to most of these wants, including a simple market buy/sell, is that a taker is trying to transform X input tokens into Y output tokens. With this understanding, we can create a generalized `transformERC20()` function that only does the following: + +1. Take X input tokens from the taker. +2. Perform arbitrary transformations (provided by the taker) on the tokens we hold. +3. Validate that the taker has received Y output tokens. + +We don’t particularly care what goes on inside the transformations so long as the taker winds up receiving the required amount of output tokens at the end of it. This is similar to how ERC20Bridges operate, in that the `ERC20BridgeProxy` does not know what the bridges are doing, but asserts at the end that the taker has received enough maker tokens + +### Transformers + +We define each of our desired transformations as “Transformer” contracts. Similar to bridge contracts, transformers typically receive tokens and produce some kind of “output” token (but aren’t required to do either of these). Some example transformers we would have: + +* WethAbstractionTransformer: + * If WETH is received, unwrap to ETH + * If ETH is received, wrap to WETH +* AffiliateFeeTransformer: + * Transfers % of the tokens received to an affiliate address. +* ProtocolFeeBrokerTransformer: + * Converts % of the tokens received into ETH, possibly by executing another swap. +* PayTakerTransformer: + * Transfers tokens directly to the taker. +* FillQuoteTransformer: + * Perform a market buy/sell on a set of orders, most likely generated by 0x-API. +* CompoundTokenTransformer: + * If DAI is received, convert to cDAI + * If USDC is received, convert to cUSDC + * If cDAI is received, convert to DAI + * if cUSDC is received, convert to USDC + +### Flash Wallets + +A big difference from bridge contracts is that we `delegatecall` into transformers instead of performing a regular `call`. This is to avoid the massive gas overhead that would be incurred if we had to transfer intermediate tokens to each transformer contract. However, we don’t want to perform the `delegatecall` in the context of the Exchange proxy because that would give transformers full control over the Exchange proxy. + +So instead we use a middle-man contract called a `FlashWallet`. The wallet contract will hold all intermediate token balances and perform `delegatecall`s to the individual transformers, in a context separate from the Exchange proxy. The `TransformERC20` feature will hold a single `FlashWallet` contract instance which can be reused by `transformERC20()` all operations. + +The wallet instance can be (re)created with `createTransformWallet()`, which is callable by the owner/governor. This allows us to deploy a fresh wallet in case we somehow break the old one, like if we accidentally `selfdestruct` it or clobber its state. + +The wallet also works with ERC1155 and ERC223 tokens by implementing the required fallbacks for those standards. This allows transformers to hold ERC1155 and ERC223 intermediate assets. If we decide to add support for more tokens, we will have to upgrade the `TransformERC20` feature since the FlashWallet bytecode is hardcoded into it. + +### Transformation Pipeline + +`transformERC20()` executes each Transformer in sequence, manipulating the token balance held by the wallet. The final transformer will simply transfer the output token directly to the taker. This creates a composable pipeline of operations/transitions. + +![transform-pipeline](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/transform-pipeline.png) + +For example, if a taker wanted to do the following: + +1. Pay ETH +2. Convert to WETH +3. Fill a WETH → USDC quote +4. Pay affiliate fee in USDC +5. Wrap the USDC in cUSDC + +It might look like: + + +![transform-example](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/transform-example.png) + +### Locking Down Transformers + +Transformers need to be permissioned since they’re taking control of the wallet instance. Though no loss of funds should be able to occur in otherwise, opening up wallets to arbitrary transformers could lead to griefing by transformers rendering the wallets unusable (e.g., `selfdestruct`). + +If we maintained a registry of allowed Transformer contracts on the Exchange proxy, changes would likely have to go through the governor and be subject to timelocks, which defeats the nimbleness aspect of this entire architecture. This also incurs an `SLOAD`. Instead, we can adopt the pattern of using a single, known deployer address to deploy all Transformers. Instead of taking an address of a transformer, the `transformERC20()` function takes a “deployment nonce,” which is the nonce the trusted deployer had when deploying the transformer. The address of the transformer will be derived from this value. + +A consequence of this approach is that a transformer will always be valid. If the transformer can render the state of the `FlashWallet` invalid, it can be perpetually used to grief the system. The recommendation here is to expose a self-destructing `die()` function on all transformers which: + +* Is only callable by the deployer/owner. +* Checks that `address(this) == _implementation` to ensure it always self-destructs in its own context. + +## Key Benefits + +* Composable flexibility + * Every integrator/taker has different needs. Allowing the taker to compose their own operations means they’re more likely to get the behavior they want. +* Rapid development + * By separating out transformation logic from the Exchange proxy, we can iterate on these operations much more quickly. +* Peace-of-mind + * By asserting that the taker has received the required amount of output tokens at the end, we can treat transformers as black boxes and have reasonable certainty that the taker is not being swindled. +* Novel use cases + * The pluggable nature of this function means it is not limited to fills. One could use it to perform many common DeFi operations in a single transaction. + +### + +## Implementation + +The feature interface: + +```solidity +interface ITransformERC20 { + + /// @dev Defines a transformation to run in `transformERC20()`. + struct Transformation { + // The deployment nonce of the transformer. + // This is the nonce the deployer had when deploying the transformer. + // The address of the transformer will be derived from this value. + uint32 deploymentNonce; + // Arbitrary data to pass to the transformer. + bytes data; + } + + /// @dev Executes a series of transformations to convert an ERC20 `inputToken` + /// to an ERC20 `outputToken`. + /// @param inputToken The token being provided by the sender. + /// If `0xeee...`, ETH is implied and should be provided with the call.` + /// @param outputToken The token to be acquired by the sender. + /// `0xeee...` implies ETH. + /// @param inputTokenAmount The amount of `inputToken` to take from the sender. + /// May be `uint256(-1)` to indicate the maximum transferrable. + /// @param minOutputTokenAmount The minimum amount of `outputToken` the sender + /// must receive for the entire transformation to succeed. + /// @param transformations Sequence of transformations to apply to the token + /// balances. + /// @return outputTokenAmount The amount of `outputToken` received by the sender. + function transformERC20( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + uint256 minOutputTokenAmount, + Transformation[] calldata transformations + ) + external + payable + returns (uint256 outputTokenAmount); +} +``` + +The transformer interface: + +```solidity +/// @dev A transformation callback used in `TransformERC20.transformERC20()`. +interface IERC20Transformer { + + /// @dev Called from `TransformERC20.transformERC20()`, AFTER the requested + /// ERC20 tokens have been transferred to this contract. + /// If ETH is requested, it will be attached to this call. + /// Some or all of the caller's balance of requested tokens/ETH will be + /// transferred, and any unused tokens/ETH should be returned to + /// `msg.sender` before returning. + /// @param callDataHash The hash of the `TransformERC20.transformERC20()` calldata. + /// @param taker The taker address (caller of `TransformERC20.transformERC20()`). + /// @param data Arbitrary data to pass to the transformer. + /// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`). + function transform( + bytes32 callDataHash, + address taker, + bytes calldata data + ) + external + payable + returns (bytes memory rlpDeploymentNonce); +} +``` diff --git a/exchange-proxy/img/exchange-proxy-bootstrap.png b/exchange-proxy/img/exchange-proxy-bootstrap.png new file mode 100644 index 0000000..a23e485 Binary files /dev/null and b/exchange-proxy/img/exchange-proxy-bootstrap.png differ diff --git a/exchange-proxy/img/exchange-proxy-call-forwarding.png b/exchange-proxy/img/exchange-proxy-call-forwarding.png new file mode 100644 index 0000000..6cb757c Binary files /dev/null and b/exchange-proxy/img/exchange-proxy-call-forwarding.png differ diff --git a/exchange-proxy/img/token-spender.png b/exchange-proxy/img/token-spender.png new file mode 100644 index 0000000..6a5ce9a Binary files /dev/null and b/exchange-proxy/img/token-spender.png differ diff --git a/exchange-proxy/img/transform-example.png b/exchange-proxy/img/transform-example.png new file mode 100644 index 0000000..ec2ab0b Binary files /dev/null and b/exchange-proxy/img/transform-example.png differ diff --git a/exchange-proxy/img/transform-pipeline.png b/exchange-proxy/img/transform-pipeline.png new file mode 100644 index 0000000..a7e9887 Binary files /dev/null and b/exchange-proxy/img/transform-pipeline.png differ diff --git a/exchange-proxy/img/zero_ex_migrate.png b/exchange-proxy/img/zero_ex_migrate.png new file mode 100644 index 0000000..cb9db88 Binary files /dev/null and b/exchange-proxy/img/zero_ex_migrate.png differ