Squashed commit of the following:
commit fcf35eb806100de300bd9803ce3150dde1ecc424
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 17 17:16:04 2019 -0300
remove all docsite dependency
commit eeaee9a9d43d70704f6ab17b5126ddbd52b93a50
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 17 17:15:23 2019 -0300
update solidity-docgen
commit f021ff951829ea0c155186749819403c6b76e803
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 17 17:05:06 2019 -0300
update docsite script for new setup
commit ff887699d381cfbbe3acf1f1c0de8e22b58480f3
Merge: c938aa1d 84f85a41
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 17 16:46:46 2019 -0300
Merge branch 'master' into antora
commit c938aa1d9ed05ac83a34e2cebd8353f8331ad6d6
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jul 16 18:24:29 2019 -0300
make component name shorter
commit 5bbd6931e02cbbd8864c82655ad0f390ceead5f3
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 20:16:17 2019 -0300
add all info to docs templates
commit 39682c4515d7cf0f0368ed557f50d2709174208a
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 20:13:49 2019 -0300
fix npm docsite script
commit 7ae46bd4a0437abf66150d54d05adf46e3de2cab
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 18:48:05 2019 -0300
convert inline docs to asciidoc
commit cfdfd3dee4b4bf582fde22c8cb6e17a603d6e0c8
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 17:34:52 2019 -0300
add missing contract names in readmes
commit 15b6a2f9bfb546cf1d3bf4f104278b118bf1b3f4
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 17:16:47 2019 -0300
fix script path
commit 80d82b909f9460d1450d401f00b3f309da506b29
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 17:13:53 2019 -0300
update version of solidity-docgen
commit a870b6c607b9c2d0012f8a60a4ed1a1c8b7e8ebd
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 17:03:53 2019 -0300
add nav generation of api ref
commit 069cff4a25b83752650b54b86d85608c2f547e5e
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Wed Jul 10 16:32:14 2019 -0300
initial migration to asciidoc and new docgen version
commit 55216eed0a6551da913c8d1da4b2a0d0d3faa1a8
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 20:39:35 2019 -0300
add basic api doc example
commit 0cbe50ce2173b6d1d9a698329d91220f58822a53
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 19:31:31 2019 -0300
add sidebars
commit 256fc942845307258ac9dc25aace48117fa10f79
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 15:22:38 2019 -0300
add page titles
commit f4d0effa70e1fc0662729863e8ee72a8821bc458
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 15:19:41 2019 -0300
add contracts index file
commit b73b06359979f7d933df7f2b283c50cb1c31b2a0
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 15:14:52 2019 -0300
fix header levels
commit fb57d9b820f09a1b7c04eed1a205be0e45866cac
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 15:11:47 2019 -0300
switch format to preferred asciidoctor format
commit 032181d8804137332c71534753929d080a31a71f
Author: Francisco Giordano <frangio.1@gmail.com>
Date: Tue Jun 25 15:05:38 2019 -0300
initialize antora component and convert docs to asciidoc
245 lines
11 KiB
Plaintext
245 lines
11 KiB
Plaintext
= Crowdsales
|
|
|
|
Crowdsales are a popular use for Ethereum; they let you allocate tokens to network participants in various ways, mostly in exchange for Ether. They come in a variety of shapes and flavors, so let's go over the various types available in OpenZeppelin and how to use them.
|
|
|
|
Crowdsales have a bunch of different properties, but here are some important ones: - Price & Rate Configuration - Does your crowdsale sell tokens at a fixed price? - Does the price change over time or as a function of demand? - Emission - How is this token actually sent to participants? - Validation — Who is allowed to purchase tokens? - Are there KYC / AML checks? - Is there a max cap on tokens? - What if that cap is per-participant? - Is there a starting and ending time frame? - Distribution - Does distribution of funds happen in real-time or after the crowdsale? - Can participants receive a refund if the goal is not met?
|
|
|
|
To manage all of the different combinations and flavors of crowdsales, OpenZeppelin provides a highly configurable link:api/crowdsale#crowdsale[`Crowdsale`] base contract that can be combined with various other functionalities to construct a bespoke crowdsale.
|
|
|
|
[[crowdsale-rate]]
|
|
== Crowdsale Rate
|
|
|
|
Understanding the rate of a crowdsale is super important, and mistakes here are a common source of bugs.
|
|
|
|
✨ *HOLD UP FAM THIS IS IMPORTANT* ✨
|
|
|
|
Firstly, *all currency math is done in the smallest unit of that currency and converted to the correct decimal places when _displaying_ the currency*.
|
|
|
|
This means that when you do math in your smart contracts, you need to understand that you're adding, dividing, and multiplying the smallest amount of a currency (like wei), _not_ the commonly-used displayed value of the currency (Ether).
|
|
|
|
In Ether, the smallest unit of the currency is wei, and `1 ETH === 10^18 wei`. In tokens, the process is _very similar_: `1 TKN === 10^(decimals) TKNbits`.
|
|
|
|
* The smallest unit of a token is "bits" or `TKNbits`.
|
|
* The display value of a token is `TKN`, which is `TKNbits * 10^(decimals)`
|
|
|
|
What people usually call "one token" is actually a bunch of TKNbits, displayed to look like `1 TKN`. This is the same relationship that Ether and wei have. And what you're _always_ doing calculations in is *TKNbits and wei*.
|
|
|
|
So, if you want to issue someone "one token for every 2 wei" and your decimals are 18, your rate is `0.5e18`. Then, when I send you `2 wei`, your crowdsale issues me `2 * 0.5e18 TKNbits`, which is exactly equal to `10^18 TKNbits` and is displayed as `1 TKN`.
|
|
|
|
If you want to issue someone "`1 TKN` for every `1 ETH`", and your decimals are 18, your rate is `1`. This is because what's actually happening with the math is that the contract sees a user send `10^18 wei`, not `1 ETH`. Then it uses your rate of 1 to calculate `TKNbits = rate * wei`, or `1 * 10^18`, which is still `10^18`. And because your decimals are 18, this is displayed as `1 TKN`.
|
|
|
|
One more for practice: if I want to issue "1 TKN for every dollar (USD) in Ether", we would calculate it as follows:
|
|
|
|
* assume 1 ETH == $400
|
|
* therefore, 10^18 wei = $400
|
|
* therefore, 1 USD is `10^18 / 400`, or `2.5 * 10^15 wei`
|
|
* we have a decimals of 18, so we'll use `10 ^ 18 TKNbits` instead of `1 TKN`
|
|
* therefore, if the participant sends the crowdsale `2.5 * 10^15 wei` we should give them `10 ^ 18 TKNbits`
|
|
* therefore the rate is `2.5 * 10^15 wei === 10^18 TKNbits`, or `1 wei = 400 TKNbits`
|
|
* therefore, our rate is `400`
|
|
|
|
(this process is pretty straightforward when you keep 18 decimals, the same as Ether/wei)
|
|
|
|
[[token-emission]]
|
|
== Token Emission
|
|
|
|
One of the first decisions you have to make is "how do I get these tokens to users?". This is usually done in one of three ways:
|
|
|
|
* (default) — The `Crowdsale` contract owns tokens and simply transfers tokens from its own ownership to users that purchase them.
|
|
* link:api/crowdsale#mintedcrowdsale[`MintedCrowdsale`] — The `Crowdsale` mints tokens when a purchase is made.
|
|
* link:api/crowdsale#allowancecrowdsale[`AllowanceCrowdsale`] — The `Crowdsale` is granted an allowance to another wallet (like a Multisig) that already owns the tokens to be sold in the crowdsale.
|
|
|
|
[[default-emission]]
|
|
=== Default Emission
|
|
|
|
In the default scenario, your crowdsale must own the tokens that are sold. You can send the crowdsale tokens through a variety of methods, but here's what it looks like in Solidity:
|
|
|
|
[source,solidity]
|
|
----
|
|
IERC20(tokenAddress).transfer(CROWDSALE_ADDRESS, SOME_TOKEN_AMOUNT);
|
|
----
|
|
|
|
Then when you deploy your crowdsale, simply tell it about the token
|
|
|
|
[source,solidity]
|
|
----
|
|
new Crowdsale(
|
|
1, // rate in TKNbits
|
|
MY_WALLET, // address where Ether is sent
|
|
TOKEN_ADDRESS // the token contract address
|
|
);
|
|
----
|
|
|
|
[[minted-crowdsale]]
|
|
=== Minted Crowdsale
|
|
|
|
To use a link:api/crowdsale#mintedcrowdsale[`MintedCrowdsale`], your token must also be a link:api/token/ERC20#erc20mintable[`ERC20Mintable`] token that the crowdsale has permission to mint from. This can look like:
|
|
|
|
[source,solidity]
|
|
----
|
|
contract MyToken is ERC20, ERC20Mintable {
|
|
// ... see "Tokens" for more info
|
|
}
|
|
|
|
contract MyCrowdsale is Crowdsale, MintedCrowdsale {
|
|
constructor(
|
|
uint256 rate, // rate in TKNbits
|
|
address payable wallet,
|
|
IERC20 token
|
|
)
|
|
MintedCrowdsale()
|
|
Crowdsale(rate, wallet, token)
|
|
public
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
contract MyCrowdsaleDeployer {
|
|
constructor()
|
|
public
|
|
{
|
|
// create a mintable token
|
|
ERC20Mintable token = new MyToken();
|
|
|
|
// create the crowdsale and tell it about the token
|
|
Crowdsale crowdsale = new MyCrowdsale(
|
|
1, // rate, still in TKNbits
|
|
msg.sender, // send Ether to the deployer
|
|
address(token) // the token
|
|
);
|
|
// transfer the minter role from this contract (the default)
|
|
// to the crowdsale, so it can mint tokens
|
|
token.addMinter(address(crowdsale));
|
|
token.renounceMinter();
|
|
}
|
|
}
|
|
----
|
|
|
|
[[allowancecrowdsale]]
|
|
=== AllowanceCrowdsale
|
|
|
|
Use an link:api/crowdsale#allowancecrowdsale[`AllowanceCrowdsale`] to send tokens from another wallet to the participants of the crowdsale. In order for this to work, the source wallet must give the crowdsale an allowance via the ERC20 link:api/token/ERC20#IERC20.approve(address,uint256)[`approve`] method.
|
|
|
|
[source,solidity]
|
|
----
|
|
contract MyCrowdsale is Crowdsale, AllowanceCrowdsale {
|
|
constructor(
|
|
uint256 rate,
|
|
address payable wallet,
|
|
IERC20 token,
|
|
address tokenWallet // <- new argument
|
|
)
|
|
AllowanceCrowdsale(tokenWallet) // <- used here
|
|
Crowdsale(rate, wallet, token)
|
|
public
|
|
{
|
|
|
|
}
|
|
}
|
|
----
|
|
|
|
Then after the crowdsale is created, don't forget to approve it to use your tokens!
|
|
|
|
[source,solidity]
|
|
----
|
|
IERC20(tokenAddress).approve(CROWDSALE_ADDRESS, SOME_TOKEN_AMOUNT);
|
|
----
|
|
|
|
[[validation]]
|
|
== Validation
|
|
|
|
There are a bunch of different validation requirements that your crowdsale might be a part of:
|
|
|
|
* link:api/crowdsale#cappedcrowdsale[`CappedCrowdsale`] — adds a cap to your crowdsale, invalidating any purchases that would exceed that cap
|
|
* link:api/crowdsale#individuallycappedcrowdsale[`IndividuallyCappedCrowdsale`] — caps an individual's contributions.
|
|
* link:api/crowdsale#whitelistcrowdsale[`WhitelistCrowdsale`] — only allow whitelisted participants to purchase tokens. this is useful for putting your KYC / AML whitelist on-chain!
|
|
* link:api/crowdsale#timedcrowdsale[`TimedCrowdsale`] — adds an link:api/crowdsale#TimedCrowdsale.openingTime()[`openingTime`] and link:api/crowdsale#TimedCrowdsale.closingTime()[`closingTime`] to your crowdsale
|
|
|
|
Simply mix and match these crowdsale flavors to your heart's content:
|
|
|
|
[source,solidity]
|
|
----
|
|
contract MyCrowdsale is Crowdsale, CappedCrowdsale, TimedCrowdsale {
|
|
|
|
constructor(
|
|
uint256 rate, // rate, in TKNbits
|
|
address payable wallet, // wallet to send Ether
|
|
IERC20 token, // the token
|
|
uint256 cap, // total cap, in wei
|
|
uint256 openingTime, // opening time in unix epoch seconds
|
|
uint256 closingTime // closing time in unix epoch seconds
|
|
)
|
|
CappedCrowdsale(cap)
|
|
TimedCrowdsale(openingTime, closingTime)
|
|
Crowdsale(rate, wallet, token)
|
|
public
|
|
{
|
|
// nice, we just created a crowdsale that's only open
|
|
// for a certain amount of time
|
|
// and stops accepting contributions once it reaches `cap`
|
|
}
|
|
}
|
|
----
|
|
|
|
[[distribution]]
|
|
== Distribution
|
|
|
|
There comes a time in every crowdsale's life where it must relinquish the tokens it's been entrusted with. It's your decision as to when that happens!
|
|
|
|
The default behavior is to release tokens as participants purchase them, but sometimes that may not be desirable. For example, what if we want to give users a refund if we don't hit a minimum raised in the sale? Or, maybe we want to wait until after the sale is over before users can claim their tokens and start trading them, perhaps for compliance reasons?
|
|
|
|
OpenZeppelin is here to make that easy!
|
|
|
|
[[postdeliverycrowdsale]]
|
|
=== PostDeliveryCrowdsale
|
|
|
|
The link:api/crowdsale#postdeliverycrowdsale[`PostDeliveryCrowdsale`], as its name implies, distributes tokens after the crowdsale has finished, letting users call link:api/crowdsale#PostDeliveryCrowdsale.withdrawTokens(address)[`withdrawTokens`] in order to claim the tokens they've purchased.
|
|
|
|
[source,solidity]
|
|
----
|
|
contract MyCrowdsale is Crowdsale, TimedCrowdsale, PostDeliveryCrowdsale {
|
|
|
|
constructor(
|
|
uint256 rate, // rate, in TKNbits
|
|
address payable wallet, // wallet to send Ether
|
|
IERC20 token, // the token
|
|
uint256 openingTime, // opening time in unix epoch seconds
|
|
uint256 closingTime // closing time in unix epoch seconds
|
|
)
|
|
PostDeliveryCrowdsale()
|
|
TimedCrowdsale(openingTime, closingTime)
|
|
Crowdsale(rate, wallet, token)
|
|
public
|
|
{
|
|
// nice! this Crowdsale will keep all of the tokens until the end of the crowdsale
|
|
// and then users can `withdrawTokens()` to get the tokens they're owed
|
|
}
|
|
}
|
|
----
|
|
|
|
[[refundablecrowdsale]]
|
|
=== RefundableCrowdsale
|
|
|
|
The link:api/crowdsale#refundablecrowdsale[`RefundableCrowdsale`] offers to refund users if a minimum goal is not reached. If the goal is not reached, the users can link:api/crowdsale#RefundableCrowdsale.claimRefund(address%20payable)[`claimRefund`] to get their Ether back.
|
|
|
|
[source,solidity]
|
|
----
|
|
contract MyCrowdsale is Crowdsale, RefundableCrowdsale {
|
|
|
|
constructor(
|
|
uint256 rate, // rate, in TKNbits
|
|
address payable wallet, // wallet to send Ether
|
|
IERC20 token, // the token
|
|
uint256 goal // the minimum goal, in wei
|
|
)
|
|
RefundableCrowdsale(goal)
|
|
Crowdsale(rate, wallet, token)
|
|
public
|
|
{
|
|
// nice! this crowdsale will, if it doesn't hit `goal`, allow everyone to get their money back
|
|
// by calling claimRefund(...)
|
|
}
|
|
}
|
|
----
|