-
Notifications
You must be signed in to change notification settings - Fork 460
ZeroEx: TransformERC20, TokenSpender #2545
ZeroEx: TransformERC20, TokenSpender #2545
Conversation
af4a61c
to
8350c09
Compare
7a803f9
to
7d5162e
Compare
001283e
to
7df6530
Compare
68aa72c
to
1126a26
Compare
b2da1e9
to
9ed933f
Compare
dbb6018
to
4cba718
Compare
import "./IERC20TokenV06.sol"; | ||
|
||
|
||
library LibERC20TokenV06 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This library has significant changes from the original (0.5) version.
4cba718
to
1fc7d3f
Compare
/// pool. If one is not available, a new one will be deployed. | ||
/// Only callable from within. | ||
/// @return puppet The acquired puppet. | ||
function _acquirePuppet() external returns (IPuppet puppet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proposing prefixing "internal" registered functions with _
.
/// @dev Internal version of `transformERC20()`. Only callable from within. | ||
/// @param callDataHash Hash of the ingress calldata. | ||
/// @param taker The taker address. | ||
/// @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. | ||
/// @param minOutputTokenAmount The minimum amount of `outputToken` the sender | ||
/// must receive for the entire transformation to succeed. | ||
/// @return outputTokenAmount The amount of `outputToken` received by the sender. | ||
function _transformERC20( | ||
bytes32 callDataHash, | ||
address payable taker, | ||
IERC20TokenV06 inputToken, | ||
IERC20TokenV06 outputToken, | ||
uint256 inputTokenAmount, | ||
uint256 minOutputTokenAmount, | ||
Transformation[] calldata transformations | ||
) | ||
external | ||
payable | ||
returns (uint256 outputTokenAmount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expose this function (internally) so we can wrap it with friendlier market fill functions down the line.
5189e3d
to
6de6076
Compare
/// @dev Initializes this feature. | ||
/// @param impl The actual address of this feature contract. | ||
/// @return success Magic bytes if successful. | ||
function bootstrap(address impl) external returns (bytes4 success) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dunno why I was passing in impl
when immutable _implementation
was a thing.
|
||
// tslint:disable: max-classes-per-file | ||
|
||
type ArgTypes = string | BigNumber | number | boolean | BigNumber[] | string[] | number[] | boolean[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add Numberish
array support to revert errors.
"chai-bignumber": "^3.0.0", | ||
"dirty-chai": "^2.0.1", | ||
"ganache-core": "^2.9.0-istanbul.0", | ||
"ganache-core": "^2.10.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixes a weird invalid opcode bug.
/// @param features Features to bootstrap into the proxy. | ||
/// @return zeroEx The deployed and configured `ZeroEx` contract. | ||
function deploy(address payable owner) public virtual returns (ZeroEx zeroEx) { | ||
function deploy(address payable owner, BootstrapFeatures memory features) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So now the migrators won't deploy the feature contracts for you. Turns out some of these are quite large. And they were messing with my geth_call.
ad91546
to
0afcc07
Compare
0afcc07
to
b4c5081
Compare
…nsform()` and disable hex capitalization linter rule because of prettier conflicts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good, love the optimizations!
/// @param callData The call data. | ||
/// @param value Ether to attach to the call. | ||
/// @return resultData The data returned by the call. | ||
function execute( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be more explicit to name these call
and delegateCall
, ex:
IPuppet(address).call(ZRX, transferFromCalldata);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't call
a reserved keyword?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't appear to be, just verified in Remix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually want this close resemblance? Semantically they're different and that could lead to confusion.
// Call the `call()` function on `addr`.
// `msg.sender` will be `addr`.
IPuppet(addr).call(target, callData);
// vs
// Call a raw payload on `addr`.
// `msg.sender` will be `address(this)`
addr.call(callData);
And it's even more nuanced with delegatecall
:
// Call the `delegatecall()` function on `addr`.
// Executes in the context of `addr`.
// `msg.sender` will be `address(this)`.
IPuppet(addr).delegatecall(target, callData)
// vs
// Execute the raw `callData` with the code of `addr`
// in the context of `this`.
// `msg.sender` will be `msg.sender`.
addr.delegatecall(callData)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about execute
and delegateExecute
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's go with executeCall()
and executeDelegateCall()
so everyone is left equally unimpressed.
// was deployed. | ||
// This is used to verify that the transformer was deployed | ||
// by a contract we trust. | ||
bytes rlpNonce; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should hardcode this for now, since we only have one trusted deployer. Just to simplify the call and reduce the possibility of a client-side error. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nonce isn't a known value. It increments for every contract. If a client screws it up, the tx just reverts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was thinking along the lines of hardcoding the unique nonce into each contract before it's deployed. Then having an automatic check before or after the call, either by querying the transformer or standardizing the return data. Ex:
require(
isZeroExTransformer(transformer.getNonce()),
"ONLY_ZERO_EX_TRANSFORMERS"
);
transformer.transform(...);
OR
returnData = transformer.transform(...);
require(
isZeroExTransformer(returnData.getNonce()),
"ONLY_ZERO_EX_TRANSFORMERS"
);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the latter. One less call. Just returning the nonce alone should be sufficient to indicate success since the RLP encoding is never empty and always unique.
@@ -0,0 +1,95 @@ | |||
/* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we've explored this before, but could you remind me why we have to duplicate these files? It would seem cleaner to just bump the version in-place and create a new major version of the package. Additionally, this creates a lot more work for a complete review/audit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels bad bumping up the major version when I've only upgraded a handful of the contracts.
This would also freeze this contract package from the context of its dependents. Lerna will choose to install this package from npm for them. So we could never make changes to the previous version from within the monorepo and have it be reflected in its dependents, because it no longer technically exists in the monorepo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, yeah that does sound kind of hectic. Another option could be to keep this in a branch separate from development
until the transition is complete, similar to how we used to do for new versions of the Exchange. What do you think, would that be more or less cumbersome?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
idk, I think that worked for 3.0 because we weren't immediately productionizing the changes. We'd be folding this stuff into asset-swapper and other packages pretty much right away so we'd be looking for a solution in 2 weeks again.
uint256 takerOutputTokenBalanceAfter = | ||
LibERC20Transformer.getTokenBalanceOf(outputToken, taker); | ||
if (takerOutputTokenBalanceAfter > takerOutputTokenBalanceBefore) { | ||
outputTokenAmount = takerOutputTokenBalanceAfter.safeSub( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would seem to me that we would always want to compute outputTokenAmount
, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'll stay at zero if the balance does not increase. We can't represent decreases with a uint256.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some more context, suppose minOutputTokenAmount == 0
. Then the user doesn't mind how much they receive, but wouldn't we still want to emit and return the value? Additionally, if takerOutputTokenBalanceAfter < takerOutputTokenBalanceBefore
then the safeSub
will just revert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that makes sense. We probably still want to handle a negative change because SafeMath rich reverts are obnoxiously opaque.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda like having the option of not querying balances twice, e.g. for (un)wrapping WETH it'd be a waste of gas to do so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do too, but I also think it's pretty useful for this function to return the amount transferred regardless.
|
||
|
||
/// @dev Feature that allows spending token allowances. | ||
contract TokenSpender is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the plan for generalizing this to other standards? Will the next version of this feature add support for 721 etc, or are we going to have a separate feature and allowance target?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm thinking we'll just update this feature and keep the same allowance target.
|
||
|
||
/// @dev A contract that can execute arbitrary calls from its owner. | ||
contract Puppet is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda prefer FlashWallet? (flash loans so hot right now)
uint256 takerOutputTokenBalanceAfter = | ||
LibERC20Transformer.getTokenBalanceOf(outputToken, taker); | ||
if (takerOutputTokenBalanceAfter > takerOutputTokenBalanceBefore) { | ||
outputTokenAmount = takerOutputTokenBalanceAfter.safeSub( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda like having the option of not querying balances twice, e.g. for (un)wrapping WETH it'd be a waste of gas to do so
/// @param callData The call data. | ||
/// @param value Ether to attach to the call. | ||
/// @return resultData The data returned by the call. | ||
function execute( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about execute
and delegateExecute
?
b3d2f47
to
c1e34b4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome stuff, just a few nits 🚀
contracts/zero-ex/contracts/src/errors/LibTransformERC20RichErrors.sol
Outdated
Show resolved
Hide resolved
outputTokenAmount = takerOutputTokenBalanceAfter.safeSub( | ||
takerOutputTokenBalanceBefore | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should revert if the final balance is less than the initial balance? What's a scenario where the user would want this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I can't really think of a compelling scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We now revert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, so excited about this! I just left one question but other than that looks good to go (~‾▿‾)~
Description
This adds the
TransformERC20
andTokenSpender
feature to the ZeroEx contract. Please refer to the internal spec for background on these features.This PR does not include the transformers themselves. Those are in #2576.
Testing instructions
Types of changes
Checklist:
[WIP]
if necessary.