Closed alvrs closed 1 year ago
Back when I made #268 I myself wasn't sure subsystems were useful. Now I actually have a bunch of examples, so here's an updated pitch.
System to system calls aren't common. Small projects wouldn't need them at all. Libraries are great, use them whenever possible.
Open this deploy.json, scroll down to CycleCombatSystem
.
It uses some components and CombatSubsystem
, which in turn uses some components, DurationSubsystem
and EffectSubsystem
All these subsystems are also used by other (sub)systems, not just CycleCombatSystem
.
(See #303 for details on EffectSubsystem
and DurationSubsystem
and callbacks and them using each other)
What's a subsystem? Just a system that inherits OwnableWritable, whereas normally systems inherit only Ownable. Subsystems are systems.
Why do I need OwnableWritable?
I don't want users to call subsystems. They're basically stateful libraries. Components already have authorizeWriter
, lets reuse it.
Why not just use Systems then?
authorizeWriter
etc) to the required systems yourself. Inconvenient boilerplate.writeAccess
in deploy.json
. Subsystems as primitves can be built into cli. You don't wanna modify LibDeploy.ejs
manually every time.Why not just use libraries?
CycleCombatSystem
uses if you unroll the subsystems. Manually managing huge writeAccess
es is very inconvenient.writeAccess
. Subsystems do. You can easily see which components/subsystems a subsystem writes to. You have to read the whole library to know what it writes to.world
or components
to a subsystem.Subsystem examples: ERC721BaseSubsystem (a special case, this one can't be a library not for any of the above reasons, but because it has to be stateful due to events) ERC1155BaseSubsystem (same thing) ScopedDurationSubsystem EffectSubsystem CombatSubsystem (this is a perfect example of "Can't, contracts have a size limit". It also has 2 dedicated libs, to keep it less bloated) EquipmentSubsystem (btw this one isn't aware of items or characters or anything. It's a really abstract utility to equip stuff to slots) RandomMapSubsystem (this is an example of a subsystem I think may be better of as a library instead, I'm not sure yet)
@dk1a thanks for the updated pitch / explanation of subsystems and how they compare to regular systems and libraries. I just updated the issue description with a more detailed problem description and potential solutions.
We can definitely agree on the usefulness of "stateful libraries" or calling systems from other systems. In my opinion the subsystem approach perfectly solves the "first party" issues (adding the possibility for stateful libraries for a developer's own systems), but I'd like to think about whether we can come up with a solution that would give the same benefits to third party developers (using existing systems as modules to create more complex higher level systems).
Curious to hear your thoughts on this.
@alvrs Interesting, I've never thought of that side of the problem in relation to subsystems. I haven't really gotten there yet with my game (I just use msg.sender atm), but what I've been planning is to have more of a bottom-up approach. So just don't use simple permissions.
Lets say you have a character entity, and a MoveSystem.
Then you have an ApprovalComponent with mapping(characterEntity=>approvedAddress=>approvedSystem)
(The implementation details are hazy, the mapping can be done e.g. by hashing 2 of 3 or something like that)
Then you have some convenient utility to set and check generic permissions (it can check msg.sender too).
Then if you wanna use PathSystem, call something like approve(getAddressById(PathSystemID), MoveSystemID, charEntity)
Maybe add some utility to recs or std-client that can largely automate this.
approvedAddress instead of approvedSystemId maybe avoids upgradeability phishing, but not really (it could call an upgradeable system, so is it even worth it?)
my details may be off, haven't actually written any code for this
The main point of that whole approval thing is, it's ok if you get phished. You character will just move incorrectly for a while. The attacker doesn't get full permission to do whatever
Kind of related: https://blog.curio.gg/how-we-built-this-treaty-technical-overview/
Very interesting idea! It got me thinking about how we could use a general approval pattern to solve this issue (modular systems) as well as the session wallet issue (to move on from pure burner wallets). Created a proposal in #327, lmk what you think!
Closing this since it's pre v2 (we basically implemented subsystems, and the global entry point is part of #1124)
Problem description
Systems encapsulate logic. It would be nice if it was possible to use systems as modular building blocks to create higher level systems, but when a system is called from another system, any access control logic related to
msg.sender
fails, because the calling system is the newmsg.sender
.A common workaround is to encapsulate logic in libraries instead, use those libraries as building blocks and use systems only as entry points to the application. However, this comes with two issues:
Example of a situation where it would be nice to use systems as building blocks, but we run into the issue of
msg.sender
being different:MoveSystem
has access to aPositionComponent
and implements the rules of movement in the world (eg. the entity to be moved needs to be owned by the system caller (msg.sender
), and the target position needs to be adjacent to the current position)PathSystem
, which batches calls toMoveSystem
to move multiple steps in one transaction. In an ideal worldPathSystem
could just callMoveSystem
multiple times with valid steps, thus respecting the existing rules and adding a higher level system.PathSystem
toMoveSystem
fails, becausemsg.sender
isPathSystem
and not the owner of the entity to be moved.Approaches
1. ~access control via tx.origin~
tx.origin
instead ofmsg.sender
-> not recommended because it can lead to phishing vulnerabilities and discriminates against smart contract wallets.~2. Subsystems
3. Global entry point
World
contract.World
, before calling the requested systems, and finally resetting the caller variable to some placeholder value.msg.sender
, thereby allowing systems to call other systems while preserving the initial caller.tx.origin
for access control, with the exception of supporting smart contracts wallets, and maybe less phishing potential because the entry point call has to be explicit (instead of the possible use of fallback functions withtx.origin
).