-
-
Notifications
You must be signed in to change notification settings - Fork 812
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
VIP: [research] Static balance sheet analysis for Vyper contracts #1277
Comments
From Meeting: we will investigate this properly post RC1. |
Also: investigate more concise syntax |
A couple syntactic alternatives for the journal control structure:
Throw out the journal keyword and use debits and credits sections
Throw out the journal keyword and use indentation to mark the debits and credits section
So far I like the first one best. Here is def split_payment_better(tok: ERC20, amt: uint256, recipient1: address, recipient2: address):
if recipient1 != 0x00 and recipient2 != 0x00:
journal:
debit(erc20_balance(tok), amt/2)
debit(erc20_balance(tok), amt/2)
credit(erc20.user(tok)(recipient1), amt/2)
credit(erc20.user(tok)(recipient2), amt/2)
elif recipient1 != 0x00:
journal:
debit(erc20_balance(tok), amt)
credit(erc20.user(tok)(recipient1), amt) Finally, we could get rid of the control structure entirely and just have a sequence of debits and credits
But it would make error messages more opaque if a user accidentally orphans one of the statements. |
Suggestion from @jacqueswww since the debits and credits must correspond (at least until we do an SMT checker), we can coalesce each pair into a transfer function. That shortens our function to def split_payment_better(tok: ERC20, amt: uint256, recipient1: address, recipient2: address):
if recipient1 != 0x00 and recipient2 != 0x00:
journal:
transfer(erc20_balance(tok), erc20.user(tok)(recipient1), amt/2)
transfer(erc20_balance(tok), erc20.user(tok)(recipient2), amt/2)
elif recipient1 != 0x00:
journal:
transfer(erc20_balance(tok), erc20.user(tok)(recipient1), amt) Or without the def split_payment_better(tok: ERC20, amt: uint256, recipient1: address, recipient2: address):
if recipient1 != 0x00 and recipient2 != 0x00:
transfer(erc20_balance(tok), recipient1, amt/2)
transfer(erc20_balance(tok), recipient2, amt/2)
elif recipient1 != 0x00:
transfer(erc20_balance(tok), erc20.user(tok)(recipient1), amt) I kind of like the special control structure for clarity but it seems it's not necessary. |
Cf. flintlang/flint#383 for a related approach with linear types |
Interesting implementation here vis a vis pooled values: https://diligence.consensys.net/blog/2020/05/an-experiment-in-designing-a-new-smart-contract-language/ |
Simple Summary
Create a way for contract developers to ensure that balance sheet invariants (e.g., the total amount a user can withdraw is equal to to the total amount that user has deposited) are preserved.
Abstract
Balance sheet analysis of smart contracts is prone to error. Rather than have developers prove ad-hoc invariants about cash flows using constraint-based checkers (cf. Zilliqa/scilla#18 for one approach to this), this VIP proposes a DSL for handling funds based on double-entry accounting principles which maintains balance-sheet invariants by construction. This DSL aims to be easy to read, write and reason about, and familiar to non-programmer accountants. (It is unclear whether this is within the scope of Vyper core or should be an external tool which is why this VIP is marked as [research], but because it requires syntactic support, I believe it is more appropriate as a core Vyper feature).
Motivation
One of Vyper's goals is to create readable, auditable code which is difficult to be misleading. One tricky area for contract developers is accounting! Fortunately, accountants have developed techniques over the years to help with doing accounting precisely.
This proposal centers around two concepts: the journal entry and the chart of accounts. A journal entry is a collection of debits and credits. Here is an example journal entry:
This journal entry balances, that is, the debits and credits sum to the same amount. A journal entry MUST balance in order to be valid. This preserves the balance sheet invariant, which is (in this case) that Equity == Assets. In fact, the chart of accounts defines multiple balance sheet invariants.
But first, a digression into some definitions around accounts relevant to this proposal. The chart of accounts is a hierarchical list of accounts. The basic chart of accounts has three top-level accounts: Equity, Liabilities and Assets. Equity and Liability accounts are credit-normal, while Asset accounts are debit-normal. This means that debits increase and credits decrease the balance of an Asset account, while debits decrease and credits increase the balance of an Equity or Liability account. The reason for this (cue sweeping statements) stems from how corporate accounting developed in the West; accounting entities do not have value. They may hold property (Asset accounts) but it is always canceled by claims by first lienholders (Liability accounts) and second lienholders (Equity accounts). This is represented by the accounting equation, Assets = Equity + Liabilities. So, when assets increase (are debited), liabilities or equity must increase (get credited) and vice versa.
Here is an example chart of accounts:
Here is an example journal entry involving such accounts. A user deposits 1.1 ether into the contract, and the contract deducts 0.1 as a fee:
Clearly, the top-level balance sheet invariant is preserved:
But there is another, 'phantom' balance sheet invariant implied by the chart of accounts which is equally important: User deposits = User 1 deposits + User 2 deposits. To show how journal entries maintain both invariants at the same time, suppose User 1 transfers ownership of their deposit to User 2 (and just for fun, charge a fee of 0.1 ether).
The balance sheet invariants are now:
In normal accounting, the intermediate invariants are implied (and also easily checked by simply summing the account balances). As a smart contract programmer, it can be useful to know that all of these invariants are maintained every step of the way (and since summing the account balances could have unbounded gas cost, to have the ability to store the balances of the intermediate accounts).
Specification (tentative)
A couple common use cases shape the syntax in this VIP. One is the use case of mapping identifiers to balances, and the other is the need to define custom debit and credit functions to interact with the outside world.
A data structure which handles funds is marked using the
@funds
decorator. In order so that the compiler knows what statements to generate for debits and credits, it must also be marked either@debitnormal
or@creditnormal
. In order to define a nested account, arguments can be provided to the@funds
decorator. For example, the nested accountcan be defined like
Defining a custom debit/credit can be done by defining a special function which is then decorated with
@debit/@credit
and its scope. For example, to debit and credit a phantom token balance, it could be defined as follows:Finally, funds can only be touched with the special
journal
control structure. A journal entry can be created as follows:The compiler should throw unless it can verify that the sum of the debits is equal to the sum of the credits (or the journal entry does not have at least one debit and at least one credit). Also, any function which has a journal entry should have a non-reentrant lock associated with it (cf. #1204).
Examples
Here is a sample token, with just
mint
,burn
andtransfer
functions defined:Here is an example smart contract which illustrates some current error-prone techniques and how this proposal improves the situation, defined once using current Vyper, and once using this VIP. It is a payment splitter where a user can define multiple recipients for a single payment, held in escrow until the recipient withdraws the funds. The first implementation contains three accounting bugs which are easily caught by the journal entry format, and one bug which is prevented by the re-entrancy lock.
Unresolved design issues / design weaknesses
'balances_by_erc20'
) should actually be stored. This seems useful though if the contract has multiple account subgroups (because the smart contract developer can easily access the aggregated balance of each subgroup), e.g.@funds
withmap
. (Should arrays be supported?)@debit/@credit
decorators with their function arguments.funds
(e.g. onlywei_value
allowed).Backwards Compatibility
Possibly add a feature to nonreentrancy in private functions such that their reentrancy properties percolate up to all calling functions.
Dependencies
#563
#1204
Copyright
Copyright and related rights waived via CC0
The text was updated successfully, but these errors were encountered: