From ff15e6d717eebaced26194f3b35a6666fd306f44 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Thu, 10 Jun 2021 16:46:50 +0300 Subject: [PATCH 01/14] ErgoFund EIP alpha version --- eip-0018.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 eip-0018.md diff --git a/eip-0018.md b/eip-0018.md new file mode 100644 index 00000000..7a3c953e --- /dev/null +++ b/eip-0018.md @@ -0,0 +1,64 @@ + +## Campaign Data + +//For campaign, it is enough just to create box with ErgoFund token, campaign ID, campaign desc, script, recommended deadline, min to raise. + +value >= 1 ERG (1000000000 nanoERG) +script = "4MQyMKvMbnCJG3aJ" (false proposition, so no one can spend) + +*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 succesful fundraising (Long) + +// https://explorer.ergoplatform.com/en/transactions/2e25bc0ea4d01108ab1cd76969f49022228b533a2ea50540f6cde6258029a510 + +test token ID: 08fc8bd24f0eaa011db3342131cb06eb890066ac6d7e6f7fd61fcdd138bd1e2c + +example: + +[ + { + "address": "4MQyMKvMbnCJG3aJ", + "value": 100000000, + "assets": [ + { + "tokenId": "08fc8bd24f0eaa011db3342131cb06eb890066ac6d7e6f7fd61fcdd138bd1e2c", + "amount": 1 + } + ], + "registers": { + "R4": "0400", + "R5": "0e00", + "R6": "08cd0327e65711a59378c59359c3e1d0f7abe906479eccb76094e50fe79d743ccc15e6", + "R7": "04a0be49", + "R8": "0580d0acf30e" + } + } +] + + +## Pledge Contract + +{ + 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 +*R6* - campaign script (funds collected will go to this) +*R7* - fundraising deadline (Int, # of block, exclusive) +*R8* - min value for succesful fundraising (Long) \ No newline at end of file From 809d522cc53b41d4ea350cc00122ed3765be1226 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Fri, 11 Jun 2021 20:48:04 +0300 Subject: [PATCH 02/14] motivation --- eip-0018.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 7a3c953e..7befb72b 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -1,7 +1,22 @@ +# ErgoFund contracts -## Campaign Data +* Author: kushti +* Status: Proposed +* Created: 11-June-2021 +* License: CC0 +* Forking: not needed -//For campaign, it is enough just to create box with ErgoFund token, campaign ID, campaign desc, script, recommended deadline, min to raise. +## Motivation + +Collecting funds in different contexts is very much needed for building common infrastructure and projects + in the Ergo ecosystem. This EIP is proposing contracts and standardized boxes formats for announcing + crowdfunding campaigns and collecting funds. + +## Campaign Registration + +## Campaign Box Data + +For campaign, it is enough just to create box with ErgoFund token, campaign ID, campaign desc, script, recommended deadline, min to raise. value >= 1 ERG (1000000000 nanoERG) script = "4MQyMKvMbnCJG3aJ" (false proposition, so no one can spend) @@ -12,7 +27,7 @@ script = "4MQyMKvMbnCJG3aJ" (false proposition, so no one can spend) *R7* - fundraising deadline (Int, # of block, exclusive) *R8* - min value for succesful fundraising (Long) -// https://explorer.ergoplatform.com/en/transactions/2e25bc0ea4d01108ab1cd76969f49022228b533a2ea50540f6cde6258029a510 +https://explorer.ergoplatform.com/en/transactions/2e25bc0ea4d01108ab1cd76969f49022228b533a2ea50540f6cde6258029a510 test token ID: 08fc8bd24f0eaa011db3342131cb06eb890066ac6d7e6f7fd61fcdd138bd1e2c @@ -61,4 +76,10 @@ address: XUFypmadXVvYmBWtiuwDioN1rtj6nSvqgzgWjx1yFmHAVndPaAEgnUvEvEDSkpgZPRmCYeq *R5* - backer script *R6* - campaign script (funds collected will go to this) *R7* - fundraising deadline (Int, # of block, exclusive) -*R8* - min value for succesful fundraising (Long) \ No newline at end of file +*R8* - min value for succesful fundraising (Long) + + +## TO-DO + +* Contracts to collect funds in SigUSD and other tokens. +* Contracts for campaigns with thousands of pledges. \ No newline at end of file From 2f72e7b8777c89688f956ea9d089e23f78f85aa3 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Sat, 12 Jun 2021 00:54:35 +0300 Subject: [PATCH 03/14] tokensale contract --- eip-0018.md | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 7befb72b..6f64dfa4 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -14,6 +14,42 @@ Collecting funds in different contexts is very much needed for building common i ## Campaign Registration +Sell contract: + +```scala +{ + val controlBox = CONTEXT.dataInputs(0) + + // check control box NFT + val properControlBox = controlBox.tokens(0)._1 == fromBase64("Q9z7qAx3AIz6MWMtmJ6Rk8CS/fk4HVXcyRgc7HjKtwA=") + + val price = controlBox.R4[Long].get + val script = controlBox.R5[Coll[Byte]].get + + + val inTokensCount = SELF.tokens(1)._2 + + val selfOut = OUTPUTS(0) + + val validNFT = selfOut.tokens(1)._1 == fromBase64("KPqq2cwwkO4DaGWJsIw5Zb4nHIa2UKiPNW5X2laIYvw=") + val validOutTokenId = selfOut.tokens(1)._1 == fromBase64("CPyL0k8OqgEdszQhMcsG64kAZqxtfm9/1h/N0Ti9Hiw=") + val outTokensCount = selfOut.tokens(1)._2 + + val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1) + + val validScript = SELF.propositionBytes == selfOut.propositionBytes + + val rewardOut = OUTPUTS(1) + val validPayment = rewardOut.value >= 10000000000L && rewardOut.propositionBytes == script + + properControlBox && validTokens && validScript && validPayment +} +``` + + +Control box NFT id: 43dcfba80c77008cfa31632d989e9193c092fdf9381d55dcc9181cec78cab700 +Token sale NFT id: 28faaad9cc3090ee03686589b08c3965be271c86b650a88f356e57da568862fc + ## Campaign Box Data For campaign, it is enough just to create box with ErgoFund token, campaign ID, campaign desc, script, recommended deadline, min to raise. @@ -33,6 +69,7 @@ test token ID: 08fc8bd24f0eaa011db3342131cb06eb890066ac6d7e6f7fd61fcdd138bd1e2c example: +```json [ { "address": "4MQyMKvMbnCJG3aJ", @@ -52,10 +89,11 @@ example: } } ] - +``` ## Pledge Contract +```scala { val campaignId = SELF.R4[Int].get val backerPubKey = SELF.R5[SigmaProp].get @@ -68,7 +106,8 @@ example: val fundraisingSuccess = HEIGHT < deadline && enoughRaised(OUTPUTS(0)) fundraisingFailure || fundraisingSuccess - } +} +``` address: XUFypmadXVvYmBWtiuwDioN1rtj6nSvqgzgWjx1yFmHAVndPaAEgnUvEvEDSkpgZPRmCYeqxewi8ZKZ4Pamp1M9DAdu8d4PgShGRDV9inwzN6TtDeefyQbFXRmKCSJSyzySrGAt16 From 6147201b8a161bc318eee2c8a8d19a7f94ad602a Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Tue, 22 Jun 2021 00:27:06 +0300 Subject: [PATCH 04/14] overall design and campaign reg text --- eip-0018.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/eip-0018.md b/eip-0018.md index 6f64dfa4..7484f177 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -12,8 +12,24 @@ Collecting funds in different contexts is very much needed for building common i in the Ergo ecosystem. This EIP is proposing contracts and standardized boxes formats for announcing crowdfunding campaigns and collecting funds. +## Overall Design + +The design of the crowdfunding contracts and box templates below is centered around +efficiency of blockchain scanning by an offchain application (implementing backend of ErgoFund service). + ## 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 (long value) and script to pay for registration on register R5 +(byte array). + +To register a new crowdfunding campaign, crowdfunding token must be bought (to compensate expenses for scanning, +storing crowdfunding data, doing an UI) + Sell contract: ```scala @@ -50,6 +66,9 @@ Sell contract: Control box NFT id: 43dcfba80c77008cfa31632d989e9193c092fdf9381d55dcc9181cec78cab700 Token sale NFT id: 28faaad9cc3090ee03686589b08c3965be271c86b650a88f356e57da568862fc +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. + ## Campaign Box Data For campaign, it is enough just to create box with ErgoFund token, campaign ID, campaign desc, script, recommended deadline, min to raise. @@ -61,7 +80,7 @@ script = "4MQyMKvMbnCJG3aJ" (false proposition, so no one can spend) *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 succesful fundraising (Long) +*R8* - min value for successful fundraising (Long) https://explorer.ergoplatform.com/en/transactions/2e25bc0ea4d01108ab1cd76969f49022228b533a2ea50540f6cde6258029a510 From c216706a76a8eb2c3fe9eb5a38d11bd66ce6e5b8 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Tue, 22 Jun 2021 17:27:58 +0300 Subject: [PATCH 05/14] tokensale contract fixes --- eip-0018.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 7484f177..8d09083e 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -47,7 +47,7 @@ Sell contract: val selfOut = OUTPUTS(0) - val validNFT = selfOut.tokens(1)._1 == fromBase64("KPqq2cwwkO4DaGWJsIw5Zb4nHIa2UKiPNW5X2laIYvw=") + val validNFT = selfOut.tokens(0)._1 == fromBase64("KPqq2cwwkO4DaGWJsIw5Zb4nHIa2UKiPNW5X2laIYvw=") val validOutTokenId = selfOut.tokens(1)._1 == fromBase64("CPyL0k8OqgEdszQhMcsG64kAZqxtfm9/1h/N0Ti9Hiw=") val outTokensCount = selfOut.tokens(1)._2 @@ -56,7 +56,7 @@ Sell contract: val validScript = SELF.propositionBytes == selfOut.propositionBytes val rewardOut = OUTPUTS(1) - val validPayment = rewardOut.value >= 10000000000L && rewardOut.propositionBytes == script + val validPayment = rewardOut.value >= price && rewardOut.propositionBytes == script properControlBox && validTokens && validScript && validPayment } From 08103d2c5d8c3080d2062b3ff31b0f27ab439025 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Tue, 22 Jun 2021 20:30:29 +0300 Subject: [PATCH 06/14] new test token ids --- eip-0018.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eip-0018.md b/eip-0018.md index 8d09083e..6b7ba253 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -64,7 +64,8 @@ Sell contract: Control box NFT id: 43dcfba80c77008cfa31632d989e9193c092fdf9381d55dcc9181cec78cab700 -Token sale NFT id: 28faaad9cc3090ee03686589b08c3965be271c86b650a88f356e57da568862fc +Token sale NFT id: 765f0123c14997e9f7b65530ba4e98b810541d23a65035c32aa1af56c9cbff04 +Campaign identification token: 1b1666211a4dc267f7aa3e38bcdb7a55d21311f9f24b05bb63834661710f4aa7 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. From 6f57caa7f3f162422ca420bbe85b18b88d237e34 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Thu, 22 Jul 2021 14:55:20 +0300 Subject: [PATCH 07/14] updating testing ids --- eip-0018.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 6b7ba253..7be97115 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -37,7 +37,7 @@ Sell contract: val controlBox = CONTEXT.dataInputs(0) // check control box NFT - val properControlBox = controlBox.tokens(0)._1 == fromBase64("Q9z7qAx3AIz6MWMtmJ6Rk8CS/fk4HVXcyRgc7HjKtwA=") + val properControlBox = controlBox.tokens(0)._1 == fromBase64("uuqgee/jRz3XRo5Vsg2pYceRVdqahII27Hpc8NdM+Zo=") val price = controlBox.R4[Long].get val script = controlBox.R5[Coll[Byte]].get @@ -47,8 +47,8 @@ Sell contract: val selfOut = OUTPUTS(0) - val validNFT = selfOut.tokens(0)._1 == fromBase64("KPqq2cwwkO4DaGWJsIw5Zb4nHIa2UKiPNW5X2laIYvw=") - val validOutTokenId = selfOut.tokens(1)._1 == fromBase64("CPyL0k8OqgEdszQhMcsG64kAZqxtfm9/1h/N0Ti9Hiw=") + val validNFT = selfOut.tokens(0)._1 == fromBase64("SKsyra9+sTLHbAd/3rPfamaSQXFo8VQcg6VUXBDGP4E=") + val validOutTokenId = selfOut.tokens(1)._1 == fromBase64("B6V6SJ0YdzStTJYFFP28sXm+rlgid0lUrBVkCF5kHc4=") val outTokensCount = selfOut.tokens(1)._2 val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1) @@ -63,9 +63,11 @@ Sell contract: ``` -Control box NFT id: 43dcfba80c77008cfa31632d989e9193c092fdf9381d55dcc9181cec78cab700 -Token sale NFT id: 765f0123c14997e9f7b65530ba4e98b810541d23a65035c32aa1af56c9cbff04 -Campaign identification token: 1b1666211a4dc267f7aa3e38bcdb7a55d21311f9f24b05bb63834661710f4aa7 +Control box NFT id: baeaa079efe3473dd7468e55b20da961c79155da9a848236ec7a5cf0d74cf99a +Token sale NFT id: 48ab32adaf7eb132c76c077fdeb3df6a6692417168f1541c83a5545c10c63f81 +Campaign identification token: 07a57a489d187734ad4c960514fdbcb179beae5822774954ac1564085e641dce + +Tokensale box P2S address: r5wDC8T2yMcZWW8UzJoEsUD6fKjxe89SQPWCEHKLkjZLYAxLdrxYnBGvqXZDu8bZsurA3SGVhVQjcXYwVbtT2g8i91PmGbD2XNuoxT43bM56RSFGHDuyAzgE9GUtBegYbcYQ2XWy6ks6wHdq3cnBRWzf3WkXuR4LyuuDNN1jkQQE5NBBJfpVD6Eyuh1sSApMoJxf6Hv5jJjsUEMEkpZwyqjXzQBR1KUcp8nw7D88w135PfnDQVTAqmRAim1YEk35dvNtqBDAqaJK1ukRdcfbNe5axr8BRsn4wR9jX3fpuoh9q2FkmyvAWj2VWqYgacFvD9LwBVpRZi7nNBWVtp1 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. @@ -85,7 +87,6 @@ script = "4MQyMKvMbnCJG3aJ" (false proposition, so no one can spend) https://explorer.ergoplatform.com/en/transactions/2e25bc0ea4d01108ab1cd76969f49022228b533a2ea50540f6cde6258029a510 -test token ID: 08fc8bd24f0eaa011db3342131cb06eb890066ac6d7e6f7fd61fcdd138bd1e2c example: @@ -135,7 +136,7 @@ address: XUFypmadXVvYmBWtiuwDioN1rtj6nSvqgzgWjx1yFmHAVndPaAEgnUvEvEDSkpgZPRmCYeq *R5* - backer script *R6* - campaign script (funds collected will go to this) *R7* - fundraising deadline (Int, # of block, exclusive) -*R8* - min value for succesful fundraising (Long) +*R8* - min value for successful fundraising (Long) ## TO-DO From 28c24095c8693ede6ecf40fdcaf0952993385504 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Sat, 24 Jul 2021 01:19:03 +0300 Subject: [PATCH 08/14] campaignId check added --- eip-0018.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 7be97115..39249f32 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -36,6 +36,8 @@ Sell contract: { val controlBox = CONTEXT.dataInputs(0) + val firstUnusedCampaignId = SELF.R4[Int].get + // check control box NFT val properControlBox = controlBox.tokens(0)._1 == fromBase64("uuqgee/jRz3XRo5Vsg2pYceRVdqahII27Hpc8NdM+Zo=") @@ -47,18 +49,25 @@ Sell contract: val selfOut = OUTPUTS(0) - val validNFT = selfOut.tokens(0)._1 == fromBase64("SKsyra9+sTLHbAd/3rPfamaSQXFo8VQcg6VUXBDGP4E=") - val validOutTokenId = selfOut.tokens(1)._1 == fromBase64("B6V6SJ0YdzStTJYFFP28sXm+rlgid0lUrBVkCF5kHc4=") + val validNFT = selfOut.tokens(1)._1 == fromBase64("SKsyra9+sTLHbAd/3rPfamaSQXFo8VQcg6VUXBDGP4E=") + val tokenId = selfOut.tokens(1)._1 // token the contract is selling + val validOutTokenId = tokenId == fromBase64("B6V6SJ0YdzStTJYFFP28sXm+rlgid0lUrBVkCF5kHc4=") 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 + val validPayment = rewardOut.value >= 10000000000L && rewardOut.propositionBytes == script + + val campaignOut = OUTPUTS(2) + val validCampaign = campaignOut.tokens(0)._1 == tokenId && campaignOut.R4[Int].get == firstUnusedCampaignId + - properControlBox && validTokens && validScript && validPayment + properControlBox && validTokens && validScript && validPayment && validCampaignIdUpdate && validCampaign } ``` @@ -67,7 +76,7 @@ Control box NFT id: baeaa079efe3473dd7468e55b20da961c79155da9a848236ec7a5cf0d74c Token sale NFT id: 48ab32adaf7eb132c76c077fdeb3df6a6692417168f1541c83a5545c10c63f81 Campaign identification token: 07a57a489d187734ad4c960514fdbcb179beae5822774954ac1564085e641dce -Tokensale box P2S address: r5wDC8T2yMcZWW8UzJoEsUD6fKjxe89SQPWCEHKLkjZLYAxLdrxYnBGvqXZDu8bZsurA3SGVhVQjcXYwVbtT2g8i91PmGbD2XNuoxT43bM56RSFGHDuyAzgE9GUtBegYbcYQ2XWy6ks6wHdq3cnBRWzf3WkXuR4LyuuDNN1jkQQE5NBBJfpVD6Eyuh1sSApMoJxf6Hv5jJjsUEMEkpZwyqjXzQBR1KUcp8nw7D88w135PfnDQVTAqmRAim1YEk35dvNtqBDAqaJK1ukRdcfbNe5axr8BRsn4wR9jX3fpuoh9q2FkmyvAWj2VWqYgacFvD9LwBVpRZi7nNBWVtp1 +Tokensale box P2S address: u1JSmVkJbNKB4uAzLp7NkCusTZmu5nC2ogyQJminm7MDjw4Db2ykMTQYr1mPvy7kghqqrNjFUQNZP6cpL6cApjghg8wgx5Dn246zqbwzgACnqQuijY7gQx5HZ2fEEZrWZ5mm84kgrBfA1NpXWmw8h1i2RNsDBnrhPbzcsq5DdXzydQt1sp9UBfHbRCDshLyasTxKKj5QzThWZYvTxTMcAxJfucnok3vC3LvgLGaa67VneJWNshaVwScwiXHXGtFFXSVqh2NqRjQUMse4Y64dec6A4G7gumtifiM9NbSxzPvr5EviiVL5qKxXGmqj8uJgxMGNUEkgNSdW2XZg9TXgRuVDf1PbPesh1kt6cau5kFaRGeoSdwkGmpJx8reiuH2mMSqe7VaeqfqSbMirUG2vh7He47 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. @@ -135,7 +144,7 @@ address: XUFypmadXVvYmBWtiuwDioN1rtj6nSvqgzgWjx1yFmHAVndPaAEgnUvEvEDSkpgZPRmCYeq *R4* - campaign ID (Int) *R5* - backer script *R6* - campaign script (funds collected will go to this) -*R7* - fundraising deadline (Int, # of block, exclusive) +*R7* - fundraising deadline (Int, # of block, inclusive) *R8* - min value for successful fundraising (Long) From 1448ef1fa1165dbfcc414ee425cab3eac8aa05c9 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Tue, 10 Aug 2021 18:57:02 +0300 Subject: [PATCH 09/14] SigmaProp for dev script --- eip-0018.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 39249f32..2a0e6a59 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -8,14 +8,15 @@ ## Motivation -Collecting funds in different contexts is very much needed for building common infrastructure and projects - in the Ergo ecosystem. This EIP is proposing contracts and standardized boxes formats for announcing +Collecting funds in different contexts is very much needed for building common infrastructure and applications + in the Ergo ecosystem. 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 around -efficiency of blockchain scanning by an offchain application (implementing backend of ErgoFund service). +efficiency of blockchain scanning by an offchain application (backend of ErgoFund service built on top of the Scanner, +). ## Campaign Registration @@ -24,8 +25,8 @@ contain registration price and address (script) to pay for campaign registration Control box: -Contains campaign registration price in register R4 (long value) and script to pay for registration on register R5 -(byte array). +Contains campaign registration price in register R4 (as long value) and script to pay for registration in register R5 +(as byte array). To register a new crowdfunding campaign, crowdfunding token must be bought (to compensate expenses for scanning, storing crowdfunding data, doing an UI) @@ -39,19 +40,19 @@ Sell contract: val firstUnusedCampaignId = SELF.R4[Int].get // check control box NFT - val properControlBox = controlBox.tokens(0)._1 == fromBase64("uuqgee/jRz3XRo5Vsg2pYceRVdqahII27Hpc8NdM+Zo=") + val properControlBox = controlBox.tokens(0)._1 == fromBase64("yIvSlx5W4Mc9SNf9xGbv4GiktkzfGbXunzjxzYUN4z4=") val price = controlBox.R4[Long].get - val script = controlBox.R5[Coll[Byte]].get + val script = controlBox.R5[SigmaProp].get val inTokensCount = SELF.tokens(1)._2 val selfOut = OUTPUTS(0) - val validNFT = selfOut.tokens(1)._1 == fromBase64("SKsyra9+sTLHbAd/3rPfamaSQXFo8VQcg6VUXBDGP4E=") + val validNFT = selfOut.tokens(0)._1 == fromBase64("HiS07HwkSWJzfT9Y7IOkRKoP1F8lBbQJ6ks8m6hCexI=") val tokenId = selfOut.tokens(1)._1 // token the contract is selling - val validOutTokenId = tokenId == fromBase64("B6V6SJ0YdzStTJYFFP28sXm+rlgid0lUrBVkCF5kHc4=") + val validOutTokenId = tokenId == fromBase64("e2oiizI+3iNhwC1632YiTHm2otIHXmODS5iIbkAEgC8=") val outTokensCount = selfOut.tokens(1)._2 val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1) @@ -61,7 +62,7 @@ Sell contract: val validCampaignIdUpdate = selfOut.R4[Int].get == (firstUnusedCampaignId + 1) val rewardOut = OUTPUTS(1) - val validPayment = rewardOut.value >= 10000000000L && rewardOut.propositionBytes == script + val validPayment = rewardOut.value >= 10000000000L && rewardOut.propositionBytes == script.propBytes val campaignOut = OUTPUTS(2) val validCampaign = campaignOut.tokens(0)._1 == tokenId && campaignOut.R4[Int].get == firstUnusedCampaignId @@ -72,11 +73,11 @@ Sell contract: ``` -Control box NFT id: baeaa079efe3473dd7468e55b20da961c79155da9a848236ec7a5cf0d74cf99a -Token sale NFT id: 48ab32adaf7eb132c76c077fdeb3df6a6692417168f1541c83a5545c10c63f81 -Campaign identification token: 07a57a489d187734ad4c960514fdbcb179beae5822774954ac1564085e641dce +Control box NFT id: c88bd2971e56e0c73d48d7fdc466efe068a4b64cdf19b5ee9f38f1cd850de33e +Tokensale box NFT id: 1e24b4ec7c244962737d3f58ec83a444aa0fd45f2505b409ea4b3c9ba8427b12 +Campaign identification token: 7b6a228b323ede2361c02d7adf66224c79b6a2d2075e63834b98886e4004802f -Tokensale box P2S address: u1JSmVkJbNKB4uAzLp7NkCusTZmu5nC2ogyQJminm7MDjw4Db2ykMTQYr1mPvy7kghqqrNjFUQNZP6cpL6cApjghg8wgx5Dn246zqbwzgACnqQuijY7gQx5HZ2fEEZrWZ5mm84kgrBfA1NpXWmw8h1i2RNsDBnrhPbzcsq5DdXzydQt1sp9UBfHbRCDshLyasTxKKj5QzThWZYvTxTMcAxJfucnok3vC3LvgLGaa67VneJWNshaVwScwiXHXGtFFXSVqh2NqRjQUMse4Y64dec6A4G7gumtifiM9NbSxzPvr5EviiVL5qKxXGmqj8uJgxMGNUEkgNSdW2XZg9TXgRuVDf1PbPesh1kt6cau5kFaRGeoSdwkGmpJx8reiuH2mMSqe7VaeqfqSbMirUG2vh7He47 +Tokensale box P2S address: L7uDNCg9r1Zp6JUTJGEtnb6pPogTDs7Bxm4NUK94PXnhS6se7Mungk1g8a5NA3qwNeJi8mtj7aHmTrUzEe4ErZp2PZLcKAHC2LEdQHMrS8Y68x9wLVqbD5PocKNfD5hGrm3NZAecWmK47QWXSFuJG6M2ciUiVggXiR3Gr1nidtjT5VogZcuUjWHBMyXCh3hCtyaQA3icbAQaJSM2g1GyiFcyZrCFpn1P5B2j2JDGMSSCzQfpgueoC5yN5GMh6Hgwz7YGr7uvAQkFsfLLMAg9HKY7XzCByAUQVdDGetYhNngazmP1YDdaZ5U5VUg1TRtCYBsyZk1JYPJGBRTq5j2zcFLhcZkZBwBs92kp7BpGZcTe8vHvBnRv8h3jfUxGZ4K1h68SiH6Q6mbyvHyNz6vB3PgocArMKBbzAQzCpJio5BoS 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. From 871811032c0131ba125b0c051f2a74cebf3b35f1 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Fri, 13 Aug 2021 01:00:46 +0300 Subject: [PATCH 10/14] upd tokens and tokensale contract fix --- eip-0018.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 2a0e6a59..d5080b3a 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -40,7 +40,7 @@ Sell contract: val firstUnusedCampaignId = SELF.R4[Int].get // check control box NFT - val properControlBox = controlBox.tokens(0)._1 == fromBase64("yIvSlx5W4Mc9SNf9xGbv4GiktkzfGbXunzjxzYUN4z4=") + val properControlBox = controlBox.tokens(0)._1 == fromBase64("csP7zjJD1JHYHrVkzasWYrH41MfjEriIcM7Hm3z9QyE=") val price = controlBox.R4[Long].get val script = controlBox.R5[SigmaProp].get @@ -50,9 +50,9 @@ Sell contract: val selfOut = OUTPUTS(0) - val validNFT = selfOut.tokens(0)._1 == fromBase64("HiS07HwkSWJzfT9Y7IOkRKoP1F8lBbQJ6ks8m6hCexI=") + val validNFT = selfOut.tokens(0)._1 == fromBase64("FbCuQcJCMAaf+W2susCTKFCsDCoJJNr3KjnojLzzrNU=") val tokenId = selfOut.tokens(1)._1 // token the contract is selling - val validOutTokenId = tokenId == fromBase64("e2oiizI+3iNhwC1632YiTHm2otIHXmODS5iIbkAEgC8=") + val validOutTokenId = tokenId == fromBase64("BbZrl+WAL2RHtn/jDLQFXhTWsXuxT19WPWXJYixDplk=") val outTokensCount = selfOut.tokens(1)._2 val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1) @@ -62,7 +62,7 @@ Sell contract: val validCampaignIdUpdate = selfOut.R4[Int].get == (firstUnusedCampaignId + 1) val rewardOut = OUTPUTS(1) - val validPayment = rewardOut.value >= 10000000000L && rewardOut.propositionBytes == script.propBytes + 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 @@ -73,11 +73,11 @@ Sell contract: ``` -Control box NFT id: c88bd2971e56e0c73d48d7fdc466efe068a4b64cdf19b5ee9f38f1cd850de33e -Tokensale box NFT id: 1e24b4ec7c244962737d3f58ec83a444aa0fd45f2505b409ea4b3c9ba8427b12 -Campaign identification token: 7b6a228b323ede2361c02d7adf66224c79b6a2d2075e63834b98886e4004802f +Control box NFT id: 72c3fbce3243d491d81eb564cdab1662b1f8d4c7e312b88870cec79b7cfd4321 +Tokensale box NFT id: 15b0ae41c24230069ff96dacbac0932850ac0c2a0924daf72a39e88cbcf3acd5 +Campaign identification token: 05b66b97e5802f6447b67fe30cb4055e14d6b17bb14f5f563d65c9622c43a659 -Tokensale box P2S address: L7uDNCg9r1Zp6JUTJGEtnb6pPogTDs7Bxm4NUK94PXnhS6se7Mungk1g8a5NA3qwNeJi8mtj7aHmTrUzEe4ErZp2PZLcKAHC2LEdQHMrS8Y68x9wLVqbD5PocKNfD5hGrm3NZAecWmK47QWXSFuJG6M2ciUiVggXiR3Gr1nidtjT5VogZcuUjWHBMyXCh3hCtyaQA3icbAQaJSM2g1GyiFcyZrCFpn1P5B2j2JDGMSSCzQfpgueoC5yN5GMh6Hgwz7YGr7uvAQkFsfLLMAg9HKY7XzCByAUQVdDGetYhNngazmP1YDdaZ5U5VUg1TRtCYBsyZk1JYPJGBRTq5j2zcFLhcZkZBwBs92kp7BpGZcTe8vHvBnRv8h3jfUxGZ4K1h68SiH6Q6mbyvHyNz6vB3PgocArMKBbzAQzCpJio5BoS +Tokensale box P2S address: yvNXjWe8vBvZTXwyUHemPU59CRfm8AnvnzXowRQkQ9hoiGXsS7oEUGLPof6RoYAdKXEgTWR4qTK44w3GBuNdBjXeWcHFeBvyFxHWdEBsnqnYhtaFC2S71eHRPpLEndDghZebLdx25nufFh8YFJ9D8gTxTvxqahBgzpJT7pbUAUEkE2iRSZwpiXvj5SUmA6vmTrQzMTxicBG2mGV1NLq5rjBCAxjM5FpNSJ1KgqrxeTAfBvs1QnMUZ6CTBtGPLyNUpFoPTaYUPp9PfBni7FAQsumx36tukUHA2p3NdgfhTbYYSJTDQi7eWW2YikFvczXpTCfecbCL7HA1o6Cg6DvntmpJHWfjopWnsbpDdLg6SbP4Fup7wCnQaeL6NcSTVP1K5btmK5JryZc36V8KdHkheyaVzYTMA1Gufp6bAGtZP 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. @@ -143,9 +143,9 @@ example: address: XUFypmadXVvYmBWtiuwDioN1rtj6nSvqgzgWjx1yFmHAVndPaAEgnUvEvEDSkpgZPRmCYeqxewi8ZKZ4Pamp1M9DAdu8d4PgShGRDV9inwzN6TtDeefyQbFXRmKCSJSyzySrGAt16 *R4* - campaign ID (Int) -*R5* - backer script -*R6* - campaign script (funds collected will go to this) -*R7* - fundraising deadline (Int, # of block, inclusive) +*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) From f09e0b45f34b96db336968c90175cf3cd5e6884b Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Fri, 13 Aug 2021 16:15:29 +0300 Subject: [PATCH 11/14] more text --- eip-0018.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index d5080b3a..8532d9d9 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -84,11 +84,14 @@ campaign box data specified below in outputs. ## Campaign Box Data -For campaign, it is enough just to create box with ErgoFund token, campaign ID, campaign desc, script, recommended deadline, min to raise. +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) @@ -107,7 +110,7 @@ example: "value": 100000000, "assets": [ { - "tokenId": "08fc8bd24f0eaa011db3342131cb06eb890066ac6d7e6f7fd61fcdd138bd1e2c", + "tokenId": "05b66b97e5802f6447b67fe30cb4055e14d6b17bb14f5f563d65c9622c43a659", "amount": 1 } ], @@ -124,6 +127,9 @@ example: ## 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 From c5c975401e7457b0e9a77ab69b96d70b89e7cc46 Mon Sep 17 00:00:00 2001 From: Alex Chepurnoy Date: Mon, 16 Aug 2021 13:21:10 +0300 Subject: [PATCH 12/14] text improvements --- eip-0018.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eip-0018.md b/eip-0018.md index 8532d9d9..86dd5027 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -16,7 +16,7 @@ Collecting funds in different contexts is very much needed for building common i 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 @@ -26,10 +26,10 @@ 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 byte array). +(as SigmaProp constant). To register a new crowdfunding campaign, crowdfunding token must be bought (to compensate expenses for scanning, -storing crowdfunding data, doing an UI) +storing crowdfunding data, producing and maintaining UI). Sell contract: @@ -72,6 +72,9 @@ Sell contract: } ``` +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 @@ -79,9 +82,6 @@ Campaign identification token: 05b66b97e5802f6447b67fe30cb4055e14d6b17bb14f5f563 Tokensale box P2S address: yvNXjWe8vBvZTXwyUHemPU59CRfm8AnvnzXowRQkQ9hoiGXsS7oEUGLPof6RoYAdKXEgTWR4qTK44w3GBuNdBjXeWcHFeBvyFxHWdEBsnqnYhtaFC2S71eHRPpLEndDghZebLdx25nufFh8YFJ9D8gTxTvxqahBgzpJT7pbUAUEkE2iRSZwpiXvj5SUmA6vmTrQzMTxicBG2mGV1NLq5rjBCAxjM5FpNSJ1KgqrxeTAfBvs1QnMUZ6CTBtGPLyNUpFoPTaYUPp9PfBni7FAQsumx36tukUHA2p3NdgfhTbYYSJTDQi7eWW2YikFvczXpTCfecbCL7HA1o6Cg6DvntmpJHWfjopWnsbpDdLg6SbP4Fup7wCnQaeL6NcSTVP1K5btmK5JryZc36V8KdHkheyaVzYTMA1Gufp6bAGtZP -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. - ## 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). From bd24911414be29836fab40e6cdd00bb4ae06a230 Mon Sep 17 00:00:00 2001 From: code-for-uss Date: Sun, 7 Nov 2021 21:06:55 +0330 Subject: [PATCH 13/14] Update ErgoFund eip --- README.md | 1 + eip-0018-old.md | 161 +++++++++++++++++++++++ eip-0018.md | 330 ++++++++++++++++++++++++++++++------------------ 3 files changed, 367 insertions(+), 125 deletions(-) create mode 100644 eip-0018-old.md diff --git a/README.md b/README.md index a6257f52..ab6cb796 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,4 @@ 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 | diff --git a/eip-0018-old.md b/eip-0018-old.md new file mode 100644 index 00000000..86dd5027 --- /dev/null +++ b/eip-0018-old.md @@ -0,0 +1,161 @@ +# ErgoFund contracts + +* Author: kushti +* Status: Proposed +* Created: 11-June-2021 +* License: CC0 +* Forking: not needed + +## Motivation + +Collecting funds in different contexts is very much needed for building common infrastructure and applications + in the Ergo ecosystem. 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 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) + + +## TO-DO + +* Contracts to collect funds in SigUSD and other tokens. +* Contracts for campaigns with thousands of pledges. \ No newline at end of file diff --git a/eip-0018.md b/eip-0018.md index 86dd5027..b4592155 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -1,161 +1,241 @@ # ErgoFund contracts -* Author: kushti +* Author: code-for-uss, kushti * Status: Proposed -* Created: 11-June-2021 +* Created: 5-Nov-2021 * License: CC0 * Forking: not needed ## Motivation -Collecting funds in different contexts is very much needed for building common infrastructure and applications - in the Ergo ecosystem. This EIP is proposing contracts and standardized box formats for announcing - crowdfunding campaigns and collecting funds. +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 around -efficiency of blockchain scanning by an offchain application (backend of ErgoFund service built on top of the Scanner, -https://github.com/ergoplatform/scanner ). +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). -## Campaign Registration +There is an old version of ErgoFund eip [here](./eip-0018-old.md), prepared by Kushti. -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. +## Tokens -Control box: +The ErgoFund has the following types of tokens. -Contains campaign registration price in register R4 (as long value) and script to pay for registration in register R5 -(as SigmaProp constant). +| 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
Campaign boxes | +|Campaign participant tokens | 100 million per campaign | Identify campaign and each pledge box| Capmaign box
pledge boxes | -To register a new crowdfunding campaign, crowdfunding token must be bought (to compensate expenses for scanning, -storing crowdfunding data, producing and maintaining UI). +## Boxes -Sell contract: +There are a total of 3 contracts, and each corresponds to a box-type + control box. -```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 +| Box | Quantity | Tokens | Additional registers used | Purpose | Spending transactions | +|---|---|---|---|---|---| +|Control | 1 | ControlBoxNFT | R4: Price (Long),
R5: address[script] (SigmaProp) | Control price and address to pay for campaign registration |
-
| +|Sell| 1 | SellNFT
Registration tokens |
-
| Register a new campaign | registerCampaign,
returnRegistrationToken | +|Campaign | 0 - 100 million | Registration token
Campaign participant tokens | R4: Campaign description (Coll[Byte]),
R5: Public key of project owner (SigmaProp),
R6: Campaign deadline (Int),
R7: minToRaise (Long),
R8 (optional): campaign Proposal CID (Coll[Byte]) | Store campaign data and collect funds | Donate to campaign,
Withdraw funds,
Refund donates,
Return registrationToken | +|pledge | 0 - 100 million (per campaign) | Campaign participant tokens | R4: Donate value (Long),
R5: Public key of backer (SigmaProp) | Store donate value and Backer public key | Refund donate (fundraising failure) | - val validTokens = validNFT && validOutTokenId && (outTokensCount == inTokensCount - 1) +## Contracts - val validScript = SELF.propositionBytes == selfOut.propositionBytes +### Pledge Contract - val validCampaignIdUpdate = selfOut.R4[Int].get == (firstUnusedCampaignId + 1) +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.
- 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 - +- Registers: + * R4: Amount of donates [donateValue] (Long) + * R5: The public key of backer [backerPubKey] (SigmaProp) +- Tokens: + 1. campaignParticipantToken NO: [1]
+ We use this token as campaignId. - properControlBox && validTokens && validScript && validPayment && validCampaignIdUpdate && validCampaign -} +```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 +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]
+ Each campaign should buy one registrationToken from the Sell contract + 2. campaignParticipantToken NO: [This can set manually when creating each campaign]
+ After donating, Backer gets this token protected in the pledge contract. In other words, we use this token as campaignId. -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 -[ +```scala { - "address": "4MQyMKvMbnCJG3aJ", - "value": 100000000, - "assets": [ - { - "tokenId": "05b66b97e5802f6447b67fe30cb4055e14d6b17bb14f5f563d65c9622c43a659", - "amount": 1 + 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 } - ], - "registers": { - "R4": "0400", - "R5": "0e00", - "R6": "08cd0327e65711a59378c59359c3e1d0f7abe906479eccb76094e50fe79d743ccc15e6", - "R7": "04a0be49", - "R8": "0580d0acf30e" } + 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) } -] ``` -## 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. - +### 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]
+ Sell NFT used for sell box detection + 2. crowdfunding registrationToken NO: [Manually] ```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 -} + { + 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) + } ``` -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) - - ## TO-DO - -* Contracts to collect funds in SigUSD and other tokens. -* Contracts for campaigns with thousands of pledges. \ No newline at end of file +* Contracts to collect funds in SigUSD and other tokens. \ No newline at end of file From 63dfcc5c1c888e1e9e498448c1a4fdb4cbb9b862 Mon Sep 17 00:00:00 2001 From: code-for-uss Date: Mon, 22 Nov 2021 19:41:45 +0330 Subject: [PATCH 14/14] merged both eips into one --- eip-0018-old.md | 161 ------------------------------------------------ eip-0018.md | 150 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 163 deletions(-) delete mode 100644 eip-0018-old.md diff --git a/eip-0018-old.md b/eip-0018-old.md deleted file mode 100644 index 86dd5027..00000000 --- a/eip-0018-old.md +++ /dev/null @@ -1,161 +0,0 @@ -# ErgoFund contracts - -* Author: kushti -* Status: Proposed -* Created: 11-June-2021 -* License: CC0 -* Forking: not needed - -## Motivation - -Collecting funds in different contexts is very much needed for building common infrastructure and applications - in the Ergo ecosystem. 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 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) - - -## TO-DO - -* Contracts to collect funds in SigUSD and other tokens. -* Contracts for campaigns with thousands of pledges. \ No newline at end of file diff --git a/eip-0018.md b/eip-0018.md index b4592155..e5ea71c4 100644 --- a/eip-0018.md +++ b/eip-0018.md @@ -14,7 +14,7 @@ Collecting funds in different contexts is needed to build common infrastructure 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](./eip-0018-old.md), prepared by Kushti. +There is an old version of ErgoFund eip [here](#old-ergofund-contracts), prepared by Kushti. ## Tokens @@ -238,4 +238,150 @@ There are two ways to spend the boxes protected by this contract: ``` ## TO-DO -* Contracts to collect funds in SigUSD and other tokens. \ No newline at end of file +* 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)