vyper
vyper copied to clipboard
POC feat[lang]: add EIP-3074 support
POC, add EIP-3074 support to the language via new authcall call type and authorize() builtin.
What I did
How I did it
How to verify it
example usage:
from ethereum.ercs import IERC20
@external
def do_transfer(token: IERC20, receiver: address, amount: uint256, sig: Bytes[97]):
authorize(addr, sig)
authcall token.approve(receiver, amount)
authcall token.transfer(receiver, amount)
Commit message
Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see our commit message style guide for what we would ideally like to see in a commit message.)
Description for the changelog
Cute Animal Picture
from ethereum.ercs import IERC20 @external def do_transfer(token: IERC20, receiver: address, amount: uint256, sig: Bytes[97]): authorize(addr, sig) authcall token.approve(receiver, amount) authcall token.transfer(receiver, amount)
I feel like while it is not necessary to use a context manager based on how this EIP works, we might be able to ensure a little better level of safety by using one:
from ethereum.ercs import IERC20
@external
def do_transfer(token: IERC20, receiver: address, amount: uint256, sig: Bytes[97]):
commit: bytes32 = abi_encode(...) # could be an empty bytes32 literal
with authorize(msg.sender, sig, commit=commit): # Calls `AUTH`
# NOTE: EIP-3074 doesn't require `token.approve` workflow
authcall token.transfer(receiver, amount) # happens within `authorize` context
# authcall outside of context raises compile-time exception
from ethereum.ercs import IERC20 @external def do_transfer(token: IERC20, receiver: address, amount: uint256, sig: Bytes[97]): authorize(addr, sig) authcall token.approve(receiver, amount) authcall token.transfer(receiver, amount)I feel like while it is not necessary to use a context manager based on how this EIP works, we might be able to ensure a little better level of safety by using one:
from ethereum.ercs import IERC20 @external def do_transfer(token: IERC20, receiver: address, amount: uint256, sig: Bytes[97]): commit: bytes32 = abi_encode(...) # could be an empty bytes32 literal with authorize(msg.sender, sig, commit=commit): # Calls `AUTH` # NOTE: EIP-3074 doesn't require `token.approve` workflow authcall token.transfer(receiver, amount) # happens within `authorize` context # authcall outside of context raises compile-time exception
prank? :)
with prank(msg.sender, sig):
extcall token.transfer(receiver, amount)
but yea it's a somewhat interesting design space because AUTH and AUTHCALL are actually so closely coupled. another possibility is
authcall token.transfer(receiver, amount, auth=sig)
or
token = authorize(address, sig)
# "token" is a compile-time concept which gets erased at runtime
# it is invalidated at compile-time if any other call to `authorize()` happens
extcall token.transfer(receiver, amount, auth=token)
token = authorize(address, sig) # "token" is a compile-time concept which gets erased at runtime # it is invalidated at compile-time if any other call to `authorize()` happens extcall token.transfer(receiver, amount, auth=token)
token is far too loaded, but this isn't bad
should still be authcall though
i think with any of the above techniques to link the AUTH invocation with AUTHCALL, we no longer need the authcall keyword, especially with the scoped thing. like is there any reason you would want to switch between regular and authcall inside of the with authorized(addr, sig): block?
token = authorize(address, sig) # "token" is a compile-time concept which gets erased at runtime # it is invalidated at compile-time if any other call to `authorize()` happens extcall token.transfer(receiver, amount, auth=token)
tokenis far too loaded, but this isn't badshould still be
authcallthough
actually, more I think about this the more I like it. the auth= kwarg precludes any usage without first doing authorize. we can keep track of authorization context when compiling and raise when it uses older:
assert extcall token.transfer(receiver, amount, auth=...) # can't do this for obvious reasons
auth1 = authorize(acct1, sig1)
assert extcall token.transfer(receiver, amount, auth=auth1, default_return_value=True)
auth2 = authorize(acct2, sig2) # auth1 is now "stale"
assert extcall token.transfer(receiver, amount, auth=auth2, default_return_value=True)
assert extcall token.transfer(receiver, amount, auth=auth1, default_return_value=True) # raises
Best part is we don't have to argue what happens when you exit context, the context persists until we exit the call, or authorize is done again