We’ve talked about supporting general resources in Athena as opposed to only simple balances.
Here are some ideas to kickstart the discussion on how it could work:
Each resource is defined by a contract. The resource address is the contract address + resource id (one contract can control several resources; for non-fungible (data) resources, each resource will have its own id).
Every resource contract has a transfer method: this method receives the source address, destination address, resource id and amount, and some optional “context info”. If it succeeds the resource is transferred. The transfer method implementation can query the principle of the transaction, so if the source address is the principle, a simple resource wouldn’t need to check additional signatures – it can rely on the verification done for the transaction itself before executing it.
We will need to support third party transfers (i.e., principal A moving a resource from address B to address C) – it would be nice to make this completely abstract, but maybe we want to force ERC20-like semantics to make things more consistent.
Resources have “traits” (like move, although I think we will want different ones). These are declared by the resource contract and enforced by the runtime. Here are some examples:
Data: the resource is non-fungible. Each data resource has (optional) associated data, which is also “owned” by the owner of the resource. Ownership gives exclusive read-only access to the data (so it can be accessed without a cross-contract call); modifying the data still requires a call to the resource contract.
Rent: an owner of this resource must pay rent (this will be a trait of most resources, but not the native coin). It’s a marker rather than an API (the amount of rent is determined by the runtime based on the storage size of the resource).
Hold: The resource can be held persistently by other addresses. A resource that doesn’t have the hold ability can’t be owned by any address except the resource contract itself at the end of every transaction (i.e., during a transaction the resource can be owned and transferred, but the runtime checks that all the resources of this type were “returned” by the end of the transaction).
Refusable: the resource cannot be transferred to an address that hasn’t “agreed” to accept it. I’m not sure about the best way to signal agreement. One possibility is that every resource contract has a dummy accept method. If this method is successfully called by a principal, the runtime records that principal’s agreement to receive this resource type (this allows wallets to accept resources that weren’t known at the time the wallet was instantiated). Note that the registration for accepting a “Rent” resource already requires some rent.
Burn: The resource can be burned freely. In this case the resource contract has a “burn” method, but even if the burn method fails, the resource will be burned (i.e., there is no way to prevent a principal from burning its own resource).
Resources that have both the Hold and the Rent traits must have both the Refusable and Burn traits (otherwise, someone could “stick” you with a resource you can’t get rid of and must pay rent on) .
I think it’s helpful to discuss resources in the context of what’s already been done. Here are some good starting points for resources and abilities in Move. I’m not aware of implementations in other languages, but if you are, feel free to share them.
Move seems to define only four abilities: copy, drop, store, and key. It’s a simple, elegant system (e.g., store serves double-duty in Sui: it also indicates resources that can be transferred outside the defining module). We should see if we can make it work for us, too, and if we really need more abilities than these.
I think it’s also important to distinguish between primitive abilities defined on low level structs (resources), vs. “traits” that particular programs may adhere to. As one example, delegatable feels more like a trait than a primitive.
I still don’t understand Move well enough. It defines an address, a module, and a struct (resource), in that order. (See above links.) But I don’t think individual struct has its own address. Maybe we should start the design with the top-to-bottom data model.
This is true for every fungible resource and not true for non-fungible (data) resources, so it doesn’t need a separate trait.
I’m not sure what this means
This is already captured by the custom transfer method that decides whether a transfer is allowed. I don’t think it needs special VM support.
I think the closest thing to what we want is actually Polygon Miden’s assets, except that we’re not using their asynchronous transaction model. In Miden’s model, a “forced” transfer is impossible, because each transfer consists of two separate transactions, one by the sender and one by the receiver. In our synchronous model, we do have to deal with this issue (hence the restrictions I proposed regarding Hold+Rent implying MustAccept+Burn).
The abilities in move are part of a type system, whose goals are a little different than ours. Move wants the compiler to be able to verify whether the program correctly handles resources. They support arbitrary nested data structures that can hold resources as just another type, and the type abilities make this possible.
In our case, we are doing the checks at runtime, which is less efficient but much easier. We also don’t support nested resources – a resource always belongs to a top-level account, not to another resource.
Depending how this is designed, we could probably take advantage of Rust’s type system to do the checking at compile time, too. E.g., if each ability is implemented as a Rust trait, and structs (resources) conform to the wrong set of traits, the program simply won’t compile.
We might be able to do that as a feature to help programmers, but we can’t rely on it at the VM level, since contracts are specified directly in risc-v code rather than rust source. So we’ll need checking at runtime in any case.
It will be difficult (maybe impossible) to achieve the same guarantees that Move has to offer. Its resource model has both compile-time and runtime guarantees. Move ensures that resource types cannot be copied or discarded. This means that a resource can only exist in one place at a time and must be explicitly transferred or destroyed. The compiler enforces ownership rules, preventing unauthorized access and modification at compile-time. Move’s runtime ensures that resources are managed according to the rules defined at compile-time.
Rust’s ownership system enforces rules at compile time that prevent data races and ensures memory safety. However, creating a runtime verifier for Rust would mean translating its compile-time checks into runtime assertions. This would involve significant overhead and complexity, as Rust’s guarantees are designed to avoid runtime checks as much as possible (not trivial if possible at all) and as @talm mentioned we can’t rely on it at the VM level anyway.
Once we give up on compile-time verification, everything becomes hugely simpler, and we don’t need all the features of Move’s system to get similar guarantees about resources. The runtime system we’re talking about is part of the VM, not the rust toolchain, and it shouldn’t be very hard to implement.
On the rust side, we’ll just need to define the APIs that translate into the appropriate system calls.