vyper icon indicating copy to clipboard operation
vyper copied to clipboard

VIP: Custom Storage Types

Open fubuloubu opened this issue 5 years ago • 5 comments

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:

  1. Interface imports (importing other components allowed)
  2. Function decorators (full access by default)
  3. Event logging
  4. Struct definitions
  5. External calls
  6. 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

fubuloubu avatar May 07 '20 14:05 fubuloubu

Meeting notes: Come back to this when it's more concrete.

fubuloubu avatar May 11 '20 18:05 fubuloubu

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.

charles-cooper avatar Oct 18 '21 20:10 charles-cooper

One question is: should components be instantiable in memory? Or can they have HashMaps as members.

charles-cooper avatar Oct 18 '21 21:10 charles-cooper

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.

fubuloubu avatar Oct 18 '21 21:10 fubuloubu

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.

charles-cooper avatar May 20 '22 13:05 charles-cooper