-
Notifications
You must be signed in to change notification settings - Fork 188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: General approval pattern (for modular systems and session wallets) #327
Comments
A really good proposal! Much better thought out than my vague approval concept, and very generalizable unlike subsystems. For posterity: my comment about approvals was itself inspired by my OperatorApprovalComponent for ERC721/1155 Systems, so it all comes from ERC20 after all Minor concern: permanent approvals can be impossible to revert if you forget who you approved it for (since you can neither reverse the hash nor loop through all addresses), but this can be solved via an extra component, or discouraging such approvals. (ERC20 etc solve this via events having the data - the extra component could hold such data instead)
To that end I can still see a use case for a Subsystem, in the sense that allowing overrides of |
Just thought of another use-case for contract ApprovalComponent is AbstractComponent {
/** Mapping from entity id to value in this component */
mapping(uint256 => Approval) internal entityToValue;
constructor(address world, uint256 id) AbstractComponent(world, id) {}
function getSchema() public pure override returns (string[] memory keys, LibTypes.SchemaValue[] memory values) {
...
}
function set(uint256 entity, Approval memory value) public virtual onlyOwner {
_set(entity, value);
}
function _set(uint256 entity, bytes memory value) internal override {
_set(entity, abi.decode(value, (Approval)));
}
function _set(uint256 entity, Approval memory value) internal virtual {
// Store the entity's value;
entityToValue[entity] = value;
// Emit global event
IWorld(world).registerComponentValueSet(entity, abi.encode(value));
}
function _remove(uint256 entity) internal virtual override {
// Remove the entity from the mapping
delete entityToValue[entity];
// Emit global event
IWorld(world).registerComponentValueRemoved(entity);
}
function has(uint256 entity) public view virtual override returns (bool) {
return entityToValue[entity].numCalls != 0;
}
function getValue(uint256 entity) public view virtual returns (Approval memory) {
return entityToValue[entity];
}
function getRawValue(uint256 entity) public view virtual override returns (bytes memory) {
return abi.encode(entityToValue[entity]);
}
} This is sort of a bespoke alternative to bitpacking, and even more efficient in many cases (doesn't store length, which is 1 less slot; not sure how decode vs encode compares gas-wise). |
Also a note on overriding Can't speak on general ApprovalSystem, which doesn't exist yet. In theory seems fine. But for subsystems by now I actually dislike that part, in practice having no default Maybe a fully automated deployment would make having a ton of internal systems non-painful (#295 has some notes on why full automation is hard and unsolved) |
Hi, First time posting. I love this project and I've been following the issues. I'm very impressed by the quality of discussions here and this proposal in particular. I wanted to share that I'm working on a better MetaMask that can do automated transaction approval in an EIP-1993 compatible way by creating a new key for each dapp that the user connects. It works the same way as burner wallets from the dapp perspective, but the keys are created and stored in the wallet app (instead of the page's JS context) and synced across the user's devices. You can find out more about how this works in the user docs and in the technical docs. Obviously, it's not a solution for you problem today, just wanted to give you context that changes are to be expected in how wallets work in the future. Best, |
sealvault looks really useful. |
Very good point! We could also account for this by storing this information in the struct Approval {
uint128 expiryTimestamp;
uint128 numCalls;
bytes args;
address grantor;
address grantee;
uint256 systemID;
} The
Yeah, this is something I also thought about when drafting this proposal. The original idea behind requiring a default Instead, we could implement the access control checks described above in modifier onlyApproved(address grantor, bytes memory args) {
ApprovalSystem.reduceApproval(grantor, msg.sender, args);
_;
}
modifier onlyApprovedByThis(bytes memory args) {
ApprovalSystem.reduceApproval(address(this), msg.sender, args);
_;
} To improve the experience for devs who want to use the |
Thanks for sharing this context! I agree with @dk1a that sealvault looks useful and is complementary to this issue. |
The reason I leaned into a 2nd component initially was because that info is stored once, and then rarely used once to cancel. Whereas
Haha you went farther than I meant, but I agree of course (I just meant the modifiers part of it, not removing execute from ISystem). This maybe also helps with something like #325, where
While I'm still skeptical about Those comments are based on my project's current design: |
I have a very specific use-case that I am working through right now that requires this feature, so I'd like to explain the player journey to see if I understand everything correctly. The simple explanation is that I need two systems that exist on separate MUD Worlds to be able to interact with each other and transfer information between them in a trusted way. Here is the specific example of how Sky Strife would use this: Player Journey:
If I am understanding everything correctly, this proposal fits my use-case and I see no issues 👍🏼 I do have a question about internal systems though. I've been using public libraries in the same way that @dk1a described, so I don't see a need for the internal systems. Is there something that public libraries cannot do that the internal systems can? |
Looks good to me too
I have considered public libs as a workable alternative, mostly they can do what internal systems can.
I have a bias against public libs because I don't like how they fit into the deployment pipeline.
(the last 2 points are very minor and wouldn't have made a difference on their own) |
As far as deployment I've been using the default MUD deploy using forge and everything just works out of the box. I believe the only small issue is that it deploys a new library every time it is referenced in a system. I agree that managing write access is pretty nice with subsystems. I currently avoid the entire problem and just set |
If I understand the flow correctly, I think The relationship between In other words, the flow you described sounds like a use case for Note: the flow you described would also work, but would require each player to approve the However, there would also be a use case for the
|
How about MPC wallet ? |
@Osub can you elaborate? |
MPC wallets are multi-party computing shared key fragments that can interact with contracts using 2 of 3 signatures. part1 exists client, part2 game operation, part3 user backup. Interact with the contract using part1 and part2 co-signatures. The user registers the game operation server with a social account or mobile phone number, refer to https://docs.particle.network/ |
I had thought it was intentional, allowing not-fully-trusted third-party sky strife clients for amalgema or something like that. But otherwise |
For account delegation/session wallets/burner wallets, I wonder if we should use something more general purpose/widely supported, that way more things outside of MUD can plug into it directly, e.g. https://delegate.cash/ |
Started an implementation in #345, it should help discussing the finer details. |
Valid point! If we go with a central entry point (as discussed in #347) we could also easily provide multiple entry points for different account abstraction methods, without requiring system developers to think about it / to upgrade (they can just trust that the |
Wow very cool! Lots of good points in the PR description. However I wonder if we should delay this issue until we settled on sth in #347 (since it impacts a lot of code that would be written for this one) and adjust this proposal for #347 when the time has come? #347 will obviously be a breaking |
I had the same thoughts after reading the proposal. |
Closing this in favor of #1124 |
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
/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
andApprovalSystem
to the MUDWorld
contract.ApprovalComponent
EntityID:
keccak(grantor, grantee)
(for generic approvals) orkeccak(grantor, grantee, systemID)
(for approvals restricted to certain systems)Value:
Approval
expiryTimestamp
andnumCalls
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 theargs
value is empty or non-empty.)ApprovalSystem
ApprovalComponent
setApproval(address grantee, uint256 systemID, Approval memory approval)
msg.sender
asgrantor
tograntee
. 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)revokeApproval(address grantee)
/revokeApproval(address grantee, uint256 systemID)
grantee
/systemID
More functions like
checkApproval
can 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
System
contract implements the following functions:execute(bytes memory args)
: executes the system asmsg.sender
executeFrom(address from, bytes memory args)
: checks ifmsg.sender
has approval fromfrom
to call the system, then executes the system asfrom
executeInternal(address from, bytes memory args)
: checks ifmsg.sender
has approval fromaddress(this)
to call this system. If so, the call is trusted and the system is executed asfrom
Applications
Session wallets
expiryTimestamp
to a burner wallet.executeFrom
functions as the burner wallet, withfrom
set to the main wallet. All assets are owned by the main wallet.expiryTimestamp
Contract interaction
approve
/transferFrom
pattern, approvals can be used for atomic interactions with contracts, likeTransferSystem
once with a certain argument to transfer ownership of a certain item to the contract.swap
function, which callsTransferSystem
on the user’s behalf to transfer the item to the contract, and then callsTransferSystem
on its own behalf to transfer another item from the contract to the user. (Note how this is not possible ifTransferSystem
only usesmsg.sender
for access control.)First party modular systems / using systems as stateful libraries
MoveSystem
, and use it as building block in aPathSystem
andCombatSystem
(since it’s the same developer, there is full trust inPathSystem
andCombatSystem
)MoveSystem
can create a permanent generic approval forPathSystem
andCombatSystem
, to allow both to callMoveSystem
'sexecuteInternal
function. (Just like a library, except component access control happens on the level ofMoveSystem
instead ofPathSystem
/CombatSystem
)Third party modular systems
executeFrom
functions to do so.MoveSystem
deployed by an unknown developer.PathSystem
to automate pathfinding and travelling larger distances.PathSystem
declaresMoveSystem
as 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 thePathSystem
to permanently (or with restrictions) callMoveSystem
on user’s behalf.PathSystem
can callMoveSystem.executeFrom
on behalf of the user to automate movement.The text was updated successfully, but these errors were encountered: