Skip to content

Proposal: General approval pattern (for modular systems and session wallets) #327

Description

@alvrs

Note: I’ll use keccak(a,b,c) in this proposal to refer to keccak256(abi.encode(a,b,c))

Problem description

This proposal is a solution for multiple problems:

  1. Session wallets:
    Currently, we mainly use burner wallets to reduce the friction of manually approving each transaction. However, this approach is not suitable for storing valuable or permanent assets because the private key of the burner wallet is stored in the browser's local storage and is therefore vulnerable to phishing or loss. The current alternative is to use regular wallets such as MetaMask, which drastically increases the friction of using tx-heavy applications because each tx must be approved. We need a way to store valuable and permanent assets in a main wallet, but interact with the application via a temporary session wallet.
  2. Modular systems:
    Described in more detail in Modular systems / system to system calls #319. Short summary: Currently, systems can only serve as an entry point into the application, but it is inconvenient to use systems as stateful libraries or modules to build other higher level systems.

Proposal description

On a high level, the idea is to create a mechanism to allow any address to approve any other address to call certain systems on its behalf.

It is inspired by ERC20’s approve / transferFrom pattern, @dk1a’s Subsystem proposal (#319 (comment) / #268) and @dk1a's comment about approvals here: #319 (comment)

Changes to World contract

We add an ApprovalComponent and ApprovalSystem to the MUD World contract.

  • ApprovalComponent

    • EntityID: keccak(grantor, grantee) (for generic approvals) or keccak(grantor, grantee, systemID) (for approvals restricted to certain systems)

    • Value: Approval

      struct Approval {
         uint128 expiryTimestamp;
         uint128 numCalls;
         bytes args;
      }
      • (Side note: To save gas when verifying approvals, expiryTimestamp and numCalls could be bitpacked into a single storage slot. To save even more gas, one bit in this bitpacked slot could be reserved as a flag to represent whether the args value is empty or non-empty.)
  • ApprovalSystem

    • Has write access to ApprovalComponent
    • Functions:
      • setApproval(address grantee, uint256 systemID, Approval memory approval)

        • Creates a new approval from msg.sender as grantor to grantee. To create a generic approval, systemID can be set to 0.
      • reduceApproval(address grantor, address grantee, bytes args)

        • Throws if no valid approval is found, reduces numCalls if applicable (see below pseudo code for details)

          function reduceApproval(address grantor, address grantee, bytes memory args) public {
          	// Check for a generic approval
          	(Approval memory approval, bool found) = getApproval(grantor, grantee);
          	
          	// Return successfully if a valid approval is found
          	if(found && approval.expiryTimestamp > block.timestamp) return;
          
          	// Check for a specific approval
          	uint256 systemID = getIdByAddress(msg.sender); // Only the approved system can reduce the approval's numCalls value
          	(approval, found) = getApproval(grantor, grantee, systemID);
          
          	// Trow if no approval is found
          	require(found, "no approval");
          
          	// Throw if expiry timestamp is in the past
          	require(approval.expiryTimestamp > block.timestamp, "approval expired");
          
          	// Throw if numCalls is 0
          	require(approval.numCalls > 0, "no approved calls left");
          
          	// Throw if args exists and doesn't match approved args
          	require(approval.args.length == 0 || approval.args == args, "args not approved");
          
          	// Reduce numCalls, unless it's MAX_UINT128 to support permanent approvals
          	if(approval.numCalls != MAX_UINT128) {
          	     approval.numCalls--;
          	     setApproval(grantor, grantee, approval);
          	}
          }
      • revokeApproval(address grantee) / revokeApproval(address grantee, uint256 systemID)

        • Removes the approval for the given grantee / systemID
      • More functions like checkApproval can be added, but are less relevant for this proposal

Changes to Systems

Every system implements its logic in an internal _execute(address from, bytes memory args) function.

The base System contract implements the following functions:

  • execute(bytes memory args): executes the system as msg.sender

    function execute(bytes memory args) public {
    	return _execute(msg.sender, args);
    }
  • executeFrom(address from, bytes memory args): checks if msg.sender has approval from from to call the system, then executes the system as from

    function executeFrom(address from, bytes memory args) public {
    	ApprovalSystem.reduceApproval(from, msg.sender, args);
    	return _execute(from, args);
    }
  • executeInternal(address from, bytes memory args): checks if msg.sender has approval from address(this) to call this system. If so, the call is trusted and the system is executed as from

    function executeInternal(address from, bytes memory args) public {
    	ApprovalSystem.reduceApproval(address(this), msg.sender, args);
    	return _execute(from, args);
    }

Applications

Session wallets

  • At the beginning of each session, the user gives a generic (or restricted) approval with expiryTimestamp to a burner wallet.
  • The client calls all systems via their executeFrom functions as the burner wallet, with from set to the main wallet. All assets are owned by the main wallet.
  • After the session, the main wallet can revoke the approval, or the approval runs out after the expiryTimestamp

Contract interaction

  • Similar to ERC20’s approve / transferFrom pattern, approvals can be used for atomic interactions with contracts, like
    1. User approves a contract to call a TransferSystem once with a certain argument to transfer ownership of a certain item to the contract.
    2. User calls the contract’s swap function, which calls TransferSystem on the user’s behalf to transfer the item to the contract, and then calls TransferSystem on its own behalf to transfer another item from the contract to the user. (Note how this is not possible if TransferSystem only uses msg.sender for access control.)

First party modular systems / using systems as stateful libraries

  • Developer wants to modularize movement logic into a MoveSystem, and use it as building block in a PathSystem and CombatSystem (since it’s the same developer, there is full trust in PathSystem and CombatSystem)
  • MoveSystem can create a permanent generic approval for PathSystem and CombatSystem, to allow both to call MoveSystem's executeInternal function. (Just like a library, except component access control happens on the level of MoveSystem instead of PathSystem/CombatSystem)
  • This use-case and usage is very similar to the approach explored in @dk1a’s subsystems Add subsystem #268
  • Creating these permanent generic approvals between first party systems can be automated via the MUD CLI / deploy process.

Third party modular systems

  • Assume a third party developer wants to create a new higher level system by combining multiple existing lower level systems.
  • They can use the lower level system’s executeFrom functions to do so.
    • This requires users to explicitly approve the higher level system to call the lower level systems on the user’s behalf (with optional restrictions), which is intended to prevent systems from being able to call any system on behalf of users.
    • The approval process can be a default client pop-up integrated into MUD.
  • Example:
    • There is a MoveSystem deployed by an unknown developer.
    • A third party developer wants to add a new PathSystem to automate pathfinding and travelling larger distances.
    • PathSystem declares MoveSystem as dependency, client automatically shows a pop up for user to give necessary permissions the first time user attempts to call PathSystem.
      • User calls RegisterSystem.setApproval(PathSystem, MoveSystemID) to allow the PathSystem to permanently (or with restrictions) call MoveSystem on user’s behalf.
    • From now on, PathSystem can call MoveSystem.executeFrom on behalf of the user to automate movement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionA discussion (without clear action/outcome)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions