It's preferred that wallets delegate to a proxy contract which points to the account's wallet implementation. This allows users to change their wallet code without requiring them to sign another EIP-7702 authorization message. It also better mimics how smart contract wallets upgrade.
A guiding goals for the proxy forwarder:
- minimal - no superfluous features
- efficient - one storage load max
- safe - revocable and controllable only by the EOA and it's wallet implementation
The rough idea is as follows:
- if the upper bit of the proxy target is one, consider the proxy revoked and quit
- if proxy target is otherwise empty, initialize it
- to authenticate the initialization, verify a signed message from the EOA which includes the desired target and the current chain's id
- once complete, call back into the wallet with the desired user action and value
- when the caller is a special "revoke_bouncer", set the upper bit of the target
- if the proxy target is not empty, forward the user's call via delegatecall
;; ______________ ___
;; /__ /__ / __ \__ \ ____ _________ _ ____ __
;; / / / / / / /_/ // __ \/ ___/ __ \| |/_/ / / /
;; / / / / /_/ / __// /_/ / / / /_/ /> </ /_/ /
;; /_/ /_/\____/____/ .___/_/ \____/_/|_|\__, /
;; /_/ /____/
;;
;; It's preferred that wallets delegate to a proxy contract which points to the
;; account's wallet implementation. This allows users to change their wallet
;; code without requiring them to sign another EIP-7702 authorization message.
;; It also better mimics how smart contract wallets upgrade.
;;
;;
;; The rough idea is as follows:
;;
;; * if the upper bit of the proxy target is one, consider the proxy
;; revoked and quit
;; * if proxy target is otherwise empty, initialize it
;; * to authenticate the initialization, verify a signed message from the
;; EOA which includes the desired target and the current chain's id
;; * once complete, call back into the wallet with the desired user
;; action and value
;; * when the caller is a special "revoke_bouncer", set the upper bit of
;; the target
;; * if the proxy target is not empty, forward the user's call via
;; delegatecall
#define TARGET_SLOT 0
#define REVOKE_BOUNCER 4242
#define ECRECOVER_ADDR 1
;; Load target. If the revoke bit is set, exit. If the target is zero, consider
;; it unset and continue to initialization. Otherwise, forward call to target.
push TARGET_SLOT ;; [target_slot]
sload ;; [target]
dup1 ;; [target, target]
dup1 ;; [target, target, target]
push 255 ;; [255, target, target, target]
shr ;; [target >> 255, target, target]
iszero ;; [revoke_bit == 0, target, target]
jumpi @check_init ;; [target, target]
;; Proxy is revoked, exit.
push0 ;; [0]
push0 ;; [0, 0]
revert ;; []
;; Check if target is initialized.
check_init:
iszero ;; [target == 0, target]
jumpi @init ;; [target]
;; Next, see if the call is coming from the revoke bouncer contract. This
;; contract (not yet written) would essentially echo back a call to the
;; caller. For example, suppose A calls the bouncer, the bouncer contract would
;; simply call back into A. This is a way of communicate intent to the proxy
;; without polluting calldata space.
caller ;; [caller, target]
push20 REVOKE_BOUNCER ;; [caller, revoke_bouncer, target]
eq ;; [caller == revoke_bouncer, target]
iszero ;; [caller != revoke_bouncer, target]
jumpi @proxy ;; [target]
;; Process the revocation.
pop ;; []
push 1 << 255 ;; [1 << 255]
push TARGET_SLOT ;; [target_slot]
sstore ;; []
push0 ;; [0]
push0 ;; [0, 0]
return ;; []
;; Forward the original call to the proxy.
proxy:
push0 ;; [ret_ost, target]
calldatasize ;; [calldatasize, ret_ost, target]
push0 ;; [0, calldatasize, ret_ost, target]
push0 ;; [0, 0, calldatasize, ret_ost, target]
calldatacopy ;; [ret_ost, target]
calldatasize ;; [args_size, ret_ost, target]
push0 ;; [args_ost, args_size, ret_ost, target]
callvalue ;; [value, args_ost, args_size, ret_ost, target]
push0 ;; [ret_size, value, args_ost, args_size, ret_ost, target]
swap5 ;; [target, value, args_ost, args_size, ret_ost, ret_size]
gas ;; [gas, target, value, args_ost, args_size, ret_ost, ret_size]
delegatecall ;; [success]
;; Return on success, revert otherwise.
handle_ret:
returndatasize ;; [ret_size, success]
push0 ;; [idx, ret_size, success]
returndatacopy ;; [success]
push0 ;; [idx, success]
returndatasize ;; [ret_size, idx, success]
swap2 ;; [success, idx, ret_size]
jumpi @success
revert
success:
return
;; The input to init is four 32-byte values: target, v, r, and s.
;; After verifying the message has authority on this chain, it will compute the
;; message hash and verify the signature.
;; The format of the sig hash is:
;;
;; msg := keccak(chainid || address).
;;
;; Both chainid and address are left padded to 32 bytes for now. This padding
;; should probably be removed in later revisions. Accept either chainid 0 or the
;; current chain's id.
init:
chainid ;; [chainid]
push0 ;; [0, chainid]
mstore ;; []
push 128 ;; [128]
push0 ;; [0, 128]
push 32 ;; [32, 0, 128]
calldatacopy
push 64 ;; [64]
push0 ;; [0, 64]
keccak256 ;; [msg]
push0 ;; [0, msg]
mstore ;; []
;; Call ecrecover precompile to determine the signer.
push 32 ;; [ret_size]
push0 ;; [ret_ost, ret_size]
push 128 ;; [args_size, ret_ost, ret_size]
push0 ;; [args_ost, args_size, ret_ost, ret_size]
push ECRECOVER_ADDR ;; [addr, args_ost, args_size, ret_ost, ret_size]
gas ;; [gas, addr, args_ost, args_size, ret_ost, ret_size]
staticcall ;; [success]
pop ;; []
push0 ;; [0]
mload ;; [signer]
address ;; [address, signer]
eq ;; [address == signer]
jumpi @set_target ;; []
push0 ;; [0]
push0 ;; [0]
revert ;; []
set_target:
push0 ;; [0]
calldataload ;; [target]
dup1 ;; [target, target]
push TARGET_SLOT ;; [target_slot, target, target]
sstore ;; [target]
;; Call self to trigger proxy.
push0 ;; [ret_ost, target]
push 128 ;; [128, ret_ost, target]
calldatasize ;; [calldatasize, ret_ost, target]
sub ;; [calldatasize - 128, ret_ost, target]
dup1 ;; [calldatasize - 128, args_size, ret_ost, target]
push 128 ;; [128, calldatasize - 128, args_size, ret_ost, target]
push0 ;; [0, 128, calldatasize - 128, args_size, ret_ost, target]
calldatacopy ;; [args_size ret_ost, target]
push0 ;; [args_ost, args_size, ret_ost, target]
callvalue ;; [value, args_ost, args_size, ret_ost, target]
push0 ;; [ret_size, value, args_ost, args_size, ret_ost, target]
swap5 ;; [target, value, args_ost, args_size, ret_ost, ret_size]
gas ;; [gas, target, value, args_ost, args_size, ret_ost, ret_size]
call ;; [success]
jump @handle_ret
few possible corrections:
shouldn't this be push 128? since there are 4 values we want to copy -- target, v,r,s (each 32 bytes).
in line 14,
jumpi
should bejumpi @check_init
towards the end in
set_target
, shouldn't it be delegatecall and not call?in set_target:
dup1 ;; [target]
should be
dup1 ;; [target, target]