The purpose of the previous chapter was to give you a foundation in Solidity, so I focused mainly on the basic syntax that the language offers. In the next two chapters, I’ll introduce more advanced object-oriented (OO) features. I’ll start with inheritance in chapter 6 and follow with abstract contracts and interfaces in chapter 7. These OO features allow you to reduce code duplication and make your contracts more composable.
The contract side of real-world Dapps is generally more complex than the single-contract Dapp you’ve seen so far with SimpleCoin. They often span many contracts interacting with each other, with each contract being a concrete instance of a potentially complex inheritance structure. In this chapter, I’ll help you build Simple-Crowdsale, a basic crowdsale management Dapp. A crowdsale is the process through which investors fund a Dapp by buying tokens issued by the organization that’s developing it. This sample application will give you an idea of how complex the smart contract layer of a realistic Dapp can be and how inheritance, abstract classes, and interfaces can help you model it appropriately.
I’ll try to keep my presentation of inheritance and polymorphism as pragmatic as possible, so if you’re among the readers who need a refresher on object-oriented programming, you’ll still be able to follow easily. I’ll start by building an application made of a simple contract, and I’ll keep extending it throughout this chapter by introducing all the object features I’ve mentioned, bit by bit.
I bet you’ve heard the term crowdfunding, which is a way of funding a project or a cause through relatively small contribution amounts from a relatively large number of people. You might have even invested some funds toward the design of a new cool gadget at Kickstarter.com, Indiegogo.com, or Microventures.com. If so, depending on the amount of money you contributed and whether the project was successful, you might have been given an early version of the gadget, or you might have been given a considerable discount on the final official price of the product. This type of scheme is called reward-based crowdfunding.
Lately, a new crowdfunding scheme called crowdsale has emerged, mainly geared toward the funding of startup companies. Rather than being given a discounted product or service in recognition for your early contribution, you’re offered some equity in the venture, generally in the form of a monetary token similar to SimpleCoin, whose value the organizers can set before the sale starts. Alternatively, the value can be determined dynamically during the campaign, depending on market factors such as initial token supply and actual demand. Often, the token or coin crowdsale is called an initial coin offering (ICO), an expression that mirrors the more conventional initial public offering (IPO) of shares by companies that enter the stock market for the first time.
In this section, you’ll build SimpleCrowdsale, a decentralized crowdsale management application that will teach you how to design Dapps based on multiple Solidity contracts and libraries. In a nutshell, the following list describes the minimum functionality a crowdsale contract generally provides, as illustrated in the diagram of the core crowdsale workflow in figure 6.1:
listing 6.1 gives you an idea of the functions a crowdsale Solidity contract would need to fulfill these requirements. (Don’t enter the code in Remix yet!) In case you’re wondering, onlyOwner is the same modifier I introduced earlier in 5.4.2 for SimpleCoin: only the contract owner is allowed to execute the finalize() function.
contract SimpleCrowdsale { function invest(address _beneficiary) public payable {} 1 function finalize() onlyOwner public {} 2 function refund() public {} 3 }
Let’s build a basic implementation of this functionality, which we’ll use as a starting point for a later discussion on advanced object-oriented features. I’m not expecting you to start entering the code into Remix until you reach 6.1.9, as I’ll be refactoring the code progressively to explain concepts step-by-step. Then I’ll recap everything in listing 6.5, which is fully executable. But if you want to give it a try as I go along, you’re welcome to.
A crowdsale contract needs to maintain some configuration regarding the funding period during which investment contributions are accepted, the price of the token being sold, the minimum investment objective, and the address of the account accepting the investments. It also needs to keep a record of the contributions that investors submit. This data should be visible from the whole contract, so you should express it in the following state variables:
uint256 public startTime; 1 uint256 public endTime; 2 uint256 public weiTokenPrice; 3 uint256 public weiInvestmentObjective; 4 mapping (address => uint256) public investmentAmountOf; 5 uint256 public investmentReceived; 6 uint256 public investmentRefunded; 7 bool public isFinalized; 8 bool public isRefundingAllowed; 9 address public owner; 10 SimpleCoin public crowdsaleToken; 11
The contract constructor should take all the input configurations I’ve described, validate them, and instantiate the contract of the token being sold. To make things easy, you’ll use SimpleCoin, because you’re already familiar with it. You shouldn’t find anything surprising in the following code:
constructor(uint256 _startTime, uint256 _endTime, uint256 _weiTokenPrice, uint256 _etherInvestmentObjective) public { require(_startTime >= now); 1 require(_endTime >= _startTime); 1 require(_weiTokenPrice != 0); 1 require(_etherInvestmentObjective != 0); 1 startTime = _startTime; 2 endTime = _endTime; 2 weiTokenPrice = _weiTokenPrice; 2 weiInvestmentObjective = 2 _etherInvestmentObjective * 1000000000000000000; 2 crowdsaleToken = new SimpleCoin(0); 3 isFinalized = false; isRefundingAllowed = false; owner = msg.sender; 4 }
This is the most important function of the contract. Its purpose is to accept Ether funds from investors and convert them into crowdsale tokens. These tokens won’t be released to their respective owners, though, until the crowdsale has completed successfully.
As you can see in the following implementation, you’ve declared the invest() function as payable and placed validation logic and token conversion logic in three internal functions. You’ve also declared a couple of events that these functions raise:
event LogInvestment(address indexed investor, uint256 value); event LogTokenAssignment(address indexed investor, uint256 numTokens); function invest() public payable { 1 require(isValidInvestment(msg.value)); 2 address investor = msg.sender; uint256 investment = msg.value; investmentAmountOf[investor] += investment; 3 investmentReceived += investment; 3 assignTokens(investor, investment); 4 emit LogInvestment(investor, investment); 5 } function isValidInvestment(uint256 _investment) internal view returns (bool) { 6 bool nonZeroInvestment = _investment != 0; 7 bool withinCrowdsalePeriod = now >= startTime && now <= endTime; 8 return nonZeroInvestment && withinCrowdsalePeriod; } function assignTokens(address _beneficiary, uint256 _investment) internal { uint256 _numberOfTokens = calculateNumberOfTokens(_investment); 9 crowdsaleToken.mint(_beneficiary, _numberOfTokens); 10 } function calculateNumberOfTokens(uint256 _investment) internal returns (uint256) { return _investment / weiTokenPrice; 11 }
The purpose of the finalize() function is to execute the closing actions of a crowdsale. If the crowdsale has met its minimum investment objective, the contract releases the tokens to the investors so they can be used. Additionally, a token bonus that depends on the total investment collected could be assigned and released to the development team. On the other hand, if the crowdsale is unsuccessful, it moves to a refunding state, and the investors are allowed to get their investments refunded to their accounts.
Before tokens are released to the investors, they should stay locked down, in an unusable state: token owners shouldn’t be able to perform any operations on them, such as transferring them to other accounts. The contract should only release the initially locked tokens if the crowdsale is successful.
As you know, SimpleCoin operations (apart from minting(), which is restricted to the contract owner) aren’t constrained in any way and don’t depend on the contract owner unlocking any functionality. Consequently, SimpleCoin, as it stands, isn’t suitable for a crowdsale. It seems you must create a modified version of SimpleCoin—say, ReleasableSimpleCoin—whose operations, such as transfer() and transferFrom(), aren’t allowed to work unless the token has been released, as shown in the following listing.
contract ReleasableSimpleCoin { bool public released = false; 1 ... 2 function release() onlyOwner { 3 released = true; } function transfer(address _to, uint256 _amount) public { require(_to != 0x0); require(coinBalance[msg.sender] > _amount); require(coinBalance[_to] + _amount >= coinBalance[_to] ); if (released ) { 4 coinBalance[msg.sender] -= _amount; coinBalance[_to] += _amount; emit Transfer(msg.sender, _to, _amount); return true; } revert(); 5 } function transferFrom(address _from, address _to, uint256 _amount) public returns (bool success) { require(_to != 0x0); require(coinBalance[_from] > _amount); require(coinBalance[_to] + _amount >= coinBalance[_to] ); require(_amount <= allowance[_from][msg.sender]); if (released ) { 4 coinBalance[_from] -= _amount; coinBalance[_to] += _amount; allowance[_from][msg.sender] -= _amount; emit Transfer(_from, _to, _amount); return true; } revert(); 5 } ... 5 }
The ReleasableSimpleCoin implementation in listing 6.2 works but has a major problem: it’s duplicating most of SimpleCoin’s code, with obvious maintenance disadvantages. If you decide to make a change in SimpleCoin, you must remember to replicate the same change in ReleasableSimpleCoin, which is time-consuming and error-prone. A way to avoid this is to derive ReleasableSimpleCoin from SimpleCoin through single inheritance and introduce the isReleased modifier, as shown in the following listing.
pragma solidity ^0.4.18; import "./Listing5_8_SimpleCoin.sol"; 1 contract ReleasableSimpleCoin is SimpleCoin { 2 bool public released = false; 3 modifier isReleased() { 4 if(!released) { revert(); } _; } constructor(uint256 _initialSupply) SimpleCoin(_initialSupply) public {} 5 function release() onlyOwner public { 6 released = true; } function transfer(address _to, uint256 _amount) isReleased public { 7 super.transfer(_to, _amount); 8 } function transferFrom(address _from, address _to, uint256 _amount) isReleased public returns (bool) { 7 super.transferFrom(_from, _to, _amount); 8 } }
The ReleasableSimpleCoin contract has given you a concrete example of single inheritance in Solidity. Let’s summarize the main Solidity keywords involved in inheritance:
Contract ReleasableSimpleCoin is SimpleCoin
function ReleasableSimpleCoin(uint256 _initialSupply) SimpleCoin(_initialSupply) {}
function transfer(address _to, uint256 _amount) isReleased public { ...
super.transfer(_to, _amount);
If you’re not entirely familiar with object-oriented terminology, table 6.1 can help you understand it.
Definition |
Explanation |
---|---|
To derive a contract | To inherit a contract—Acquire public and internal state variables and functions from the parent contract. |
Derived contract | Inherited contract, subcontract, or child contract. |
Base contract | Parent contract. |
Overridden function | Function reimplemented in a derived contract—The overridden function is used instead of the parent one when the derived contract calls the function. |
You can see the inheritance relationship between ReleasableSimpleCoin and Simple-Coin in the contract diagram in figure 6.2. I’ve used the same drawing convention used in UML class diagrams. Although you may be able to understand UML class diagrams intuitively, if you’re not familiar with them and want to learn more, check out the related sidebar.
The Unified Modeling Language (UML) is a general-purpose modeling language that aims at standardizing the visualization of the design of a software system. It was developed in the 1990s by Grady Booch, Ivar Jacobson, and James Rumbaugh at Rational Software. It became an ISO standard in 2005.
UML covers a wide range of diagrams, including these categories:
In this book, I’ll only show you contract diagrams, which, given the similarity between the concepts of contract and class, are based on UML class diagrams, the lowest-level structure diagrams.
A contract diagram describes the content of a contract (state variables and functions) and the two main relationships between contracts:
I’ve annotated every section of text and every symbol in the sample contract diagram you see here, so you can refer back to it if you have any trouble understanding the contract diagrams that follow in the coming pages. I’ll explain new symbols as I introduce them in the coming sections.
Contract diagram, with symbols and conventions adapted from the UML class diagram
If you want to learn more about UML diagrams, I encourage you to look up the UML Quick Reference Card, at http://tnerual.eriogerg.free.fr/umlqrc.pdf, or this convenient UML Cheatsheet: http://mng.bz/jO5p. The best book on UML is The Unified Modeling Language User Guide, by Grady Booch, et al, published by Addison-Wesley.
Now that you’ve implemented a token suitable for a crowdsale, you can go back to the state variables section and replace the definition of crowdsaleToken with
ReleasableSimpleCoin public crowdsaleToken;
and amend the token instantiation in the crowdsale constructor with
crowdsaleToken = new ReleasableSimpleCoin(0);
Now I can show you a possible implementation for the finalize() function:
function finalize() onlyOwner public { 1 if (isFinalized) revert(); 2 bool isCrowdsaleComplete = now > endTime; 3 bool investmentObjectiveMet = investmentReceived >= weiInvestmentObjective; 3 if (isCrowdsaleComplete) { if (investmentObjectiveMet) crowdsaleToken.release(); 4 else isRefundingAllowed = true; 5 isFinalized = true; } }
As I mentioned earlier, onlyOwner is the same modifier I introduced in SimpleCoin at the end of chapter 5 to restrict the execution of some functions only to the contract owner:
modifier onlyOwner { if (msg.sender != owner) revert(); _; }
The last function you must implement to complete the first version of your crowdsale contract is refund(), which investors would call after an unsuccessful crowdsale:
event Refund(address investor, uint256 value); function refund() public { if (!isRefundingAllowed) revert(); 1 address investor = msg.sender; uint256 investment = investmentAmountOf[investor]; if (investment == 0) revert(); 2 investmentAmountOf[investor] = 0; 3 investmentRefunded += investment; emit Refund(msg.sender, investment); if (!investor.send(investment)) revert(); 4 }
I’ve decided to refund investors through send() rather than transfer(), only because transfer() has some quirks in Remix (at the time of writing) and might generate unwanted error messages that would slow down your learning experience. In a production environment, transfer() is recommended.
As I noted, SimpleCoin and SimpleCrowdsale use the same onlyOwner modifier to restrict access to some operations to only the contract owner. The advantage is that you’re using onlyOwner consistently across the two contracts. The downside is that you had to introduce the owner state variable and implement the onlyOwner modifier in both contracts. Wouldn’t it be nice to place this modifier somewhere so you could then drop it into both SimpleCoin and SimpleCrowdsale without introducing code duplication? Fortunately, you can by encapsulating the ownership state and onlyOwner modifier into a separate contract, called Ownable, as shown in the following listing.
pragma solidity ^0.4.18; contract Ownable { address public owner; 1 constructor() public { owner = msg.sender; 2 } modifier onlyOwner() { require(msg.sender == owner); 3 _; } }
Now you can remove the owner state variable and the onlyOwner() modifier from both SimpleCoin and SimpleCrowdsale and inherit both contracts from Ownable, as shown in the contract diagram in figure 6.3 and in this code:
SimpleCoin is Ownable { ... } SimpleCrowdsale is Ownable { ... }
You can see the refactored SimpleCoin contract inheriting from Ownable in appendix A. Congratulations! You’ve completed your first implementation of a crowdsale contract, which you can fully appreciate in the contract diagram in figure 6.4 and in listing 6.5.
The arrow between SimpleCrowdsale and ReleasableSimpleCoin in figure 6.4 is a UML symbol that stands for depends on. I’ve included the arrow because the SimpleCrowdsale contract has a state variable of type Releasable-SimpleCoin, so it depends on ReleasableSimpleCoin.
pragma solidity ^0.4.24; import "./Listing6_3_ReleasableSimpleCoin.sol"; 1 import "./Listing6_4_Ownable.sol"; 1 contract SimpleCrowdsale is Ownable { uint256 public startTime; uint256 public endTime; uint256 public weiTokenPrice; uint256 public weiInvestmentObjective; mapping (address => uint256) public investmentAmountOf; uint256 public investmentReceived; uint256 public investmentRefunded; bool public isFinalized; bool public isRefundingAllowed; ReleasableSimpleCoin public crowdsaleToken; constructor(uint256 _startTime, uint256 _endTime, uint256 _weiTokenPrice, uint256 _weiInvestmentObjective) payable public { require(_startTime >= now); require(_endTime >= _startTime); require(_weiTokenPrice != 0); require(_weiInvestmentObjective != 0); startTime = _startTime; endTime = _endTime; weiTokenPrice = _weiTokenPrice; weiInvestmentObjective = _weiInvestmentObjective; crowdsaleToken = new ReleasableSimpleCoin(0); isFinalized = false; } event LogInvestment(address indexed investor, uint256 value); event LogTokenAssignment(address indexed investor, uint256 numTokens); event Refund(address investor, uint256 value); function invest() public payable { require(isValidInvestment(msg.value)); address investor = msg.sender; uint256 investment = msg.value; investmentAmountOf[investor] += investment; investmentReceived += investment; assignTokens(investor, investment); emit LogInvestment(investor, investment); } function isValidInvestment(uint256 _investment) internal view returns (bool) { bool nonZeroInvestment = _investment != 0; bool withinCrowdsalePeriod = now >= startTime && now <= endTime; return nonZeroInvestment && withinCrowdsalePeriod; } function assignTokens(address _beneficiary, uint256 _investment) internal { uint256 _numberOfTokens = calculateNumberOfTokens(_investment); crowdsaleToken.mint(_beneficiary, _numberOfTokens); } function calculateNumberOfTokens(uint256 _investment) internal returns (uint256) { return _investment / weiTokenPrice; } function finalize() onlyOwner public { if (isFinalized) revert(); bool isCrowdsaleComplete = now > endTime; bool investmentObjectiveMet = investmentReceived >= weiInvestmentObjective; if (isCrowdsaleComplete) { if (investmentObjectiveMet) crowdsaleToken.release(); else isRefundingAllowed = true; isFinalized = true; } } function refund() public { if (!isRefundingAllowed) revert(); address investor = msg.sender; uint256 investment = investmentAmountOf[investor]; if (investment == 0) revert(); investmentAmountOf[investor] = 0; investmentRefunded += investment; emit Refund(msg.sender, investment); if (!investor.send(investment)) revert(); } }
You’ve just started implementing SimpleCrowdsale, but if you want to see what you’ve built so far in action, copy listing 6.5 into a new Remix code tab (perhaps named Listing6_5_SimpleCrowdsale.sol). Make sure you’ve placed the dependent code specified in the import directives in related code tabs named appropriately:
(You can find these files in the code downloadable from the book website.)
Before instantiating SimpleCrowdsale, I recommend you pick version 0.4.24 in the Remix Compiler tab because this is the Solidity version I’ve used. I also suggest you temporarily disable checks on startDate and endDate to make your interaction with Remix easier. Modify this line in isValidInvestment()
bool withinCrowdsalePeriod = now >= startTime && now <= endTime;
to read
bool withinCrowdsalePeriod = true;
and this line in finalize()
bool isCrowdsaleComplete = now > endTime;
bool isCrowdsaleComplete = true;
Now you can feed SimpleCrowdsale’s constructor parameters in the text box next to the Deploy button. For example, I’ll set a token price of 2,000,000,000,000,000 Wei and a funding objective of 15,000 Ether as follows:
2003526559, 2003526600, 2000000000000000, 15000
Because of your earlier modification, the 2003526559 and 2003526600 start and end dates will be ignored.
Now pick SimpleCrowdsale from the contract drop-down list and click Deploy. If SimpleCrowdsale doesn’t appear, click the Compile tab and then Start to Compile. Take a note of which account is currently selected in the Account drop-down list at the top of the screen: this will be the contract owner of both SimpleCrowdsale and ReleasableSimpleCoin. SimpleCrowdsale will be activated, and a number of buttons corresponding to its state variables and functions will appear at the bottom of the screen.
You can simulate investors’ activity by calling the invest() function as follows:
Repeat these three steps with different accounts and investment amounts.
Although investors at this point have received a number of tokens corresponding to the amount of Ether contributed, they aren’t able to transfer them yet because the owner hasn’t finalized the crowdsale. If you want to check that tokens have been assigned to each investor, you must activate the ReleasableSimpleCoin contract instance referenced in SimpleCrowdsale.
First of all, get its address by clicking crowdsaleToken. Copy this address and paste it in the textbox next to the Activate button (not wrapped with double quotes), as shown in figure 6.5 (but your address will be different):
Now click At Address, and a new panel showing ReleasableSimpleCoin’s functionality, mostly inherited from SimpleCoin, will appear at the bottom part of the screen, as shown in figure 6.6. You must click ReleasableSimpleCoin to expand the panel.
You can now check the number of tokens assigned to each investor in the following way:
Repeat this for all accounts in the drop-down. In my case, I have the token breakdown shown in table 6.2.
Number of tokens |
|
---|---|
0xca35b7d915458ef540ade6068dfe2f44e8fa733c | 10,100,000 |
0x14723a09acff6d2a60dcdf7aa4aff308fddc160c | 150,000 |
0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db | 653,500 |
0x583031d1113ad414f02576bd6afabfb302140225 | 284,000 |
0xdd870fa1b7c4700f2bd7f44238821c26f7392148 | 56,000 |
As I explained, tokens that get assigned to investors during the crowdsale are unusable until it’s been finalized successfully. This means an investor won’t be able to transfer tokens to another account. Let’s check that this is the case by trying to transfer some tokens from one account to another.
Pick an account from the Account drop-down, copy its address, and paste it into the transfer text box (wrapped, as usual, with double quotes). This will be the destination account for the token transfer. Then pick a different account from the Account drop-down. This will be the source of the token transfer.
Click Transfer. Now check the number of tokens associated with the source and destination account again, as you did earlier. You’ll notice they haven’t changed. Because the value of released is still false (the crowdsale hasn’t been finalized yet), the transfer won’t take place (but no error will be thrown), as you can see in the code extract from transfer():
function transfer(address _to, uint256 _amount) public { ... if (released) { 1 coinBalance[msg.sender] -= _amount; coinBalance[_to] += _amount; emit Transfer(msg.sender, _to, _amount); } }
If you want to test a successful crowdsale, make sure the amount of investment is above the investment objective (15,000 Ether in my example, equivalent to 15,000,000,000,000,000,000,000 Wei). Bear in mind, the amount shown next to the investmentReceived button is expressed in Wei. You can reach it quickly by making a few large Ether investments. Once the investment received is higher than the funding objective, select the SimpleCrowdsale contract owner account (the account you used to instantiate SimpleCrowdsale) and click Finalize.
Verify the crowdsale has been finalized by clicking isFinalized. The value displayed should be true. Verify ReleasableSimpleCoin has been released by clicking Released. The value displayed again should be true.
Now you can try to make a token transfer, as you did earlier. This time, though, the transfer will work because the value of released is now true and the transfer logic can get executed. You can verify that the number of tokens associated with the source and destination accounts have changed accordingly. In my case, I moved three coins from the account starting with 0x1472 to the account starting with 0x4b089. You can see their amended balances in table 6.3.
If you’re up for it, you can also test the scenario of an unsuccessful crowdsale. I recommend you restart from scratch and go through all the steps you took earlier up to the point you contribute Ether to the crowdsale through the invest() function from various investor accounts. This time, though, don’t reach the funding objective.
Now, when you call finalize() by clicking the related button, fundingObjectiveMet is false. Consequently, the isRefundingAllowed gets enabled, as you can see in this extract from the finalize() code:
if (isCrowdsaleComplete) { if (investmentObjectiveMet) crowdsaleToken.release(); else isRefundingAllowed = true; isFinalized = true; }
You can double-check this is the case by clicking isRefundingAllowed. With that enabled, you’ll be able to successfully call the refund() function, as you’ll see next. But before doing so, as an exercise I invite you to test that tokens haven’t been released and it isn’t possible to transfer them from one account to another, as you saw earlier.
Following unsuccessful finalization, investors are allowed to get refunded. Pick an address from the Account drop-down, check the amount of Ether associated with it (shown next to the address), and click Refund. You’ll see that the Ether value next to the account address will increase because it has been transferred from the crowdsale contract back to the investor.
In the next few sections, you’ll improve the functionality of the crowdsale contract by taking advantage of Solidity object-oriented features, such as single inheritance, which you’ve already used in the ReleasableSimpleCoin contract, multiple inheritance, and abstract classes.
The current crowdsale implementation assumes the price of the token being bought by the investors is fixed throughout the crowdsale, from start to end. A way to incentivize and reward early investors is to provide tranche-based pricing.
A tranche is a certain amount of total investment received. A different token price applies to each tranche, as shown in figure 6.7, so that early investors are attracted with a lower initial price. As the sale progresses, the token price rises when moving from one tranche to the next and eventually becomes constant after the minimum funding target has been met.
In the example shown in figure 6.7, the minimum funding target has been set at 15,000 Ether, and the organizers have decided to create four tranches, as defined in table 6.4.
Total investment received (in Ether) |
Token price (in Ether) |
---|---|
Above 15,000 | 0.005 |
From 10,000 to 15,000 | 0.004 |
From 3,000 to 10,000 | 0.003 |
From 0 to 3,000 | 0.002 |
Now that you understand the principle of tranche-based pricing, let’s see how you can implement it in Solidity. If you look back at listing 6.5, you’ll notice that the only function that’s directly accessing the token price is
function calculateNumberOfTokens(uint256 _investment) internal returns (uint256) { return _investment / weiTokenPrice; 1 }
You’ll also notice that the value of weiTokenPrice is never modified from within the crowdsale contract, so you can consider it fixed throughout the whole crowdsale.
Here’s a small challenge: How would you modify the implementation of calculate-NumberOfTokens() so the pricing becomes tranche-based according to the tranches defined in table 6.4? Here are some hints you could consider before attempting a solution:
struct Tranche { uint256 weiHighLimit; 1 uint256 weiTokenPrice; 2 }
mapping(uint256 => Tranche) public trancheStructure;and initialize the state variable in the contract constructor as follows
trancheStructure[0] = Tranche(3000 ether, 0.002 ether); trancheStructure[1] = Tranche(10000 ether, 0.003 ether); trancheStructure[2] = Tranche(15000 ether, 0.004 ether); trancheStructure[3] = Tranche(1000000000 ether, 0.005 ether);with the tranche limits and token price defined in Ether.
uint256 currentTrancheLevel;and you’d initialize this state variable in the constructor as follows:
currentTrancheLevel = 0;
I strongly encourage you to give tranche-based pricing a try! Finished? You can compare your modified calculateNumberOfTokens() with that in the following listing.
function calculateNumberOfTokens(uint256 investment) internal returns (uint256) { updateCurrentTrancheAndPrice(); 1 return investment / weiTokenPrice; } function updateCurrentTrancheAndPrice() 2 internal { uint256 i = currentTrancheLevel; while(trancheStructure[i].weiHighLimit < investmentReceived) 3 ++i; currentTrancheLevel = i; weiTokenPrice = trancheStructure[currentTrancheLevel] .weiTokenPrice; 4 }
As you can see in listing 6.6, only one extra function is necessary to calculate the token price based on a tranche basis. The rest of the code stays unaltered. I’m sure you might be wondering where you should place the amended calculateNumberOfTokens() and new updateCurrentTrancheAndPrice() functions. The simple answer is in a new crowdsale contract called TranchePricingCrowdsale, because you might still want to use the flat token pricing in other crowdsales. The next question is should I copy Simple-Crowdsale’s code, paste it into TranchePricingCrowdsale, and apply in it the modifications from listing 6.6? The answer is no, you shouldn’t! As I explained when you created ReleasableSimpleCoin based on SimpleCoin, you’ll be inheriting TranchePricingCrowdsale from SimpleCrowdsale, as shown in the following listing.
pragma solidity ^0.4.24; import "./Listing6_5_SimpleCrowdsale.sol"; contract TranchePricingCrowdsale is SimpleCrowdsale { 1 struct Tranche { uint256 weiHighLimit; uint256 weiTokenPrice; } mapping(uint256 => Tranche) public trancheStructure; 2 uint256 public currentTrancheLevel; 3 constructor(uint256 _startTime, uint256 _endTime, uint256 _etherInvestmentObjective) SimpleCrowdsale(_startTime, _endTime, 1, _etherInvestmentObjective) 4 payable public { trancheStructure[0] = Tranche(3000 ether, 0.002 ether); 5 trancheStructure[1] = Tranche(10000 ether, 0.003 ether); 5 trancheStructure[2] = Tranche(15000 ether, 0.004 ether); 5 trancheStructure[3] = Tranche(1000000000 ether, 0.005 ether); 5 currentTrancheLevel = 0; 5 } function calculateNumberOfTokens( uint256 investment) 6 internal returns (uint256) { updateCurrentTrancheAndPrice(); return investment / weiTokenPrice; } function updateCurrentTrancheAndPrice() 7 internal { uint256 i = currentTrancheLevel; while(trancheStructure[i].weiHighLimit < investmentReceived) ++i; currentTrancheLevel = i; weiTokenPrice = trancheStructure[currentTrancheLevel].weiTokenPrice; } }
By inheriting TranchePricingCrowdsale from SimpleCrowdsale, I’ve shown you another example of single inheritance. I’ll give you a quick summary of the inheritance features of Solidity you can appreciate in TranchePricingCrowdsale. This is almost a repeat of what you’ve already seen in ReleasableSimpleCoin, and hopefully it will help you consolidate the concepts:
contract TranchePricingCrowdsale is SimpleCrowdsale
TranchePricingCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _weiTokenPrice, uint256 _etherInvestmentObjective) SimpleCrowdsale(_startTime, _endTime, _weiTokenPrice, _etherInvestmentObjective) payable public { trancheStructure[0] = Tranche(3000 ether, 0.002 ether); ...
You can see the inheritance relationship between TranchePricingCrowdsale and SimpleCrowdsale in the contract diagram in figure 6.8.
I invite you to copy TranchePricingCrowdsale’s code from listing 6.7 into a new code tab of Remix and instantiate this contract. Then you can check how the token price rises when the tranche thresholds are hit.
You might have noticed I’ve used the expression single inheritance a few times. The reason I’ve done so is because Solidity also supports multiple inheritance. We’ll explore this in the next section.
Solidity is a young language that’s still in the early stages of its development. Its syntax is continuously improving, and consequently best practice recommendations change frequently. Even the most experienced Solidity developers, who might have a length of experience that would make them barely junior developers in more established languages, have to continuously keep up with learning the latest techniques and recommendations, especially around security.
Realistically, once a contract has been deployed into the public production Ethereum network, it’s unlikely to be modified on a regular basis. But a security flaw might be discovered, either because its developers weren’t aware of a recent recommendation on safer syntax or because someone finds the flaw at the community, node client, or language level.
Given that the possibility of things going wrong is real and the consequent risk of Ether being lost or stolen isn’t negligible, it has become common practice to include various forms of panic buttons in contracts. These range from a pause or halt function, which freezes the state and functionality of the contract until the contract owner decides to switch it on again, to a complete self-destruct function, which transfers the Ether stored in the contract to a safe account before making the contract permanently unusable.
Enough talking! The following listing shows how to add pausing functionality to ReleasableSimpleCoin, assuming you don’t want to add it to SimpleCoin as well.
pragma solidity ^0.4.24; import "./Listing5_8_SimpleCoin.sol"; contract ReleasableSimpleCoin is SimpleCoin { ... 1 bool public paused = false; 2 modifier whenNotPaused() { require(!paused); _; } modifier whenPaused() { require(paused); _; } function pause() onlyOwner whenNotPaused public { 3 paused = true; } function unpause() onlyOwner whenPaused public { 3 paused = false; } ... 4 function transfer(address _to, uint256 _amount) isReleased whenNotPaused public { 5 super.transfer(_to, _amount); } function transferFrom(address _from, address _to, uint256 _amount) isReleased whenNotPaused public returns (bool) { 5 super.transferFrom(_from, _to, _amount); } }
After making ReleasableSimpleCoin pausable, you might want to do the same with SimpleCrowdsale. You might be tempted to duplicate the pausing functionality code you’ve written into SimpleCrowdsale. A smarter way to make both ReleasableSimple-Coin and SimpleCrowdsale pausable—without introducing code duplication—is to extract the pausable functionality from ReleasableSimpleCoin into a separate contract (called Pausable, for example) and then inherit both ReleasableSimpleCoin and SimpleCrowdsale from Pausable.
You can see the new Pausable contract extracted from ReleasableSimpleCoin in the following listing.
pragma solidity ^0.4.24; import "./Listing6_4_Ownable.sol"; contract Pausable is Ownable { bool public paused = false; 1 modifier whenNotPaused() { 2 require(!paused); _; } modifier whenPaused() { 2 require(paused); _; } function pause() onlyOwner whenNotPaused public { 3 paused = true; } function unpause() onlyOwner whenPaused public { 3 paused = false; } }
Now you can reapply pausability to ReleasableSimpleCoin by inheriting Releasable-SimpleCoin from Pausable, which, by the way, I’ve inherited from Ownable so it can use the onlyOwner modifier. But, hold on, as you’ll remember, you’re already inheriting ReleasableSimpleCoin from SimpleCoin. Is that a problem? The answer is no. Solidity supports multiple inheritance, so you can inherit a contract from several contracts, as shown here:
contract ReleasableSimpleCoin is SimpleCoin, Pausable { 1 ... }
Multiple inheritance can make your code composable because you can create more complex contracts by inheriting from multiple simpler contracts. On the other hand, you should try to compose your contracts so they have only minimal cross dependencies, to avoid circular reference issues.
A circular reference happens when contract C is inherited from contract P, which in turn is inherited indirectly from contract C.
You can see the multiple inheritance relationships present in ReleasableSimpleCoin in figure 6.9.
After extracting the pausable functionality into a standalone contract, it’s easy to apply it to other contracts. For example:
contract SimpleCrowdsale is Pausable { ... }
By inheriting SimpleCrowdsale from Pausable, you’ve also made TranchePricingCrowdsale pausable. You can see the amended crowdsale contract structure in figure 6.10.
If you’re not comfortable with the Pausable functionality, and you prefer, in case of emergency, to immediately transfer the Ether stored in the contract to a safe address and then destroy the contract so that malicious attackers can no longer manipulate it, you can encapsulate this functionality in a Destructible contract, as shown in the following listing.
pragma solidity ^0.4.24; import "./Listing6_4_Ownable.sol"; contract Destructible is Ownable { constructor() payable public { } function destroyAndSend(address _recipient) onlyOwner public { selfdestruct(_recipient); 1 } }
You can now also make both ReleasableSimpleCoin and SimpleCrowdsale destructible by inheriting them from Destructible, as shown in figure 6.11 and the following snippet:
contract ReleasableSimpleCoin is SimpleCoin, Pausable, Destructible { 1 ... } contract SimpleCrowdsale is Pausable, Destructible { 1 ... }
So far, you’ve learned how to take advantage of single and multiple inheritance to implement contracts with increasing functionality while avoiding code duplication. In the next chapter, you’ll see how abstract classes and interfaces can help you further toward the maintainability of your contract code base.