|
| 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 | + |
| 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 | + |
| 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 | +``` |
0 commit comments