Skip to content
340 changes: 340 additions & 0 deletions eip-0031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
Babel Fees
============================================

* Author: nitram147
* Status: Proposed
* Created: 25-January-2022
* Last edited: 25-January-2022
* License: CC0
* Forking: not needed

Introduction
--------------------------------------------

The term “babel fees“ refers to the concept of paying transaction fees in tokens (fe. stablecoins) instead of platform’s primary token (ERG). For more information about the origin of the term “babel fees“, please see the following IOHK article:
[https://iohk.io/en/blog/posts/2021/02/25/babel-fees/](https://iohk.io/en/blog/posts/2021/02/25/babel-fees/)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historically, the idea was first proposed on ErgoForum back in 2019 https://www.ergoforum.org/t/paying-fee-in-ergomix-in-primary-tokens/73 , and implemented in Ergomixer in 2020

Copy link
Contributor Author

@nitram147 nitram147 Jul 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of this part of text (to which you left your comment), is to give credit to the place from which I've got inspiration for the name of this EIP. And since this EIP concept has the same name as the similar concept on Cardano, I thought it'd be appropriate to explain the difference to the EIP reader. Should I also mention somewhere in the EIP that the similar idea was implemented in ErgoMixer for mixing transactions fee payments?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that it would be reasonable to mention ErgoForum ideas and ErgoMixer, to give credits to their authors, and also there are babel fees variants there which can be useful in some use-cases.


EIP-0031 aims to provide the standard for paying fees in tokens, and thus has the same goal as Cardano’s “babel fees“, however, it chooses a different approach, with the main difference being that EIP-0031 does not require any type of forking.

With the Cardano’s approach, user publishes “invalid"(incomplete) transaction and has to wait, hoping that somebody will take his tokens and pay the transaction fees in a primary token (ADA), therefore completing the transaction. EIP-0031, on the other hand, chooses the opposite approach.

Supporters who wish to make money out of EIP-0031 will publish UTXOs, containing primary tokens locked by smartcontract. These will contain price attribute (i.e. how much of the primary tokens is that one specific supporter willing to pay for one piece of user’s tokens (fe. stablecoins)). Let’s call this user’s token a “babel token”.

User who is willing to pay the transaction fee in babel tokens can now find whether there exist any UTXOs belonging to the P2S address specified by the corresponding smartcontract for that specific babel token. If there is any UTXO which contains enough primary tokens for required fees, the user can calculate the price of buying the required amount of primary tokens from this UTXO and then decide whether or not he wishes to use it. In case he accepts this exchange ratio (defined by the UTXO’s price attribute), he can consequently spend this UTXO in his transaction to cover the transaction fees. This spending user now has to recreate the UTXO with the same parameters and insert the required amount of babel tokens into it (primary tokens difference should be less or equal to inserted babel tokens amount times price), which is going to be ensured by the smartcontract.

Strong advantage of this approach (compared to Cardano’s one) is that user always knows in advance whether there is an opportunity to pay via “babel fees” or not, and if there is, what is the exchange ratio. He can therefore be (almost) certain that if he decides to use it, his transaction will be included in the blockchain ledger. Be aware, however, that there exist some exceptions to this rule, which is later discussed in the “Wallets implementation” section.

Motivation
--------------------------------------------

Many users use blockchain solely for transferring (native)tokens, such as stablecoins, or even “meme” coins. These users, understandably, do not want to be bothered with keeping an eye on the amount of blockchain’s primary token they own, or even obtaining this primary token in the first place.

Once they run out of primary token, they have to difficultly (and often costly), swap their tokens of interest for the primary tokens, that they can later use to cover transaction fees. Since primary tokens are also needed for these swaps, users may be forced to introduce new capital to their portfolio solely for the purpose of purchasing primary tokens, used for fee paying.

Since basic transactional fees on the Ergo blockchain are generally quite low, the "babel fees" users would be probaby willing to pay a fee that could be higher than that of a primary token transaction, in exchange for being able to pay in their token of interest and not having to bother with the blockchain’s primary token purchase.

This brings up a financial incentive for “EIP-0031 supporters”, who could benefit out of this arbitrage by providing the liquidity for such “babel fees” users, with primary token’s selling price (expressed in tokens of interest) being higher compared to the same token pair on the exchanges.

Smartcontract specification
--------------------------------------------
Here is the smartcontract's source code in ErgoScript which will be used to protect the babel fee box:
```scala
{

val babelFeeBoxCreator = SELF.R4[SigmaProp].get

val recreatedBox = OUTPUTS(OUTPUTS.size - 2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this index chosen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm right, the Ergo core protocol does not specify transactions fee payment, however, the reference implementation has a hardcoded ErgoTree value on which it expects fees to the miner. If I'm also right (based on my observation), the reference implementation (ergo node) doesn't care about the index of such fee output and accepts it as fee on any transaction output index. Most of the wallet implementations (or to be more precise, the libraries which are used by the wallet developers) put fees UTXO as the last output of a transaction. Thus, I can consider the last output index of a transaction as reserved for fee paying in ERG. So I decided to use the penultimate (second from the end) output as "reserved" for fee paying in case that Babel Fees are used. Based on my observation, many smartcontracts written in ErgoScript use the first output of transaction (output with index = 0) for checking of recreation of some boxes etc. So selecting the penultimate output index as the index "reserved" for Babel Fees paying would be compliant with usage of such smartcontracts - the output indexes wouldn't be in conflict. Of course there can be also smartcontracts which will be using the penultimate output index for some purpose, but I haven't seen such a contract, while I have seen a lot of contracts using first transaction output from which I assume that this choice would be compliant with most of the contracts which are currently being used.

Copy link

@ldgaetano ldgaetano Jul 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the tx has a change box, for whatever reason, then it will be at index OUTPUTS.size-2. I could be wrong, but that's what I have noticed when using AppKit.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: I am wrong, the change box is last. Even then though, the miner fee box would end up in index OUTPUTS.size-2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change box is last, that is correct. But change box is not mandatory

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if the tx has a change box then you can't use this contract with Appkit, shouldn't EIP contracts take into account use with AppKit, isn't that the point of having a library?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With sigma-rust we also haven't a way to set a outputs order, so I just reorder the outputs after transaction is generated.

Copy link

@ldgaetano ldgaetano Aug 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I have been doing some more work on GuapSwap and may need to use more than one babel box, depending on the token, in one transaction. So OUTPUTS(OUTPUTS.size-2) is very restrictive in this case. Is doing:
OUTPUTS.filter{ output: Box => output.propositionBytes == SELF.propositionBytes }(0)
possible instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to set output index in context extension of spending input.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's cool way to do it


val babelFeeBoxRecreated = (
recreatedBox.propositionBytes == SELF.propositionBytes &&
recreatedBox.R4[SigmaProp].get == SELF.R4[SigmaProp].get &&
recreatedBox.R5[Long].get == SELF.R5[Long].get &&
recreatedBox.tokens(0)._1 == tokenId
)

val nanoErgsDifference = SELF.value - recreatedBox.value
val babelTokensBefore = if(SELF.tokens.size > 0){ SELF.tokens(0)._2 }else{ 0L }
val babelTokensDifference = recreatedBox.tokens(0)._2 - babelTokensBefore
val exchangeOK = babelTokensDifference * SELF.R5[Long].get >= nanoErgsDifference

sigmaProp(
babelFeeBoxCreator ||
(
babelFeeBoxRecreated && exchangeOK && (nanoErgsDifference >= 0)
)
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to enforce that the erg swapped from the babel box should be sent to a miner?

I can see a scenario were the price of a certain token spikes above the rate settled on R5, and anyone can make use of babel boxes for arbitrage. I don't know if this is something desired.

PS: I'm on my first steps on ErgoScript so It's almost certain that I'm missing something. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's ok. Babel fee box is in reality just an order book order to sell ERG in exchange for specified tokens at specified price. The babel fee box creator doesn't care about how these ERGs are spent - he earned revenue by selling these ERGs for a higher price than the ordinary market price (see text about financial incentives in this EIP). It's fully up to him to monitor whether he still wants to sell ERGs for the price specified in the R5, in case he no longer wants doing so, he can withdraw all funds from his babel fee box (and recreate it with new price in R5 in case he just wanted to update the price).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There can be coded offchain bots which will monitor current market price for some token and always recreate babel fee box with price equal to market price times some constant (in case that it would be worth spending ERG to pay fees for box recreation).

```
Compilation of babel fee box smartcontracts for different tokens of interest will result in different P2S addresses, leading to each token having a unique corresponding P2S babel fee box smartcontract address.

Parameters (creator’s pubKey, price) are specified via registers, meaning the resulting babel fee boxes from different creators will always belong to the same P2S address, which will improve their searchability.

#### Parameters:
1. Register R4
* type: SigmaProp
* value: creator's pubKey
2. Register R5
* type: Long
* value: how much nanoErgs is the creator willing to pay for one babel token

Babel fee box creator is able to spent the babel box in any circumstances.

Other users on the other hand can spend this box as input to their transaction only when they also recreate it as penultimate (second from the end) output of their transaction with the very same register (R4, R5) values together with insertion of a required amount of babel tokens (the amount of inserted babel tokens multiplied by the price specified in the R5 register has to be equal to or bigger than the amount of nanoErgs spent from the babel fee box).

Wallets implementation
--------------------------------------------

Wallet developers will need to decide whether they want to support EIP-0031 or not. If they do decide to support this standard, they should also decide on which tokens they want to support (this could be done based on user requirements – e.g. implementing big stablecoins or “meme” coins, etc.), as this could be more convenient than supporting all tokens.

As P2S addresses belonging to specific token of interest stay the same, these addresses could be easily “hardcoded” when supporting only a few tokens. If the developers decide to support any token, the previously mentioned smartcontract for each token which user holds should be compiled and the availability of babel fee boxes (UTXOs) for the specific tokens of interest in the blockchain should be subsequently checked.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As pointed out before, the P2S addresses can change when a new compiler version is used. The addresses for a token should be defined in the EIP to avoid having diverging addresses in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no need for that.

  • Address will change from token to token;
  • It's very unlikely to have compiler serializations changes, I could be wrong but it requires a hard fork to happen.

Copy link
Member

@aslesarenko aslesarenko Oct 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is some misconception, in the EIP text "smartcontract for each token which user holds should be compiled".
When we talk about smart contracts we should really mean contract templates (see EIP-5) or at least ErgoTree.

ErgoScript should be out of question as well as any compilation. Compilation should not be part of any on-chain protocol. First, it is unstable, second it is not standardized.

Contract Template capture the parameterized nature of smart contracts and ideally any App which want to support pre-defined list of contracts should actually use a predefined list of Contract Templates.

Potentially unlimited P2S addresses can belong to the same Contract Template.
At the same time, any protocol usually require fixed list of Contract Tempates which can be hard-coded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't know about EIP-5, contract template sounds like a great addition to have on EIP text. Thanks for the clarification )

Copy link
Member

@MrStahlfelge MrStahlfelge Nov 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no need for that.

I see a high need for a more defined approach, as the contract in the EIP does not compile for me. So changes are needed on my side to get it working, and the chances are high that I end up with another ergotree than others.
Maybe EIP-5 is a solution as well.

Btw, ergotree I came up with is 100604000e20 tokenId 0400040005000500d803d601e30004d602e4c6a70408d603e4c6a7050595e67201d804d604b2a5e4720100d605b2db63087204730000d606db6308a7d60799c1a7c17204d1968302019683050193c27204c2a7938c720501730193e4c672040408720293e4c672040505720393e4c67204060ec5a796830201929c998c7205029591b1720673028cb272067303000273047203720792720773057202

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contract fixes pushed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is your ergo tree the same?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What stops us from adding it to the eip? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Contract Template" subsection added


The proposed babel fee smartcontract is quite general and does not impose much restriction for the transaction. It is therefore possible to transact some tokens while paying babel fees with another tokens, etc.

Once the wallet finds a babel fee box which could be used to pay required transaction fees, it should calculate the required price for the transaction fee and present it to the user, so he can decide on using this particular option or not.

The wallet should also check current mempool and determine whether there exists someone who is currently trying to spend this specific babel fee box. In that case, the wallet should construct “chained” transaction (using the mempool’s transaction penultimate output (recreated babel fee output) as the new babel fee box input). This way, many transactions spending “the same” box could be chained and mined inside a single block. There can also occur a situation when the babel fee box owner is trying to spend his own box. In that case, the wallet should select another babel fee box, if available.

Once the wallet successfully crafts and relays the transaction to the mempool, it MUST keep an eye on the transaction until it is mined. This is important because the wallet cannot prevent somebody else from trying to spend the exact same babel fee box as our user’s transaction is trying to spend. When there are two transactions trying to spend the same box, only one from them can be mined and therefore included in the blockchain, while the other one has to be recrafted with the new babel fee box and relayed again to the network. Such thing should occur rarely, but when it does, the wallet has to be able to handle the situation correctly while notifying the user that the transaction did not go through.

ErgoPlayground
--------------------------------------------
Try example use case scenario [here](https://scastie.scala-lang.org/uboAMwSkSguOfSem8II59g).
Example use case scenario source code (for ErgoPlayground):
```scala
/* +----------------------------------+ */
/* | EIP-0031 - Babel Fees | */
/* | eip31proposal.scala | */
/* | (c)copyright nitram147 2022 | */
/* +----------------------------------+ */

// This file contains reference implementation of EIP-0031 proposal in Scala & ErgoScript for "Ergo Playground"
// (Smartcontract together with example use case scenario)
// For more detailed information please see EIP-0031 specification.

import org.ergoplatform.compiler.ErgoScalaCompiler._
import org.ergoplatform.playgroundenv.utils.ErgoScriptCompiler
import org.ergoplatform.playground._

val blockchainSim = newBlockChainSimulationScenario("Babel Fees scenario")

// create new token which will be used as the "babel token"
val tokenToBeUsed = blockchainSim.newToken("TKN")

// smartcontract protecting babel fees UTXO
// R4 contains creator's (primary token for babel tokens "seller") public key in SigmaProp format
// R5 contains price in Long format (price is the amount of nanoErgs which is creator willing to pay for one babel token)
// creator can spend this UTXO anytime,
// babel fees user can spend it as input in transaction while recreating UTXO as penultimate output of his transaction
val babelFeeScript = s"""
{

val babelFeeBoxCreator = SELF.R4[SigmaProp].get

val recreatedBox = OUTPUTS(OUTPUTS.size - 2)

val babelFeeBoxRecreated = (
recreatedBox.propositionBytes == SELF.propositionBytes &&
recreatedBox.R4[SigmaProp].get == SELF.R4[SigmaProp].get &&
recreatedBox.R5[Long].get == SELF.R5[Long].get &&
recreatedBox.tokens(0)._1 == tokenId
)

val nanoErgsDifference = SELF.value - recreatedBox.value
val babelTokensBefore = if(SELF.tokens.size > 0){ SELF.tokens(0)._2 }else{ 0L }
val babelTokensDifference = recreatedBox.tokens(0)._2 - babelTokensBefore
val exchangeOK = babelTokensDifference * SELF.R5[Long].get >= nanoErgsDifference

sigmaProp(
babelFeeBoxCreator ||
(
babelFeeBoxRecreated && exchangeOK && (nanoErgsDifference >= 0)
)
)
}
""".stripMargin

val babelFeesContract = ErgoScriptCompiler.compile(Map("tokenId" -> tokenToBeUsed.tokenId), babelFeeScript)

// definition of example use case participants
val alice = blockchainSim.newParty("alice (sender)")
val bob = blockchainSim.newParty("bob (receiver)")
val carol = blockchainSim.newParty("carol (babel fees exchange provider)")

val aliceInitialNanoErgs = MinErg // alice has minimal amount of ergs
val carolInitialNanoErgs = 1100000000L // 1.1 Erg

// amount of nanoErgs in carol's babel fee box after creation
val carolsBabelFeeBoxInitialValue = 1000000000L // 1 Erg
// how much nanoErgs is carol willing to pay for one babel token
val carolsBabelFeeBoxExchangePrice = 1000000L

val babelTokensInitialAmount = 100000L

// generate box containg babel tokens for alice
alice.generateUnspentBoxes(toSpend = aliceInitialNanoErgs, tokensToSpend = List(tokenToBeUsed -> babelTokensInitialAmount))
alice.printUnspentAssets()

// bob doesn't have anything in this moment
bob.printUnspentAssets()

carol.generateUnspentBoxes(toSpend = carolInitialNanoErgs)
carol.printUnspentAssets()

val carolsBabelFeeBox = Box(
value = carolsBabelFeeBoxInitialValue,
registers = Map(
R4 -> carol.wallet.getAddress.pubKey,
R5 -> carolsBabelFeeBoxExchangePrice
),
script = babelFeesContract
)

val carolFeeBoxCreate = Transaction(
inputs = carol.selectUnspentBoxes(toSpend = carolInitialNanoErgs),
outputs = List(carolsBabelFeeBox),
fee = MinTxFee,
sendChangeTo = carol.wallet.getAddress
)

println("--------------------------------------------")
println("Carol's babel fees box creation transaction:")
println(carolFeeBoxCreate)

val carolFeeBoxCreateSigned = carol.wallet.sign(carolFeeBoxCreate)
blockchainSim.send(carolFeeBoxCreateSigned)

carol.printUnspentAssets()

// Alice is willing to send some tokens to Bob, however she doesn't want to spend any ergs
// (she even couldn't becuase she owns only MinErg amount of Ergs)
// Alice will use Carol's babel fee box to cover the ergs needed for her transaction
// in exchange for a few of her babel tokens

// how many babel tokens is alice willing to pay for transaction
val aliceBabelFee = 50L
val aliceKeepTokensAmount = 10000L
val toBobTokensAmount = babelTokensInitialAmount - aliceKeepTokensAmount - aliceBabelFee

val toBobBox = Box(
value = MinErg,
token = (tokenToBeUsed -> toBobTokensAmount),
script = contract(bob.wallet.getAddress.pubKey),
)

val aliceChangeBox = Box(
value = aliceInitialNanoErgs,
token = (tokenToBeUsed -> aliceKeepTokensAmount),
script = contract(alice.wallet.getAddress.pubKey)
)

// MinErg is required for creating aliceChangeBox box
// (because the MinErgs from the Alice's origin box was used for creation of toBobBox)
val consumeNanoErgs = MinErg + MinTxFee

val babelFeeBoxReCreated = Box(
value = carolsBabelFeeBoxInitialValue - consumeNanoErgs,
token = (tokenToBeUsed -> aliceBabelFee),
registers = Map(
R4 -> carol.wallet.getAddress.pubKey,
R5 -> carolsBabelFeeBoxExchangePrice
),
script = babelFeesContract
)

val aliceToBob = Transaction(
inputs = (
alice.selectUnspentBoxes(toSpend = aliceInitialNanoErgs, tokensToSpend = List(tokenToBeUsed -> babelTokensInitialAmount))
++
List(carolFeeBoxCreateSigned.outputs(0))
),
outputs = List(toBobBox, aliceChangeBox, babelFeeBoxReCreated),
fee = MinTxFee,
)

println("--------------------------------------------")
println("Alice to Bob babel tokens transaction:")
println(aliceToBob)

val aliceToBobSigned = alice.wallet.sign(aliceToBob)
blockchainSim.send(aliceToBobSigned)

println("--------------------------------------------")
println("Current state of participants accounts:")
alice.printUnspentAssets()
bob.printUnspentAssets()
carol.printUnspentAssets()

// Bob now wants to send some tokens back to Alice, he also don't have Ergs required for paying transaction fees,
// so he will also use Carol's babel fee box to cover transaction fees

val bobSendBackTokensAmount = 10000L
// Bob decides to pay less than Alice, however it's still sufficient to cover Carol's price requirements
val bobBabelFee = 4L

val toAliceBox = Box(
value = MinErg,
token = (tokenToBeUsed -> bobSendBackTokensAmount),
script = contract(alice.wallet.getAddress.pubKey)
)

val bobChangeBox = Box(
value = MinErg,
token = (tokenToBeUsed -> (toBobTokensAmount - bobSendBackTokensAmount - bobBabelFee)),
script = contract(bob.wallet.getAddress.pubKey)
)

// Bob is also creating one new box so he needs also additional MinErg
val againConsumeNanoErgs = MinErg + MinTxFee

val babelFeeBoxReCreatedAgain = Box(
value = carolsBabelFeeBoxInitialValue - consumeNanoErgs - againConsumeNanoErgs,
token = (tokenToBeUsed -> (aliceBabelFee + bobBabelFee)),
registers = Map(
R4 -> carol.wallet.getAddress.pubKey,
R5 -> carolsBabelFeeBoxExchangePrice
),
script = babelFeesContract
)

val bobToAlice = Transaction(
inputs = List(aliceToBobSigned.outputs(0), aliceToBobSigned.outputs(2)),
outputs = List(toAliceBox, bobChangeBox, babelFeeBoxReCreatedAgain),
fee = MinTxFee
)

println("--------------------------------------------")
println("Bob to Alice babel tokens transaction:")
println(bobToAlice)

val bobToAliceSigned = bob.wallet.sign(bobToAlice)
blockchainSim.send(bobToAliceSigned)

// Carol has now dediced that he wants to withdraw his babel tokens earnings, so he's going to destroy his babelFeeBox

val extractTokensBox = Box(
value = MinErg,
token = (tokenToBeUsed -> (aliceBabelFee + bobBabelFee)),
script = contract(carol.wallet.getAddress.pubKey)
)

val carolDestroyFeeBox = Transaction(
inputs = List(bobToAliceSigned.outputs(2)),
outputs = List(extractTokensBox),
fee = MinTxFee,
sendChangeTo = carol.wallet.getAddress
)

println("--------------------------------------------")
println("Carol's fee box destroy transaction:")
println(carolDestroyFeeBox)

val carolDestroyFeeBoxSigned = carol.wallet.sign(carolDestroyFeeBox)
blockchainSim.send(carolDestroyFeeBoxSigned)

println("--------------------------------------------")
println("Final state of participants' accounts is:")
alice.printUnspentAssets()
bob.printUnspentAssets()
carol.printUnspentAssets()

```