diff --git a/eip-0035.md b/eip-0035.md new file mode 100644 index 00000000..f84b37f4 --- /dev/null +++ b/eip-0035.md @@ -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. \ No newline at end of file