vyper
vyper copied to clipboard
VIP: Custom Storage Types
Simple Summary
Allow logical components of a program to be specified separately from a main contract file
Motivation
It is very important to larger contracts to be able to break apart functionality from the main file for logical separation. This means we have to find an appropriate way to allow to break out logical functionality that has a high degree of separation. This could also simplify the implementation of several proposed features such as #484 and #1020 by making them internal library components.
Specification
A Component is defined very similarly to a contract, with certain features disallowed:
- Interface imports (importing other components allowed)
- Function decorators (full access by default)
- Event logging
- Struct definitions
- External calls
- Environment variables disallowed for internal methods
Examples
Constructor methods are defined using __init__(self, *args):
component MyComponent:
def __init__(self, _a: uint256):
... # do something with _a
Components can have internal storage:
component MyComponent:
...
bar: uint256 # internal state
def foo(self):
self.bar += 1
Components can use environment variables:
component MyComponent:
...
def baz() -> uint256:
return msg.timestamp
Usage
A Component can be imported into a contract file similar to an interface:
from my_component import MyComponent
It is defined similar to Python objects, and must be instantiated:
...
c: MyComponent = MyComponent(...)
...
Their implementation would be similar to structs, but with the addition of methods as struct members. They can be used in the same way as structs:
c: MyComponent
def bar() -> uint256:
self.c.foo() # self.c.bar += 1
return self.c.bar
Backwards Compatibility
No backwards incompatibilities.
Dependencies
No dependencies.
References
https://diligence.consensys.net/blog/2020/05/an-experiment-in-designing-a-new-smart-contract-language/
Copyright
Copyright and related rights waived via CC0
Meeting notes: Come back to this when it's more concrete.
Right now the thinking is if a contract has storage variables in it, then it's one of these (storage variables + methods on them), otherwise it's a stateless "library" as in #2431. Unclear if there should be syntactic difference from a regular contract.
One question is: should components be instantiable in memory? Or can they have HashMaps as members.
One question is: should components be instantiable in memory? Or can they have HashMaps as members.
In Solidity, it is possible to define a struct with a mapping as a member, however it implicitly precludes it from being used as a memory variable, which is weird.
i ported an example solidity contract (https://github.com/tellor-io/TellorPlayground/blob/575cfc565c9526df1320d3389fcb015bc86d1107/contracts/TellorPlayground.sol#L224-L279) to play around with this idea. so far, the encapsulation looks good, but it might be more natural if the "component" could access the parent contract's methods.
compare methods depositStake(), requestStakingWithdraw(), withdrawStake() at https://gist.github.com/charles-cooper/c40d8ea66b8afc0d552dc8776d0fc952#file-tellor_playground-vy. the fact that storage variables are not needed is nice. however, there is a bit of awkardness, because withdrawStake() and depositStake() require a call to self._transfer() in the parent contract. it might be better if it were possible to add the transfer logic inside of StakeInfo.mark_stake_withdrawn() and StakeInfo.deposit_stake(), because it would be easier to verify the invariants.