vyperlang / vyper

Pythonic Smart Contract Language for the EVM
https://vyperlang.org
Other
4.87k stars 796 forks source link

POC feat[lang]: add EIP-3074 support #3958

Open charles-cooper opened 6 months ago

charles-cooper commented 6 months ago

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-->]()

fubuloubu commented 6 months ago
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
charles-cooper commented 6 months ago
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)
fubuloubu commented 6 months ago
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

charles-cooper commented 6 months ago

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?

fubuloubu commented 6 months ago
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