Skip to content
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

EIP-0035 Token Sale Contract #76

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions eip-0035.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Open Token Sale Contract

* Author: MrStahlfelge
* Status: Proposed
* Created: 29-August-2022
* Last edited: 29-August-2022
* License: CC0
* Forking: not needed

## Description

This EIP proposes an open token sell offer contract with the following features:

* partial sells are possible, seller can set a unit amount
* implementing UIs can set an optional UI fee that is paid on withdrawals of revenue
* contract is flexible and can be used by bots to match against other contracts within a transaction (but see section Caveats)
* the sell can be cancelled, which can also be used to withdraw already collected revenue

## Motivation

Ergo supports native tokens on the blockchain, a feature that is widely used by projects building
on Ergo and users as well. Naturally, projects want to sell their tokens to fund themselves, and
users want to buy and sell tokens.
Exchanging, purchasing and selling tokens is right now possible via the following ways:

* dApps that have not specified a standard open contract via an EIP
* [EIP-0014 DEX contacts](eip-0014.md) for AMM and order based DEX
* [EIP-0022 Auction contract](eip-0022.md) for auctioning a certain amount of tokens at best price, min price and immediate buy possible
* [EIP-0031 Babel fees](eip-0031.md), a token buy offer contract for a variable amount of tokens at a fixed price, partial buys possible

An open token sell contract is missing so far.

## Smart contract specification

Here is the smart contract's source code in ErgoScript which will be used to protect the offer sell box:
```scala
{
// R5: The seller's ergo tree
// R6: Coll[Long] defining token price per unit in nanoerg, unit
// R7: UI fee in thousand
// R8: UI fee ErgoTree
// R4: used for box recreation
val seller = SELF.R5[SigmaProp].get
val priceConfig = SELF.R6[Coll[Long]]
val pricePerUnit = priceConfig.get(0)
val unitAmount = priceConfig.get(1)
val uiFeePerThousand = SELF.R7[Long].getOrElse(0L)
val uiFeeAddressOptional = SELF.R8[Coll[Byte]]
val recreatedSellBoxIndex: Option[Int] = getVar[Int](0)

// token is bought
val isSold = if (recreatedSellBoxIndex.isDefined && SELF.tokens.size > 0) {
val tokenToSell = SELF.tokens(0)
val newSellBox = OUTPUTS(recreatedSellBoxIndex.get)
val tokenInSellBox: (Coll[Byte], Long) = if (newSellBox.tokens.size > 0) newSellBox.tokens(0) else (tokenToSell._1, 0L)
val tokensSold = tokenToSell._2 - tokenInSellBox._2
val unitsSold = tokensSold / unitAmount // this might be wrong when tokensSold can't be divided completely - thus we check again later

val ergAmountToPay = unitsSold * pricePerUnit

newSellBox.R5[SigmaProp].get == seller && // preserve...
newSellBox.R6[Coll[Long]].get(0) == pricePerUnit && //...the...
newSellBox.R6[Coll[Long]].get(1) == unitAmount && // ...original...
newSellBox.R7[Long].getOrElse(0L) == uiFeePerThousand && // ...box...
newSellBox.R8[Coll[Byte]] == uiFeeAddressOptional && // ...settings
newSellBox.propositionBytes == SELF.propositionBytes && // ...and script
unitsSold * unitAmount == tokensSold && // prevent rounding down attacks
tokenInSellBox._1 == tokenToSell._1 && tokenInSellBox._2 >= tokenToSell._2 - tokensSold && // preserve tokens
newSellBox.value >= (SELF.value + ergAmountToPay) && // check the actual payment
ergAmountToPay > 0 &&
newSellBox.R4[Coll[Byte]].get == SELF.id // prevent same price attack
} else false

// payout to seller
val isPayoutToSeller = {
val feeAmount = (SELF.value * uiFeePerThousand) / 1000
val feeBox = OUTPUTS(1)
val payoutBox = OUTPUTS(0)
payoutBox.propositionBytes == seller.propBytes &&
payoutBox.R4[Coll[Byte]].get == SELF.id &&
(feeAmount == 0 || SELF.value <= 1000000 || // in case of cancel with nothing sold no fee box is needed
feeBox.propositionBytes == uiFeeAddressOptional.get && feeBox.value >= feeAmount && feeBox.R4[Coll[Byte]].get == SELF.id) &&
payoutBox.value >= SELF.value - feeAmount - 1000000 // we allow tx fee to be paid
}

val isComplete = SELF.tokens.size == 0 // don't let others cancel the sale

sigmaProp(isSold || isPayoutToSeller && isComplete) || seller && sigmaProp(isPayoutToSeller)
}
```
The contract is located at address (inserted when EIP gets approved).

## Caveats
Due to [current semantics of accessing registers](https://github.com/ScorexFoundation/sigmastate-interpreter/issues/783),
transactions for buying tokens must not set `OUTPUTS(0).R4` or must set it to `Coll[Byte]`, even if
the new sell box is recreated at a different index. This is not a problem for withdrawals or
single buys, but might prevent some more complex transactions matching different types of token
exchange contracts into a single transaction.