-
Notifications
You must be signed in to change notification settings - Fork 34
EIP-0031 - Babel Fees #54
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
Changes from 3 commits
ad20164
446955c
93dc555
718e4cd
d0c4b62
7332f82
ee86d5f
349b5d5
af56cc7
6fb3449
81edece
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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/) | ||
|
|
||
| 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) | ||
|
||
|
|
||
| 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) | ||
| ) | ||
| ) | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 PS: I'm on my first steps on ErgoScript so It's almost certain that I'm missing something. :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
||
|
|
||
| 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() | ||
|
|
||
| ``` | ||
There was a problem hiding this comment.
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
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.