Collect Calls in the Dune Network

Alain Mebsout
Dune Network
Published in
15 min readOct 31, 2019
Photo by Brunno Tozzo

TL;DR: Smart Contracts in Dune can pay their users’ transaction fees, through a new programmatic mechanism. This safe and flexible system makes it possible to design and implement a token standard where users can pay fees in tokens instead of DUNs.

This article is the first part of a two-part series on collect calls in the Dune Network.

One major factor that hinders adoption of blockchain based applications (dApps) is the need to hold the platform’s native cryptocurrency tokens to cover any transfer expenses.

These expenses take the form of a fee in major blockchains ( e.g., like Ethereum and Bitcoin). This fee has to be included with every transaction and call on these platform and is redistributed to the validators (or miners, or baker in Dune lingo) as an incentive to include said transaction in a block.

This fee system presents several drawbacks for dApps users.

Because the fees must be payed in the base currency they are subject to the same volatility and price variations. This means that transaction costs on major blockchains are tributary to the whims of a constantly evolving market driven by speculators. This is far from ideal.

Validators typically decide to include first the transactions with higher fees (or higher fees to platform usage ratio), and their own strategy can change arbitrarily (but usually with the aim to maximize profit).

Bitcoin Average Transaction Fees (https://bitinfocharts.com)

For instance, on the graph above we can see that Bitcoin transaction fees peaked at $55 in December 2017 and recently $6.5 in June 2019. The daily average is today (October 2019) $0.6.

Ethereum Average Transaction Fees (https://bitinfocharts.com)

The situation is more nuanced for Ethereum with a peak at $5.5 in July 2018 and a daily average of $0.15 at present day (October 2019).

In an ideal setting, dApps users should not be reminded that they are using a blockchain. They should not be forced to purchase and own the base crypto-currency to interact with applications.

The Dune Network offers the possibility to make collect calls to smart contracts in which the sender specifies that the fees incurred by the transaction will be payed by the recipient. This exciting new feature comes with a number of challenges (and small compromises) that we try to solve in Dune.

Fees in Dune Network

When making a transfer or a contract call in Dune, the user has to specify three things as part of his or her transaction:

  • The gas limit: this says that the execution of the transaction will not consume more than this limit in gas.
  • The storage limit: this says that the execution of the transaction will not add more than this number of bytes to the storage.
  • The fee, in DUN: this fee is payed by the sender and credited to the baker who includes the transaction in a block on the chain.
Fees in Dune Network

When validating a transaction, some inexpensive checks are performed first, such that ensuring that the limits are within the protocol bounds, that the arguments can be deserialized with the provided gas, that the sender can pay the fees (the fee is actually directly deducted), etc.

The transaction is valid if it uses less gas and storage than advertised. The sender pays for any additional increase in the storage size in the form of a burn. These tokens are not credited to anyone, they simply disappear from the circulating supply.

Advertising limits ( i.e. guarantees on resource consumption) allows the baker (who also validates transactions) to quickly decides if it should include a transaction in its next block or not. This is done simply by comparing the ratio fees / (gas limit + storage limit) with predefined values. The more a user pays fees, and the lesser gas (or storage) its transaction uses, the more chance it has to be included by a baker. When there are too many transactions to include in a single block, the baker can also choose to promote transactions that have higher fees / gas ratios so as to maximize their profit.

Photo by Lee Russell — the New York Public Library

This mechanism is similar to the one found in Ethereum, excepted that instead of specifying fees directly, Ethereum users specify a gas price and the fee is computed with the total gas used by a transaction.

Smart Contracts that Pay Fees

The fee mechanism presented above is for regular transfers and contract calls. However there is an additional mechanism in Dune which allows a smart contract to pay transaction fees and burn in place of a user. We call these new kinds of transactions collect calls.

Smart Contracts that Pay Fees

Smart contracts which accept collect calls must have additional code which computes the maximum fee and burn that the contract is ready to pay for the given call.

This code is executed every time a collect call is made to the contract, in a second pre-check phase. The transaction validation scheme briefly depicted in the previous section is identical, excepted for this additional pre-check. First, the same inexpensive checks are performed but the fee is taken from the contract instead of the source (it will fail this check if the contract has not enough to cover fees, independently of what it accepts). In the second check, the special fee code is executed with the same transaction arguments and storage. This execution must return a pair (maximum_fees, maximum_storage) which corresponds to the maximum fee the contract can pay in DUN, and the maximum storage increase it allows in bytes (this translates directly to the maximum burn). All this execution must happen within a special gas bound called the fee gas limit ( cf. the diagram above).

This fee gas limit must be within the protocol bounds (it is much lower than the actual gas limit). The gas used in the fee code execution is deducted from the total gas allocation. This special purpose, separate, gas limit prevents attacks where a caller spends all his gas in the pre-check without anyone paying fees.

If these checks succeed, the validation scheme goes on. The actual code of the smart contract can know if it is executing a collect call, so as to perform additional or different actions.

This programmatic way of handling fees by the smart contract itself brings great flexibility as it allows the smart contract programmer to precisely decide for which entry points the contract can pay fees, and to compute these fees depending on the actual call arguments and the storage. For instance, a smart contract can use a settable value of the storage to perform conversions.

Liquidity Toy Example

The following is a small example of a smart contract which pays fees in the Liquidity language.

Liquidity toy example of a smart contract that pays fees

In Liquidity, the code to compute fees must appear as an annotation of the main body either with

begin[@fee <fee_code>] <body> end

or

( <body> )[@fee <fee_code>]

The fee code for the entry point vote does not do anything really. It simply returns the pair(0.01_DUN, 0p) which means that it accepts collect calls for which it can pay fees of up to 0.01_DUN, and it does not accept to pay for any storage increase.

The fee code for the second entry point default fails directly. This means that this entry point can never be called with a collect call.

This example can be compiled with the Liquidity compiler (or with the web editor):

> liquidity demo_fee.liq 
Main contract Demo_fee
Constant initial storage generated in "./demo_fee.init.tz"
File "./demo_fee.tz" generated
You may want to typecheck with:
dune-client typecheck script ./demo_fee.tz

We can inspect the generated Michelson:

parameter (or :_entries (string %vote) (unit %default));
storage (map string int);
code { DUP ;
DIP { CDR @storage_slash_1 } ;
CAR @parameter_slash_2 ;
DUP @parameter ;
IF_LEFT
{ RENAME @choice_slash_3 ;
DUUUP @storage ;
DUP @votes ;
DUUUP @choice ;
GET ;
IF_NONE
{ PUSH string "Bad vote" ; FAILWITH }
{ DUUP @votes ;
PUSH int 1 ;
DUUUP @x ;
ADD ;
DUUUUUP @choice ;
DIP { SOME } ;
DIIIP { DROP } ;
UPDATE @votes ;
NIL operation ;
PAIR } ;
DIP { DROP ; DROP } }
{ DROP ; DUUP ; NIL operation ; PAIR } ;
DIP { DROP ; DROP } };
code @fee
{ DUP ;
DIP { CDR @storage_slash_1 } ;
CAR @parameter_slash_2 ;
DUP @parameter ;
IF_LEFT
{ RENAME @choice_slash_9 ;
PUSH (pair mutez nat) (Pair 10000 0) ;
DIP { DROP } }
{ RENAME @__slash_11 ;
PUSH string "I don't pay fees for default" ;
FAILWITH } ;
DIP { DROP ; DROP } };

Notice the extra code @fee element in the contract. Here the @fee annotation is used to specify that the following instructions sequence is the code that should be executed to compute fees. For it to be well typed in Michelson, it needs to transform a stack with one element of the type[ pair <parameter> <storage> ] into a stack of one element of the type[ pair mutez nat ].

Making Collect Calls with the Client

We can deploy this contract on the Dune blockchain. We use a sandbox node for this demonstration.

> dune-client originate contract demo_fee \
for bootstrap1 transferring 10 from bootstrap1 \
running demo_fee.tz \
--init "$(cat demo_fee.init.tz)" \
--burn-cap 1.097
...
New contract KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn originated.

Once the operation is included, we can start interacting with the contract. Let’s first make a normal call to vote for house Harkonnen from the user vladimir who has 10 đ.

> dune-client show address vladimir
Hash: dn1RTGy18myNxVPA36PaEFT9wgfwq6gepa68
Public Key: edpktnmdhJUdVRemQs5VwuYCwtCvwDCfqVMJo8SNXSyQKfyEStnZGG
> dune-client get balance for vladimir
10 đ
> dune-client transfer 0 from vladimir to demo_fee --arg 'Left "Harkonnen"'Node is bootstrapped, ready for injecting operations.
Estimated gas: 21956 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'ootXSK5LrL2XeJj19ce5ULHxAkmGJCtGoW4B1coc4QNsyi9RdtN'
Waiting for the operation to be included...
Operation found in block: BLVDuXr3DgBoAr2rzTf8RtMY7LA1WbTMyY3xNthLcqAMS4dyHRy (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: dn1RTGy18myNxVPA36PaEFT9wgfwq6gepa68
Fee to the baker: đ0.002477
Expected counter: 32
Gas limit: 22056
Storage limit: 0 bytes
Balance updates:
dn1RTGy18myNxVPA36PaEFT9wgfwq6gepa68 ........... -đ0.002477
fees(dn1XcnRktbepk2ZrasF3XSZq5L6EzbVdZWv3,4) ... +đ0.002477
Transaction:
Amount: đ0
From: dn1RTGy18myNxVPA36PaEFT9wgfwq6gepa68
To: KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn
Parameter: (Left "Harkonnen")
This transaction was successfully applied
Updated storage:
{ Elt "Atreides" 0 ; Elt "Fremen" 0 ; Elt "Harkonnen" 1 }
Storage size: 840 bytes
Consumed gas: 21956

We can see that the total fee was 0.002477 đ, that it was taken from vladimir and given to the baker. Looking at the balances confirms that.

> dune-client get balance for vladimir
9.997523 đ
> dune-client get balance for demo_fee
10 đ

Now let’s try to perform a similar operation with a collect call. We use a different key paul who also has 10 đ.

> dune-client show address paul
Hash: dn1Q3Mou654z5LngZ3nk3MWQW82EV44tmJMh
Public Key: edpkugzpCG6pLrjKa9H9JBzMKw1rX9XMeDrh8PApdHfE8EGd2UHbQD
> dune-client get balance for paul
10 đ

Paul wants to vote for house Atreides, but wants the transaction fees to be payed by the contract. For this we can use the option --collect-call of the client.

> dune-client transfer 0 from paul to demo_fee --arg 'Left "Atreides"' --collect-callNode is bootstrapped, ready for injecting operations.
Estimated gas: 27198 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'ooLVG2sPbLa6BzECUo6wPhG4WgXNDe8BbPZSjGSXk32yx2s9anE'
Waiting for the operation to be included...
Operation found in block: BLxjKJvaB6AHjVPaPfWtFXhV2XhPFTCqX1ZGgwbssndPQfaMSBR (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: dn1Q3Mou654z5LngZ3nk3MWQW82EV44tmJMh
Fee to the baker: đ0.003003
Expected counter: 34
Gas limit: 27298
Storage limit: 0 bytes
Balance updates:
KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn ........... -đ0.003003
fees(dn1XcnRktbepk2ZrasF3XSZq5L6EzbVdZWv3,4) ... +đ0.003003
Transaction (Collect Call):
Amount: đ0
From: dn1Q3Mou654z5LngZ3nk3MWQW82EV44tmJMh
To: KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn
Parameter: (Left "Atreides")
This transaction was successfully applied
Updated storage:
{ Elt "Atreides" 1 ; Elt "Fremen" 0 ; Elt "Harkonnen" 1 }
Storage size: 840 bytes
Consumed gas: 27198

The transaction receipt shows that the total fee was 0.003003 đ, but that it was taken from the contract KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn and given to the baker. The fee is slightly higher than for the previous non-collect call because the gas needs to also account for the execution of the fee code. If we look at the balances, we can indeed that paul did not pay anything, but that the contract demo_fee did.

> dune-client get balance for paul
10 đ
> dune-client get balance for demo_fee
9.996997 đ

That’s it, we just made our first collect call!

If we make a call with more fees than the contract allows, it will fail. (In the dune client, the failure happens at simulation time but the same thing would happen if we tried to inject this operation.)

Here, we force the fee to be higher by manually setting the gas limit (-G 100000).

> dune-client transfer 0 from paul to demo_fee --arg 'Left "Atreides"' --collect-call -G 100000Node is bootstrapped, ready for injecting operations.
Estimated storage: no bytes added
An error occured during the pre-check to ensure fees can be collected from the smart contract.
The smart contract was asked to pay 0.010273 đ fees but only allows 0.01 đ fees for this call.
Fatal error:
transfer simulation failed

Interacting with Fee Paying Contracts without DUN

This feature would not be very interesting if users were still required to hold DUN to make collect calls (even if they don’t spend them). For this, Dune allows accounts with 0 đ to still make collect calls to smart contracts.

In Dune, it is not possible to make a regular transaction from an account that has 0 đ (an empty account), because such accounts do not have any storage space associated to them. In particular there is nowhere to store the public key, the counter, etc. This is because in order to validate transactions, the Dune node needs to know the public key of an account to check that the signature is correct.

This is what revelation operations are for. They simply tell the blockchain what is the public key (edpk...) associated to a public key hash (the dn1... address). These revelation operations incur fees for the sender so a 0 balance account cannot make them.

In Dune, you can now embed revelations in a collect call operation. This essentially amounts to telling the smart contract to perform the revelation on your behalf and to pay for the resulting fees and origination burn.

Let’s create a fresh set of keys for Alia:

> dune-client gen keys alia
Key alia registered

She is not registered on the blockchain, but alia still wants to vote on our contract by making collect calls.

> dune-client transfer 0 from alia to demo_fee --arg 'Left "Fremen"' --collect-callNode is bootstrapped, ready for injecting operations.
This simulation failed:
Manager signed operations:
From: dn1MBbh1jJfJt7kW1fYXGGtBLSvEWBENARKM
Fee to the baker: đ0
Expected counter: 40
Gas limit: 800000
Storage limit: 60000 bytes
Transaction (Collect Call):
Amount: đ0
From: dn1MBbh1jJfJt7kW1fYXGGtBLSvEWBENARKM (with revelation edpkusxLtR3QJiWkkSR9NKdTt57JQ1fgACKYdbdUpLaGNio6zX5jSf)
To: KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn
Parameter: (Left "Fremen")
This transaction was BACKTRACKED, its expected effects (as follow) were NOT applied.
Updated storage:
{ Elt "Atreides" 1 ; Elt "Fremen" 1 ; Elt "Harkonnen" 1 }
Storage size: 840 bytes
Consumed gas: 37208
Balance updates:
KT1T7WFn6kiye4GREmyqnemihCZf5Z38k6Zn ... -đ0.257
The smart contract cannot pay for storage, accepted limit is 0 bytes.
Storage limit exceeded during typechecking or execution.
Try again with a higher storage limit.
Fatal error:
transfer simulation failed

We can notice the public key as part of the embedded revelation in the transaction result. Unfortunately, this call fails because the smart contract does not pay for any storage in collect calls. In particular, it cannot pay for the origination burn of 0.257 đ. Let’s modify our contract; we replace the fee code for the entry point vote by:

[@fee
(0.01_DUN, 257p (* bytes, i.e. one origination *))
]

Which means that the contract accepts to pay for increases of at most 257 bytes in the storage, or one origination. After re-originating the contract (now at KT1ScDLR9u9jA7QJ2SBnZmhbk6v7t2URt6UH) we can try to make the same call:

> dune-client transfer 0 from alia to demo_fee --arg 'Left "Fremen"' --collect-callNode is bootstrapped, ready for injecting operations.
Estimated gas: 37202 units (will add 100 for safety)
Estimated storage: 257 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'ooeWH55RPVQsMAGAEmsEttEZ1DjGWruncvh6qatUFkUvPPza5JP'
Waiting for the operation to be included...
Operation found in block: BKxc274nAimdRfZDECN5FZFhNRGZwxuJYee4ExutH2NNrsoi7nU (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: dn1MBbh1jJfJt7kW1fYXGGtBLSvEWBENARKM
Fee to the baker: đ0.004039
Expected counter: 40
Gas limit: 37302
Storage limit: 277 bytes
Balance updates:
KT1ScDLR9u9jA7QJ2SBnZmhbk6v7t2URt6UH ........... -đ0.004039
fees(dn1XcnRktbepk2ZrasF3XSZq5L6EzbVdZWv3,5) ... +đ0.004039
Transaction (Collect Call):
Amount: đ0
From: dn1MBbh1jJfJt7kW1fYXGGtBLSvEWBENARKM (with revelation edpkusxLtR3QJiWkkSR9NKdTt57JQ1fgACKYdbdUpLaGNio6zX5jSf)
To: KT1ScDLR9u9jA7QJ2SBnZmhbk6v7t2URt6UH
Parameter: (Left "Fremen")
This transaction was successfully applied
Updated storage:
{ Elt "Atreides" 0 ; Elt "Fremen" 1 ; Elt "Harkonnen" 0 }
Storage size: 841 bytes
Consumed gas: 37202
Balance updates:
KT1ScDLR9u9jA7QJ2SBnZmhbk6v7t2URt6UH ... -đ0.257

The call with the revelation is now valid and the contract pays for both the fee and the origination burn. The consumed gas is higher than the previous one because it also includes the gas for the revelation (10000). The revelation gas does not count towards the consumed fee gas but counts for the total consumed gas of the operation.

> dune-client get balance for alia
0 đ
> dune-client get balance for demo_fee
9.738961 đ

The key for alia is now revealed in the blockchain. She can continue to make collect calls without needing to reveal her key again. It can still happen that the public key is cleaned from the context, in this case she will need to embed another revelation. Revelations are embedded by the client automatically much like they are produced for usual manager operations (when the manager is not revealed).

You can already deploy your own smart contracts who pay fees on the Dune Testnet (at the time of this writing) to test the feature on your own. Collect calls will come to the mainnet in the next minor upgrade (on November 5, 2019).

Spam Prevention

The fee system of Dune was designed to prevent spam and attacks. It is thus a sensitive part of the protocol. Introducing different ways to pay fees can compromise some of these security measures if it is not done with care.

Photo by Pau Casals

Code execution to compute fee is limited in resources through a special purpose gas limit. This code cannot make any side effects and cannot produce operations. This fee gas limit also appears in the operation itself, so bakers can adopt a different strategy if they want to.

Contracts that pay fees cannot be drained (uncontrollably) of their funds by malicious actors. If their fee code agreed on to pay the announced fee, but the transaction fails, they are refunded of their fee. This fee is instead taken from the source of the operation. The user might not be able to pay for these fees though, e.g. if they interact with a 0 balance account, so in this case the operation will be marked as invalid and will never be included in any block. Users who inject, or nodes who broadcast, too many such transactions can be banned by the node. This is still a potential attack vector on a single node, but this mitigation makes it ineffective in practice.

If a smart contract decides to pay for fees indiscriminately (as is the case in our previous toy contract), then their balance could still be drained (in fees) by someone sending transactions to it. So the smart contract programmers and managers still bear some responsibility on the way their funds can be used.

In transaction chains, fees are payed by the top-level caller, which we call the source. So a practical limitation of collect calls, is that only the smart contract for the top-level call can pay fees. For instance, it is not possible to generate a collect call operation from a smart contract (with TRANSFER_TOKENS in Michelson or Contract.call in Liquidity).

Integration in Dunscan

Collect calls can already be observed in Dunscan (on the testnet for now). Successful collect calls are identified by a circled arrow (instead of a plain arrow for regular transactions and contract calls).

The fee and burn are displayed in a different color to indicate that the payer is different.

Collect calls in Dunscan

The second part of this series will explain how you can use collect calls to create token contracts where fees are payable in tokens. Stay tuned.

--

--