Skip to content

Instantly share code, notes, and snippets.

@lightclient
Last active June 7, 2024 06:34
Show Gist options
  • Save lightclient/7742e84fde4962f32928c6177eda7523 to your computer and use it in GitHub Desktop.
Save lightclient/7742e84fde4962f32928c6177eda7523 to your computer and use it in GitHub Desktop.

EIP-7702 Recommended Proxy

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.

Design Goals

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

Sketch

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
@sudeepdino008
Copy link

sudeepdino008 commented Jun 6, 2024

few possible corrections:

push 96                ;; [96]
push0                   ;; [0, 96]
push 32                 ;; [32, 0, 96]
calldatacopy

shouldn't this be push 128? since there are 4 values we want to copy -- target, v,r,s (each 32 bytes).

  1. in line 14, jumpi should be jumpi @check_init

  2. towards the end in set_target, shouldn't it be delegatecall and not call?

  3. in set_target:
    dup1 ;; [target]
    should be
    dup1 ;; [target, target]

@lightclient
Copy link
Author

  1. Correct, I've updated.
  2. Correct.
  3. We could also do the delegatecall directly here, however I think since this code path only executes during the initialization phase it's more important the code is simple and obviously correct. Using delegatecall here might necessitate we duplicate some checks that normally happen in the other path where the regular delegate call occurs. By using call, we just forward the message to the wallet and force the call down the standard code path.
  4. Updated.

@sudeepdino008
Copy link

sudeepdino008 commented Jun 7, 2024

push0                   ;; [0]
mload                   ;; [signer]
address                 ;; [address, signer]
eq                      ;; [address == signer]
jumpi @set_target       ;; []

should address here be caller by any chance? ADDRESS would return proxy contract address (or wallet implementation address if that's where init logic is), whereas maybe the intent here is that the target signer is the one which can init the proxy contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment