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-0018 ErgoFund #44

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the
| [EIP-0005](eip-0005.md) | Contract Template |
| [EIP-0006](eip-0006.md) | Informal Smart Contract Protocol Specification Format |
| [EIP-0017](eip-0017.md) | Proxy Contracts |
| [EIP-0018](eip-0018.md) | ErgoFund contracts |
| [EIP-0022](eip-0022.md) | Auction Contract |
387 changes: 387 additions & 0 deletions eip-0018.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
# ErgoFund contracts

* Author: code-for-uss, kushti
* Status: Proposed
* Created: 5-Nov-2021
* License: CC0
* Forking: not needed

## Motivation

Collecting funds in different contexts is needed to build common infrastructure and applications in the Ergo ecosystem. Therefore, this EIP is proposing contracts and standardized box formats for announcing crowdfunding campaigns and collecting funds.

## Overall Design

The design of the crowdfunding contracts and box templates below is centered by a seamless on-chain and off-chain code built on top of the Ergo Playgrounds, [ErgoFund scenarios via playground](https://scastie.scala-lang.org/TWlB0hncT5SdYcTax3Wj5A).

There is an old version of ErgoFund eip [here](#old-ergofund-contracts), prepared by Kushti.

## Tokens

The ErgoFund has the following types of tokens.

| Token | Issued quantity | purpose | where stored |
|---|---|---|---|
|ControlBoxNFT | 1 | Identify control box | Any |
|SellNFT | 1 | Identify sell box | Sell box |
|Registration tokens | 100 million | Identify each campaign box | Sell box <br /> Campaign boxes |
|Campaign participant tokens | 100 million per campaign | Identify campaign and each pledge box| Capmaign box <br /> pledge boxes |

## Boxes

There are a total of 3 contracts, and each corresponds to a box-type + control box.

| Box | Quantity | Tokens | Additional registers used | Purpose | Spending transactions |
|---|---|---|---|---|---|
|Control | 1 | ControlBoxNFT | R4: Price (Long), <br /> R5: address[script] (SigmaProp) | Control price and address to pay for campaign registration | <div align="center">-</div> |
|Sell| 1 | SellNFT <br /> Registration tokens | <div align="center">-</div> | Register a new campaign | registerCampaign, <br/> returnRegistrationToken |
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would you need to return the registration token since this box is already a bottleneck for starting crowdfunding? I guess you could burn the token instead or send it to the false script.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am opposed to the idea of having one sell box. There is no reason for that in this case (unlike cases like SigmaUSD bank box)
No reason to create a bottleneck when there is no gain.

|Campaign | 0 - 100 million | Registration token <br /> Campaign participant tokens | R4: Campaign description (Coll[Byte]), <br /> R5: Public key of project owner (SigmaProp), <br /> R6: Campaign deadline (Int), <br /> R7: minToRaise (Long), <br /> R8 (optional): campaign Proposal CID (Coll[Byte]) | Store campaign data and collect funds | Donate to campaign, <br /> Withdraw funds, <br /> Refund donates, <br /> Return registrationToken |
|pledge | 0 - 100 million (per campaign) | Campaign participant tokens | R4: Donate value (Long), <br /> R5: Public key of backer (SigmaProp) | Store donate value and Backer public key | Refund donate (fundraising failure) |

## Contracts

### Pledge Contract

This box can only be spent if the first input box, which is the campaign box related to this pledge. Which means the campaign has failed.
(Conditions: fund collected for the project is less than the defined minToRaise and the height of the network exceeds the fundRaisingDeadline), also, in this case, the fee of funds' returning will be deducted from the backer's fund.<br />
Copy link
Contributor

Choose a reason for hiding this comment

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

It can be considered to let the campaign owner finish the campaign if funds are raised even before the deadline.


- Registers:
* R4: Amount of donates [donateValue] (Long)
* R5: The public key of backer [backerPubKey] (SigmaProp)
- Tokens:
1. campaignParticipantToken NO: [1] <br/>
We use this token as campaignId.

```scala
{
val donateValue = SELF.R4[Long].get
val backerPubKey = SELF.R5[SigmaProp].get

val campaignId = SELF.tokens(0)._1
val campaignBox = INPUTS(0)
val refundBox = OUTPUTS(1)
val fundraisingFailure = campaignBox.tokens(0)._1 == campaignRegistrationTokenId &&
campaignBox.tokens(1)._1 == campaignId && // verify campaign id
refundBox.value >= donateValue + SELF.value - minFee && // ensure full refund
refundBox.propositionBytes == backerPubKey.propBytes // propositionBytes of refundBox must equal to backerPubKey
sigmaProp(fundraisingFailure)
}
```
### Campaign Contract
Copy link
Contributor

Choose a reason for hiding this comment

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

I am strongly against the idea of having campaign tokens, at least in this way.
The point of crowdfunding is to be able to collect many possibly small funds. Now we are making the campaign box a bottleneck for donating to the campaign and I see no gain for that.

With the current design, there is no point in having a separate campaign box and pledge box. You can easily get all funds in the campaign box and give the equivalent amount of token to users for a later possible refund. I am proposing to do that since this also suffers from the issue I explained above -- I am making the point that donating should not go through the campaign box.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have previously proposed an idea to @kushti about how to have campaign IDs. So if the point of having a campaign token is that, there are simpler solutions to that.

"For the ErgoFund contracts, I think the campaign ID should be controlled by the sell contract otherwise someone can mess with the UI and potentially impersonate a campaign" in a chat with kushti.

Copy link
Member

Choose a reason for hiding this comment

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

I concur @anon-real, moreover, I'm wondering if there is an option to design the whole system without the accumulation in the campaign box.
Of the top of my head:
The campaign starts with a box with height and target amount in the registers. The box value is the fee to the ErgoFund service. Box id is used as campaign id or via purchased campaign token bought from ErgoFund.
Backers create a donation box that either can be spent by the campaign owner upon height and target donation amount requirements are met or by the backers themselves if the campaign is failed.
ErgoFund service is funded from the campaign box value and optionally by taking a % from donations (enforced in donation box contract).

There are four scenarios to spend the boxes protected by this contract.

- Registers:
* R4: Campaign description [validCampaignDesc] (Coll[Byte])
* R5: The public key of the project owner [projectPubKey] (SigmaProp)
* R6: Deadline of campaign [fundRaisingDeadline] (Int)
* R7: Minimum value needed for success campaign [minToRaise] (Long)
* R8: The CID of campaign proposal on IPFS [campaignProposalCID] (Coll[Byte])
- Tokens:
1. registrationToken NO: [1] <br/>
Each campaign should buy one registrationToken from the Sell contract
2. campaignParticipantToken NO: [This can set manually when creating each campaign] <br/>
After donating, Backer gets this token protected in the pledge contract. In other words, we use this token as campaignId.


```scala
{
val selfOut = OUTPUTS(0)
val campaignRegistrationTokenId = SELF.tokens(0)._1
val participantToken = SELF.tokens(1)
val campaignDesc = SELF.R4[Coll[Byte]]
val projectPubKey = SELF.R5[SigmaProp].get
val fundRaisingDeadline = SELF.R6[Int].get // height
val minToRaise = SELF.R7[Long].get
val campaignProposalCID = SELF.R8[Coll[Byte]]
val raisedValue = SELF.value - minFee

val preservedData = SELF.propositionBytes == selfOut.propositionBytes &&
campaignDesc == selfOut.R4[Coll[Byte]] &&
projectPubKey == selfOut.R5[SigmaProp].get &&
fundRaisingDeadline == selfOut.R6[Int].get &&
minToRaise == selfOut.R7[Long].get &&
campaignProposalCID == selfOut.R8[Coll[Byte]]

val campaignCondditions = if (HEIGHT <= fundRaisingDeadline) {
// Donate to campaign:
// The main spending path ensures that the box can be spent in a transaction producing at least two boxes:
// * One of the outputs, New CampaignBox, preserves data (registers) and token of registrationToken and holds the remaining campaignParticipantTokens (campaignParticipantTokenAmount - 1). Finally, the value of this box should be increased with the amount of donation if there is at least one token left in each campaign box after donation.
// * The other output, PledgeBox, with the value of minErg, stores the donation amount and the PubKey of Backer. Thus this box is protected under the Pledge contract.

val pledgeBox = OUTPUTS(1)
val minTokenPreserved = 1L
val donateAmount = selfOut.value - SELF.value
selfOut.tokens(0)._1 == campaignRegistrationTokenId &&
selfOut.tokens(1)._1 == participantToken._1 &&
selfOut.tokens(1)._2 >= minTokenPreserved &&
selfOut.tokens(1)._2 == participantToken._2 - 1 &&
donateAmount >= minErg &&
preservedData &&
pledgeBox.propositionBytes == pledgePropBytes &&
pledgeBox.value == minErg &&
pledgeBox.tokens(0)._1 == participantToken._1 &&
pledgeBox.tokens(0)._2 == 1L &&
pledgeBox.R4[Long].get == donateAmount && // Donate value
pledgeBox.R5[SigmaProp].isDefined // Backer PubKey
}
else if (HEIGHT > fundRaisingDeadline && !(raisedValue == 0)) {
if( raisedValue >= minToRaise ) {
// Withdrawal of funds if the campaign is successful:
// In this scenario HEIGHT should be more than fundRaisingDeadline, and also raisedValue (SELF.value - minFee) should be more than minToRaise.
// The main spending path ensures that the box can be spent in a transaction producing at least two boxes:
// * New SellBox returns registrationToken to the Sell contract
// * ProjectBox returns funds to the projectPubKey (R5)
// The fee of transaction in this scenario must be first obtained from the campaign creator

val inSellBox = INPUTS(1)
val outProjectBox = OUTPUTS(1)
inSellBox.tokens(0)._1 == sellNFTId &&
outProjectBox.value >= raisedValue &&
outProjectBox.propositionBytes == projectPubKey.propBytes
}
else {
// Funds' returning if the campaign is failed:
// In this scenario HEIGHT should be more than fundRaisingDeadline and raisedValue (SELF.value - minFee) should be less than minToRaise.
// The main spending path ensures that the box can be spent in a transaction producing at least two boxes:
// * One of the outputs, New CampaignBox, preserves data (registers) and token of registrationToken and holds the remaining campaignParticipantTokens (campaignParticipantTokenAmount + 1).
// Finally, the value of this box should be decreased with the amount of donation (donateValue R4 stored in pledge box)
// * BackerBox return funds to the backerPubKey (R5 of pledge box)
// The fee of transaction in this scenario will be deducted from the backer donation

val inPledgeBox = INPUTS(1)
preservedData &&
inPledgeBox.propositionBytes == pledgePropBytes &&
selfOut.tokens(0)._1 == campaignRegistrationTokenId &&
selfOut.tokens(1)._1 == participantToken._1 &&
selfOut.tokens(1)._2 == participantToken._2 + 1 &&
selfOut.value >= SELF.value - inPledgeBox.R4[Long].get
}
}
else {
// Return registrationToken to the Sell contract:
// In this scenario, HEIGHT should be more than fundRaisingDeadline, and also raisedValue (SELF.value - minFee) should be equal to zero.
// The main spending path ensures that the box can be spent in a transaction producing at least one boxes:
// * New SellBox with registrationTokens + 1 protected under sell contract

val inSellBox = INPUTS(1)
raisedValue == 0 &&
inSellBox.tokens(0)._1 == sellNFTId
}
sigmaProp(campaignCondditions)
}
```

### Sell Contract
There are two ways to spend the boxes protected by this contract:
* To register campaign, first, it is needed to pass `control box` as a **data input**.
- Tokens:
1. sellNFT NO: [1] <br/>
Sell NFT used for sell box detection
2. crowdfunding registrationToken NO: [Manually]
```scala
{
val selfOut = OUTPUTS(0)
val validNFT = selfOut.tokens(0)._1 == sellNFTId
val tokenId = selfOut.tokens(1)._1 // token the contract is selling
val validOutTokenId = tokenId == campaignRegistrationTokenId
val inTokensCount = SELF.tokens(1)._2
val outTokensCount = selfOut.tokens(1)._2
val validScript = SELF.propositionBytes == selfOut.propositionBytes
val validValue = selfOut.value >= SELF.value

val inCampaignBox = INPUTS(0)
val sellConditions = if (inCampaignBox.tokens.size == 2 && inCampaignBox.tokens(0)._1 == tokenId && inCampaignBox.tokens(0)._2 == 1){
// Return registration token after the end of the campaign (successful or unsuccessful):
// The main spending path ensures that the box can be spent in a transaction producing at least one boxes also in this case raisedValue should be equal zero:
// * A New SellBox, preserves sellNFT token, and also puts registrationToken to the box (registrationTokenAmount + 1).
// The fee of transaction in this scenario must be first obtained from the campaign creator.

validNFT &&
validOutTokenId &&
(outTokensCount == inTokensCount + 1) &&
validScript &&
validValue
}
else {
// Register a campaign:
// The main spending path ensures that the box can be spent in a transaction producing at least three boxes:
// So to register campaign, it is needed to pass control box as a data input
// * New SellBox output preserves sellNFT token and puts remaining registrationToken to the box (registrationTokenAmount - 1).
// * The output, New CampaignBox, with minFee value(needed registers for campaign box which is explained above) and registrationToken and campaignParticipantTokens, which are protected under campaign script.
// * The output, RewardBox, with the value of the price of campaign registration (controlBox.R4[Long].get) which is protected under the ErgoFund publicKey (controlBox.R5[SigmaProp].get)
val controlBox = CONTEXT.dataInputs(0)
// Check control box NFT
val properControlBox = (controlBox.tokens(0)._1 == controlBoxNFTId)

val price = controlBox.R4[Long].get
val script = controlBox.R5[SigmaProp].get

val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1)

val rewardOut = OUTPUTS(1)
val validPayment = rewardOut.value >= price && rewardOut.propositionBytes == script.propBytes

val campaignOut = OUTPUTS(2)
val validCampaign = campaignOut.propositionBytes == campaignPropBytes &&
campaignOut.tokens.size == 2 &&
campaignOut.tokens(0)._1 == tokenId &&
campaignOut.value >= minFee &&
campaignOut.R4[Coll[Byte]].isDefined &&
campaignOut.R5[SigmaProp].isDefined &&
campaignOut.R6[Int].isDefined &&
campaignOut.R7[Long].isDefined

properControlBox && validTokens && validScript && validPayment && validCampaign && validValue
}
sigmaProp(sellConditions)
}
```

## TO-DO
* Contracts to collect funds in SigUSD and other tokens.


# Old ErgoFund contracts
This is inital version of ErgoFund contracts prepared by Kushti.

## Overall Design

The design of the crowdfunding contracts and box templates below is centered around
efficiency of blockchain scanning by an offchain application (backend of ErgoFund service built on top of the Scanner,
https://github.com/ergoplatform/scanner ).

## Campaign Registration

Crowdfunding campaign registration is controlled by a control box associated with an NFT which registers R4 and R5
contain registration price and address (script) to pay for campaign registration.

Control box:

Contains campaign registration price in register R4 (as long value) and script to pay for registration in register R5
(as SigmaProp constant).

To register a new crowdfunding campaign, crowdfunding token must be bought (to compensate expenses for scanning,
storing crowdfunding data, producing and maintaining UI).

Sell contract:

```scala
{
val controlBox = CONTEXT.dataInputs(0)

val firstUnusedCampaignId = SELF.R4[Int].get

// check control box NFT
val properControlBox = controlBox.tokens(0)._1 == fromBase64("csP7zjJD1JHYHrVkzasWYrH41MfjEriIcM7Hm3z9QyE=")

val price = controlBox.R4[Long].get
val script = controlBox.R5[SigmaProp].get


val inTokensCount = SELF.tokens(1)._2

val selfOut = OUTPUTS(0)

val validNFT = selfOut.tokens(0)._1 == fromBase64("FbCuQcJCMAaf+W2susCTKFCsDCoJJNr3KjnojLzzrNU=")
val tokenId = selfOut.tokens(1)._1 // token the contract is selling
val validOutTokenId = tokenId == fromBase64("BbZrl+WAL2RHtn/jDLQFXhTWsXuxT19WPWXJYixDplk=")
val outTokensCount = selfOut.tokens(1)._2

val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1)

val validScript = SELF.propositionBytes == selfOut.propositionBytes

val validCampaignIdUpdate = selfOut.R4[Int].get == (firstUnusedCampaignId + 1)

val rewardOut = OUTPUTS(1)
val validPayment = rewardOut.value >= price && rewardOut.propositionBytes == script.propBytes

val campaignOut = OUTPUTS(2)
val validCampaign = campaignOut.tokens(0)._1 == tokenId && campaignOut.R4[Int].get == firstUnusedCampaignId


properControlBox && validTokens && validScript && validPayment && validCampaignIdUpdate && validCampaign
}
```

So to register campaign, one need to pass control box as a data input, sell contract among inputs, and create a box with
campaign box data specified below in outputs, as well as an updated campaign token sale box.


Control box NFT id: 72c3fbce3243d491d81eb564cdab1662b1f8d4c7e312b88870cec79b7cfd4321
Tokensale box NFT id: 15b0ae41c24230069ff96dacbac0932850ac0c2a0924daf72a39e88cbcf3acd5
Campaign identification token: 05b66b97e5802f6447b67fe30cb4055e14d6b17bb14f5f563d65c9622c43a659

Tokensale box P2S address: yvNXjWe8vBvZTXwyUHemPU59CRfm8AnvnzXowRQkQ9hoiGXsS7oEUGLPof6RoYAdKXEgTWR4qTK44w3GBuNdBjXeWcHFeBvyFxHWdEBsnqnYhtaFC2S71eHRPpLEndDghZebLdx25nufFh8YFJ9D8gTxTvxqahBgzpJT7pbUAUEkE2iRSZwpiXvj5SUmA6vmTrQzMTxicBG2mGV1NLq5rjBCAxjM5FpNSJ1KgqrxeTAfBvs1QnMUZ6CTBtGPLyNUpFoPTaYUPp9PfBni7FAQsumx36tukUHA2p3NdgfhTbYYSJTDQi7eWW2YikFvczXpTCfecbCL7HA1o6Cg6DvntmpJHWfjopWnsbpDdLg6SbP4Fup7wCnQaeL6NcSTVP1K5btmK5JryZc36V8KdHkheyaVzYTMA1Gufp6bAGtZP

## Campaign Box Data

For registering campaign, it is enough just to create box with ErgoFund campaign token, proper campaign ID, campaign desc, script, recommended deadline, min to raise. Then offchain scanner will find it (and so UI will be able to display campaign data).

For the scanner, there are following requirements campaign box should satisfy:

value >= 1 ERG (1000000000 nanoERG)
script = "4MQyMKvMbnCJG3aJ" (false proposition, so no one can spend)

Registers:
*R4* - campaign ID (Int)
*R5* - campaign desc (byte array)
*R6* - campaign script (funds collected will go to this)
*R7* - fundraising deadline (Int, # of block, exclusive)
*R8* - min value for successful fundraising (Long)

https://explorer.ergoplatform.com/en/transactions/2e25bc0ea4d01108ab1cd76969f49022228b533a2ea50540f6cde6258029a510


example:

```json
[
{
"address": "4MQyMKvMbnCJG3aJ",
"value": 100000000,
"assets": [
{
"tokenId": "05b66b97e5802f6447b67fe30cb4055e14d6b17bb14f5f563d65c9622c43a659",
"amount": 1
}
],
"registers": {
"R4": "0400",
"R5": "0e00",
"R6": "08cd0327e65711a59378c59359c3e1d0f7abe906479eccb76094e50fe79d743ccc15e6",
"R7": "04a0be49",
"R8": "0580d0acf30e"
}
}
]
```

## Pledge Contract

Pledge contract is an example of self-sovereign DeFi principle. The contract is allowing box being protected by it to be spent if a spending transaction can collect at least *minToRaise* ERGs to *projectPubKey*, and inclusion height of the transaction is less than *deadline*. Otherwise, after *deadline* height is met, funds can be claimed by *projectPubKey*. Please note that both *backerPubKey* and
*projectPubKey* can be arbitrary scripts.

```scala
{
val campaignId = SELF.R4[Int].get
val backerPubKey = SELF.R5[SigmaProp].get
val projectPubKey = SELF.R6[SigmaProp].get
val deadline = SELF.R7[Int].get // height
val minToRaise = SELF.R8[Long].get

val fundraisingFailure = HEIGHT >= deadline && OUTPUTS(0).propositionBytes == backerPubKey.propBytes && OUTPUTS(0).value >= SELF.value
val enoughRaised = {(outBox: Box) => outBox.value >= minToRaise && outBox.propositionBytes == projectPubKey.propBytes && outBox.R4[Int].get == campaignId}

val fundraisingSuccess = HEIGHT < deadline && enoughRaised(OUTPUTS(0))
fundraisingFailure || fundraisingSuccess
}
```

address: XUFypmadXVvYmBWtiuwDioN1rtj6nSvqgzgWjx1yFmHAVndPaAEgnUvEvEDSkpgZPRmCYeqxewi8ZKZ4Pamp1M9DAdu8d4PgShGRDV9inwzN6TtDeefyQbFXRmKCSJSyzySrGAt16

*R4* - campaign ID (Int)
*R5* - backer script (SigmaProp)
*R6* - campaign script (funds collected will go to this) (SigmaProp)
*R7* - fundraising deadline (Int, # of block, inclusive) (Int)
*R8* - min value for successful fundraising (Long)