vyper icon indicating copy to clipboard operation
vyper copied to clipboard

POC feat[lang]: add EIP-3074 support

Open charles-cooper opened this issue 1 year ago • 5 comments

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

Put a link to a cute animal picture inside the parenthesis-->

charles-cooper avatar Apr 15 '24 21:04 charles-cooper

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

fubuloubu avatar Apr 15 '24 21:04 fubuloubu

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)

charles-cooper avatar Apr 15 '24 22:04 charles-cooper

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

fubuloubu avatar Apr 15 '24 22:04 fubuloubu

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?

charles-cooper avatar Apr 15 '24 22:04 charles-cooper

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

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

fubuloubu avatar Apr 16 '24 15:04 fubuloubu