Skip to content

Conversation

@mrice32
Copy link
Contributor

@mrice32 mrice32 commented Apr 30, 2024

This is a generic handler where the user can perform a series of calls (multicall) after their tokens are received in the contract.

User can specify a fallback address that is:

  • Paid all remaining tokens whether the call succeeds or fails.

If the user doesn't specify a fallback address, the call must succeed.

The user can call drainLeftoverTokens on the handler in their calls to drain any remaining amount of any token or ETH in their calls. This is meant as a way to deal with calls where the amount of tokens they take isn't known a priori.

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 changed the title feat: add multicall handler [Proposal] feat: add multicall handler Apr 30, 2024

for (uint256 i = 0; i < calls.length; ) {
Call memory call = calls[i];
(bool success, ) = call.target.call{ value: call.value }(call.callData);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this contract get value? Doesn't it receive WETH as a contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might unwrap the WETH in one of your calls, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah fair

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 requested review from bmzig, nicholaspai and pxrl May 1, 2024 13:44
@james-a-morris
Copy link
Contributor

Open question:

  1. Should we create multiple versions of this contract with slight variants on the Call struct? For instance, one could exclude value and one could include a canFail boolean to allow some calls to fail without reverting. The idea behind multiple versions is that we might not want users who don't need these features to pay for the extra calldata (value and canFail will add 64 bytes total to the calldata for each call).

I think this makes sense. OOC could we also have a byte at the start of message determine what format we can expect Call[] to be? This way, we could unpack accordingly and only need the one contract

address,
bytes memory message
) external nonReentrant {
Call[] memory calls = abi.decode(message, (Call[]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any validation we want to do here on converting message into Call[]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What sort of validation would you suggest?

My understanding is that abi.decode will cause a revert if the format is wrong.

@bmzig
Copy link
Contributor

bmzig commented May 1, 2024

  1. Should we create multiple versions of this contract with slight variants on the Call struct? For instance, one could exclude value and one could include a canFail boolean to allow some calls to fail without reverting. The idea behind multiple versions is that we might not want users who don't need these features to pay for the extra calldata (value and canFail will add 64 bytes total to the calldata for each call).

From what I found, the price of deployment in gas scales by around $78 \times \text{byte}$, while the price of extra calldata scaled by 4 or 7 gas per byte, so if these contracts will have a lot of volume, which they presumably will, it probably will be worth it to pay the cost of a more expensive deployment for less expensive transactions.

  1. Should we create two variants of the contract, one that uses tstore reentrancy guards and one that uses oldschool reentrancy guards? The latter will probably be required to support chains that don't yet have the Cancun EVM upgrade. The former will save gas for chains that do. This might be an overoptimization, though.

If it is a simple implementation, I'd say its a good idea.

@mrice32
Copy link
Contributor Author

mrice32 commented May 1, 2024

  1. Should we create multiple versions of this contract with slight variants on the Call struct? For instance, one could exclude value and one could include a canFail boolean to allow some calls to fail without reverting. The idea behind multiple versions is that we might not want users who don't need these features to pay for the extra calldata (value and canFail will add 64 bytes total to the calldata for each call).

From what I found, the price of deployment in gas scales by around 78×byte, while the price of extra calldata scaled by 4 or 7 gas per byte, so if these contracts will have a lot of volume, which they presumably will, it probably will be worth it to pay the cost of a more expensive deployment for less expensive transactions.

Yep! I don't think the deployment costs are a big deal. I worry about the complexity. When people integrate, instead of saying "use contract 0x1234" we'll have to split by use case. We'll say "if you want to send value, use contract 0x1234, if you want to allow reverts, use contract 0x5678", etc.

  1. Should we create two variants of the contract, one that uses tstore reentrancy guards and one that uses oldschool reentrancy guards? The latter will probably be required to support chains that don't yet have the Cancun EVM upgrade. The former will save gas for chains that do. This might be an overoptimization, though.

If it is a simple implementation, I'd say its a good idea.

Agreed. We'll probably have to upgrade the OZ library, but seems valuable enough to get both variants audited.

@mrice32
Copy link
Contributor Author

mrice32 commented May 1, 2024

Open question:

  1. Should we create multiple versions of this contract with slight variants on the Call struct? For instance, one could exclude value and one could include a canFail boolean to allow some calls to fail without reverting. The idea behind multiple versions is that we might not want users who don't need these features to pay for the extra calldata (value and canFail will add 64 bytes total to the calldata for each call).

I think this makes sense. OOC could we also have a byte at the start of message determine what format we can expect Call[] to be? This way, we could unpack accordingly and only need the one contract

Yep, I thought a little about something like this. Downside is that it makes building the message more complex.

abi encoding your params is a common pattern, but appending extra bytes to the front to create a tagged-union-like structure isn't. So it would require more effort and would have more footguns than the alternative I think.

Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 changed the title [Proposal] feat: add multicall handler feat: add multicall handler May 19, 2024
mrice32 added 2 commits May 19, 2024 16:18
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 force-pushed the mrice32/multicall_handler branch from ad3cece to 9e4ca50 Compare May 19, 2024 20:37
@mrice32 mrice32 marked this pull request as ready for review May 19, 2024 20:37
mrice32 added 4 commits May 19, 2024 22:25
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
Signed-off-by: Matt Rice <matthewcrice32@gmail.com>
@mrice32 mrice32 merged commit 16b5d30 into master May 20, 2024
@mrice32 mrice32 deleted the mrice32/multicall_handler branch May 20, 2024 05:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants