You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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.
(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.)
Throws if no valid approval is found, reduces numCalls if applicable (see below pseudo code for details)
function reduceApproval(addressgrantor, addressgrantee, bytesmemoryargs) public {
// Check for a generic approval
(Approval memoryapproval, boolfound) =getApproval(grantor, grantee);
// Return successfully if a valid approval is foundif(found && approval.expiryTimestamp >block.timestamp) return;
// Check for a specific approvaluint256 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 foundrequire(found, "no approval");
// Throw if expiry timestamp is in the pastrequire(approval.expiryTimestamp >block.timestamp, "approval expired");
// Throw if numCalls is 0require(approval.numCalls >0, "no approved calls left");
// Throw if args exists and doesn't match approved argsrequire(approval.args.length==0|| approval.args == args, "args not approved");
// Reduce numCalls, unless it's MAX_UINT128 to support permanent approvalsif(approval.numCalls != MAX_UINT128) {
approval.numCalls--;
setApproval(grantor, grantee, approval);
}
}
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(bytesmemoryargs) 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(addressfrom, bytesmemoryargs) 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(addressfrom, bytesmemoryargs) 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
User approves a contract to call a TransferSystem once with a certain argument to transfer ownership of a certain item to the contract.
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.
Problem description
This proposal is a solution for multiple problems:
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.
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/transferFrompattern, @dk1a’s Subsystem proposal (#319 (comment) / #268) and @dk1a's comment about approvals here: #319 (comment)Changes to World contract
We add an
ApprovalComponentandApprovalSystemto the MUDWorldcontract.ApprovalComponent
EntityID:
keccak(grantor, grantee)(for generic approvals) orkeccak(grantor, grantee, systemID)(for approvals restricted to certain systems)Value:
ApprovalexpiryTimestampandnumCallscould 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 theargsvalue is empty or non-empty.)ApprovalSystem
ApprovalComponentsetApproval(address grantee, uint256 systemID, Approval memory approval)msg.senderasgrantortograntee. To create a generic approval,systemIDcan be set to 0.reduceApproval(address grantor, address grantee, bytes args)Throws if no valid approval is found, reduces
numCallsif applicable (see below pseudo code for details)revokeApproval(address grantee)/revokeApproval(address grantee, uint256 systemID)grantee/systemIDMore functions like
checkApprovalcan be added, but are less relevant for this proposalChanges to Systems
Every system implements its logic in an internal
_execute(address from, bytes memory args)function.The base
Systemcontract implements the following functions:execute(bytes memory args): executes the system asmsg.senderexecuteFrom(address from, bytes memory args): checks ifmsg.senderhas approval fromfromto call the system, then executes the system asfromexecuteInternal(address from, bytes memory args): checks ifmsg.senderhas approval fromaddress(this)to call this system. If so, the call is trusted and the system is executed asfromApplications
Session wallets
expiryTimestampto a burner wallet.executeFromfunctions as the burner wallet, withfromset to the main wallet. All assets are owned by the main wallet.expiryTimestampContract interaction
approve/transferFrompattern, approvals can be used for atomic interactions with contracts, likeTransferSystemonce with a certain argument to transfer ownership of a certain item to the contract.swapfunction, which callsTransferSystemon the user’s behalf to transfer the item to the contract, and then callsTransferSystemon its own behalf to transfer another item from the contract to the user. (Note how this is not possible ifTransferSystemonly usesmsg.senderfor access control.)First party modular systems / using systems as stateful libraries
MoveSystem, and use it as building block in aPathSystemandCombatSystem(since it’s the same developer, there is full trust inPathSystemandCombatSystem)MoveSystemcan create a permanent generic approval forPathSystemandCombatSystem, to allow both to callMoveSystem'sexecuteInternalfunction. (Just like a library, except component access control happens on the level ofMoveSysteminstead ofPathSystem/CombatSystem)Third party modular systems
executeFromfunctions to do so.MoveSystemdeployed by an unknown developer.PathSystemto automate pathfinding and travelling larger distances.PathSystemdeclaresMoveSystemas dependency, client automatically shows a pop up for user to give necessary permissions the first time user attempts to callPathSystem.RegisterSystem.setApproval(PathSystem, MoveSystemID)to allow thePathSystemto permanently (or with restrictions) callMoveSystemon user’s behalf.PathSystemcan callMoveSystem.executeFromon behalf of the user to automate movement.