Skip to content

Commit ef27887

Browse files
committed
add EP specs
1 parent 2392ba7 commit ef27887

8 files changed

Lines changed: 536 additions & 0 deletions

exchange-proxy/exchange-proxy.md

Lines changed: 351 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Arbitrary ERC20 Transformations
2+
3+
## Background
4+
5+
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).
6+
7+
## The `transformERC20()` Function
8+
9+
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:
10+
11+
1. Take X input tokens from the taker.
12+
2. Perform arbitrary transformations (provided by the taker) on the tokens we hold.
13+
3. Validate that the taker has received Y output tokens.
14+
15+
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
16+
17+
### Transformers
18+
19+
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:
20+
21+
* WethAbstractionTransformer:
22+
* If WETH is received, unwrap to ETH
23+
* If ETH is received, wrap to WETH
24+
* AffiliateFeeTransformer:
25+
* Transfers % of the tokens received to an affiliate address.
26+
* ProtocolFeeBrokerTransformer:
27+
* Converts % of the tokens received into ETH, possibly by executing another swap.
28+
* PayTakerTransformer:
29+
* Transfers tokens directly to the taker.
30+
* FillQuoteTransformer:
31+
* Perform a market buy/sell on a set of orders, most likely generated by 0x-API.
32+
* CompoundTokenTransformer:
33+
* If DAI is received, convert to cDAI
34+
* If USDC is received, convert to cUSDC
35+
* If cDAI is received, convert to DAI
36+
* if cUSDC is received, convert to USDC
37+
38+
### Flash Wallets
39+
40+
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.
41+
42+
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.
43+
44+
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.
45+
46+
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.
47+
48+
### Transformation Pipeline
49+
50+
`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.
51+
52+
![transform-pipeline](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/transform-pipeline.png)
53+
54+
For example, if a taker wanted to do the following:
55+
56+
1. Pay ETH
57+
2. Convert to WETH
58+
3. Fill a WETH → USDC quote
59+
4. Pay affiliate fee in USDC
60+
5. Wrap the USDC in cUSDC
61+
62+
It might look like:
63+
64+
65+
![transform-example](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/transform-example.png)
66+
67+
### Locking Down Transformers
68+
69+
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`).
70+
71+
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.
72+
73+
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:
74+
75+
* Is only callable by the deployer/owner.
76+
* Checks that `address(this) == _implementation` to ensure it always self-destructs in its own context.
77+
78+
## Key Benefits
79+
80+
* Composable flexibility
81+
* 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.
82+
* Rapid development
83+
* By separating out transformation logic from the Exchange proxy, we can iterate on these operations much more quickly.
84+
* Peace-of-mind
85+
* 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.
86+
* Novel use cases
87+
* 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.
88+
89+
###
90+
91+
## Implementation
92+
93+
The feature interface:
94+
95+
```solidity
96+
interface ITransformERC20 {
97+
98+
/// @dev Defines a transformation to run in `transformERC20()`.
99+
struct Transformation {
100+
// The deployment nonce of the transformer.
101+
// This is the nonce the deployer had when deploying the transformer.
102+
// The address of the transformer will be derived from this value.
103+
uint32 deploymentNonce;
104+
// Arbitrary data to pass to the transformer.
105+
bytes data;
106+
}
107+
108+
/// @dev Executes a series of transformations to convert an ERC20 `inputToken`
109+
/// to an ERC20 `outputToken`.
110+
/// @param inputToken The token being provided by the sender.
111+
/// If `0xeee...`, ETH is implied and should be provided with the call.`
112+
/// @param outputToken The token to be acquired by the sender.
113+
/// `0xeee...` implies ETH.
114+
/// @param inputTokenAmount The amount of `inputToken` to take from the sender.
115+
/// May be `uint256(-1)` to indicate the maximum transferrable.
116+
/// @param minOutputTokenAmount The minimum amount of `outputToken` the sender
117+
/// must receive for the entire transformation to succeed.
118+
/// @param transformations Sequence of transformations to apply to the token
119+
/// balances.
120+
/// @return outputTokenAmount The amount of `outputToken` received by the sender.
121+
function transformERC20(
122+
IERC20TokenV06 inputToken,
123+
IERC20TokenV06 outputToken,
124+
uint256 inputTokenAmount,
125+
uint256 minOutputTokenAmount,
126+
Transformation[] calldata transformations
127+
)
128+
external
129+
payable
130+
returns (uint256 outputTokenAmount);
131+
}
132+
```
133+
134+
The transformer interface:
135+
136+
```solidity
137+
/// @dev A transformation callback used in `TransformERC20.transformERC20()`.
138+
interface IERC20Transformer {
139+
140+
/// @dev Called from `TransformERC20.transformERC20()`, AFTER the requested
141+
/// ERC20 tokens have been transferred to this contract.
142+
/// If ETH is requested, it will be attached to this call.
143+
/// Some or all of the caller's balance of requested tokens/ETH will be
144+
/// transferred, and any unused tokens/ETH should be returned to
145+
/// `msg.sender` before returning.
146+
/// @param callDataHash The hash of the `TransformERC20.transformERC20()` calldata.
147+
/// @param taker The taker address (caller of `TransformERC20.transformERC20()`).
148+
/// @param data Arbitrary data to pass to the transformer.
149+
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
150+
function transform(
151+
bytes32 callDataHash,
152+
address taker,
153+
bytes calldata data
154+
)
155+
external
156+
payable
157+
returns (bytes memory rlpDeploymentNonce);
158+
}
159+
```
160+
161+
# TokenSpender
162+
163+
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:
164+
165+
* Lack of separation of complex logic vs access to funds.
166+
* No easy way to detach allowances in case of a critical vulnerability.
167+
* Allowances do not migrate if we ever redeploy the Exchange proxy.
168+
* Dangling allowances if we decide to migrate the Exchange proxy to use the V3 asset proxies for allowances.
169+
170+
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()`.
171+
172+
This universal allowance contract has a single `executeCall()` function which blindly `call`s calldata on the caller’s behalf. This contract is akin the `FlashWallet` used by `transformERC20()` with some notable differences:
173+
174+
* Implements the `Authorizable` mixin.
175+
* Owner will be set to the governor.
176+
* The `ZeroEx` contract will be the sole authorized address.
177+
* Cannot perform a `delegatecall`. Instead exposes an (authority-only) `executeCall()` function that simply forwards a call.
178+
* Is never intended to hold a balance.
179+
180+
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)`.
181+
182+
183+
![token-spender](https://raw.githubusercontent.com/0xProject/0x-protocol-specification/master/exchange-proxy/img/token-spender.png)
184+
185+
The `AllowanceTarget` instance is owned by the same owner of the `ZeroEx` contract, so it can be detached in an emergency or a migration.
91.2 KB
Loading
26.7 KB
Loading
31.8 KB
Loading
103 KB
Loading
27 KB
Loading
61.1 KB
Loading

0 commit comments

Comments
 (0)