-
Notifications
You must be signed in to change notification settings - Fork 36
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
base: master
Are you sure you want to change the base?
Changes from all commits
ff15e6d
0cc5393
809d522
2f72e7b
6147201
c216706
08103d2
6f57caa
28c2409
1448ef1
8718110
f09e0b4
c5c9754
bd24911
72ccc5a
82e963f
63dfcc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 | | ||
|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 /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am strongly against the idea of having campaign tokens, at least in this way. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I 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.