Chapter 9
IN THIS CHAPTER
Creating your new smart contract
Developing functions and events
Protecting ownership and security
Making your functions work
You learn about the basics of developing Solidity smart contracts for Ethereum in Chapter 8. You also learn about difficulties encountered with traditional supply chain applications and how Ethereum can help address some of those problems. Developing distributed applications, or dApps, for the Ethereum blockchain may look similar to writing code in other languages, but it does have specific advantages over non-blockchain environments. However, you have to approach the software development process a little differently when working with blockchain.
Before starting to write a dApp for the Ethereum blockchain, make sure that you understand what your dApp should do and why a blockchain environment is a good fit. Getting these points cleared up in the beginning can help you avoid mistakes that waste time and money. Knowing the tips and tricks of blockchain development before you start writing your own dApps will help you develop better software than just learning as you go.
Ethereum dApps focus on providing some functionality that interacts with data stored in the blockchain environment. Due to the design of blockchain technology, each interaction with the blockchain has an associated cost. Understanding how your dApp will have to pay for blockchain access is critical to getting it right the first time. In this chapter, you learn how to use Solidity to write effective smart contracts for the Ethereum blockchain environment.
As you discover in Chapter 8, a supply chain is a framework that connects producers to consumers and manages how products and services make their way toward the consumers. In simple cases such as a farmer’s market, consumers buy their produce directly from the growers. But in most other cases, at least one intermediary helps get products from producers to consumers. Intermediaries can provide transportation, warehousing, retailing, and other value-added services.
Implementing a supply chain solution in a blockchain environment can reduce the overall cost of providing products and services to consumers and make the entire process more transparent. If you store every step of a product’s journey on the blockchain, anyone can track the product along its way.
The first step in developing a supply chain dApp is to look at the data and actions the dApp will need to provide the required functionality. For your supply chain dApp to do its job, you need at least four types of data. Here is a list of the types of data you’ll need:
To provide minimal functionality, your supply chain dApp needs to include the following capabilities:
The data and functionality your supply chain dApp will support fits nicely into two groups: payment tokens and supply chain. Some of the data and functionality applies to the supply chain, and other data and functionality applies to paying for supply chain activity. You’ll separate your data and functionality into two smart contracts.
The payment token smart contract handles anything related to payments. Your supply chain participants will buy and sell products by using tokens instead of traditional currency. Although Ethereum includes its own currency, Ether, you will implement your own token for supply chain participants to use. Although you could just have participants pay each other by using Ether, a custom token helps you to manage the entire process.
Defining your own token helps ensure that you limit supply chain participation to only valid supply chain participants and can make transfers simpler. Instead of allowing any Ethereum account to interact with your supply chain, only accounts that own your tokens can pay for products. Therefore, the only way a participant can enter the supply chain is to gain the trust of another participant. You have to either sell your products to an existing participant or exchange some other currency for your tokens.
Multiple proposed standards for Ethereum tokens in the form of Ethereum Request for Comments (ERC) documents exist. Ten of these proposals have been accepted to become Ethereum Improvement Proposals (EIP). ERC-20 (now EIP-20) defines one of the early standards for defining tokens for Ethereum. You’ll use the ERC/EIP-20 standard for your tokens.
Your token smart contract will allow participants to acquire tokens and then transfer them to other participants in exchange for moving products along the supply chain. To complete this process, you need several data items and functions. The data items you’ll define follow:
otalSupply
: The total number of tokens in circulationname
: A descriptive name for your tokendecimals
: The number of decimals to use when displaying token amountssymbol
: A short identifier for your tokenbalances
: The current balance of each participating account, mapped to the account's addressallowed
: A list of number of tokens authorized for transfer between accounts, mapped to the sender’s addressYour token smart contract will define six functions that allow users to manage token transfers. The functions you’ll define are as follows:
totalSupply()
: Returns the current total number of tokensbalanceOf()
: Returns the current balance, in tokens, of a specific accountallowance()
: Returns the remaining number of tokens that are allowed to be transferred from a specific source account to a specific target accounttransfer()
: Transfers tokens from the caller to a specified target accountapprove()
: Sets a number of tokens that are allowed to be transferred from a specific source account to a specific target accounttransferFrom()
: Transfers tokens from a specified source account to a specified target accountYour second smart contract will contain the data and functionality to manage the product, participant, and product transfer data. In other words, it will handle all supply chain activity that isn't related to payment. As you learn how to implement supply chain functionality, you’ll probably think of more things that you’d like your dApp to handle. That’s okay. The smart contracts you’ll develop in this book are just a starting point. You can extend them to handle many more use cases.
To store the supply chain data necessary for managing product migration toward consumers, you’ll define the following data items:
p_id
: The product ID uniquely identifies a product and is mapped to a product structure.u_id
: The participant ID uniquely identifies a participant and is mapped to a participant structure.r_id
: The registration ID uniquely identifies a registration and is mapped to a registration structure.Now that you know the data and functions you'll need in your smart contracts, the next step is to start writing code in Solidity.
In this section you create the files you need to implement the token and supply chain smart contracts. To get started, follow these steps to create a new project named SupplyChain, initialize it in Truffle, and launch VS Code for your new project:
mkdir SupplyChain
cd SupplyChain
truffle init
code .
In VS Code, create three new smart contracts as follows. Click or tap SUPPLYCHAIN, then Contracts, and then the New File button next to SUPPLYCHAIN. Type the following filenames to create each new file (make sure your filenames appear under Contracts and look exactly like these). (If you download the project files, you don’t have to do this.)
Now click or tap SUPPLYCHAIN and then Contracts to display your contracts. Your VS Code Explorer view should look like Figure 9-1.
The first file you’ll edit is the interface for the ERC-20 token. An interface looks just like a smart contract but doesn’t contain any executable code. Developers use interfaces to define minimum functionality for groups of programs. When you define an interface, you define the minimum data items and functions that you want to be common among smart contracts that implement the interface.
In our case, we’re going to use the standard ERC-20 (or EIP-20) token interface. Every ERC-20 token that uses this interface is guaranteed to have the same minimum data and functions. That’s the purpose of an interface. Your implementation may have more data and functions, but you can count on the fact that it has at least everything defined in the interface. In fact, if you use an interface and forget to define a data item or function, the compiler generates an error and refuses to compile the program.
To make sure that your token complies with the ERC-20 token standard, you use the ERC-20 token interface. Click or tap the erc20Interface.sol tab in VS Code and enter the following code:
// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
// ----------------------------------------------------------------------------
pragma solidity ^0.4.24;
contract ERC20Interface {
uint256 public totalSupply;
function totalSupply() public view returns (uint);
function balanceOf(address tokenOwner) public view returns (uint balance);
function allowance(address tokenOwner, address spender) public view returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
This interface defines the single variable, six functions, and two events that every token contract must implement to support the ERC-20 token standard. You’ll learn what each line of code does in the next section, where you implement the interface. At this point, just note that each line of code defines a variable, a function, or an event. There is no code that actually does anything.
After you define the interface, you can implement the code to make your token smart contract work. Click or tap the erc20Token.sol tab in VS Code and enter the following code:
// ----------------------------------------------------------------------------
///Implements EIP20 token standard: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
// ----------------------------------------------------------------------------
pragma solidity ^0.4.24;
import "./erc20Interface.sol";
contract ERC20Token is ERC20Interface {
}
You learned about the pragma
statement in Chapter 7. The import
statement tells Solidity to open an external file and read it into this file to compile it, just as if you had copied the contents of erc20Interface.sol into this file. You can use the import
statement as your projects grow to help you keep any single source code file from growing too large to manage easily. If any source code file starts to get too big, you can spilt it into multiple smaller files and import the pieces in the main file.
The last line in the code segment defines the smart contract. You use the contract
statement to define a smart contract named ERC20Token
. When you define the contract
as ERC20token is ERC20Interface
, you are telling Solidity that you intend to implement ERC20Interface
in this file. Therefore, the compiler will check to make sure that every item defined in the interface is implemented in this file.
The first step to make your token work is to add the data items you'll need. Add the following code between the two curly braces (after the contract
statement):
uint256 constant private MAX_UINT256 = 2**256 - 1;
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowed;
uint256 public totalSupply; // Total number of tokens
string public name; // Descriptive name (i.e. For Dummies Token)
uint8 public decimals; // How many decimals to use to display amounts
string public symbol; // Short identifier for token (i.e. FDT)
These lines of source code define the data you’ll store on the blockchain for your token. The first data item, MAX_UINT256
, is defined as a constant, which means that the value you assign to it can't be changed at runtime. Solidity uses the **
symbol to denote exponentiation, so 3 squared is 3**2
in Solidity. The value of 2 raised to the 256 power minus 1 is stored in the MAX_UINT256
constant. Defining MAX_UINT256 is a convenient way to store the largest possible value in a uint256
variable.
The balances and allowed data items are mappings. They exist to make it easy to look up a balance or a list of token transfer allowances for an Ethereum account address. The remaining data items are state variables that describe attributes of your token.
The next step in writing developing your supply chain dApp is to begin the definition of the supply chain smart contract. Click or tap the SupplyChain.sol tab in VS Code and enter the following code:
pragma solidity ^0.4.24;
contract supplyChain {
uint32 public p_id = 0; // Product ID
uint32 public u_id = 0; // Participant ID
uint32 public r_id = 0; // Registration ID
}
This smart contract starts like the other smart contracts you’ve seen so far. Inside the contract
body, you define three state variables to store the highest ID for products, participants, and registrations. The next sections define the details of each type of supply chain data.
The product structure defines the details for each unique product. Type the following code after the state variable definitions in the preceding section:
struct product {
string modelNumber;
string partNumber;
string serialNumber;
address productOwner;
uint32 cost;
uint32 mfgTimeStamp;
}
mapping(uint32 => product) public products;
In addition to the product structure, the products
mapping allows users to look up a product from its product ID (p_id
).
The participant structure defines the details for each unique participant. Type the following code after the products
mapping in the preceding section:
struct participant {
string userName;
string password;
string participantType;
address participantAddress;
}
mapping(uint32 => participant) public participants;
In addition to the participant structure, the participants
mapping allows users to lookup a participant from its participant ID (u_id
).
The registration structure defines the details for each unique registration. A registration is defined as the point in time when a product's owner changes from one participant to another. Each registration represents a product moving along the supply chain.
Type the following code after the participants
mapping in the preceding section:
struct registration {
uint32 productId;
uint32 ownerId;
uint32 trxTimeStamp;
address productOwner;
}
mapping(uint32 => registration) public registrations; // Registrations by Registration ID (r_id)
mapping(uint32 => uint32[]) public productTrack; // Registrations by Product ID (p_id) / Movement track for a product
In addition to the product structure, the registrations
mapping allows users to look up a registration from its registration ID (r_id
). The productTrack
mapping returns the supply chain movement history for a specified product (p_id
).
After defining the basic contract structure and data items, the next step in developing your smart contracts is to write the code for each smart contract function. Functions provide the actions of your smart contracts and define what your smart contracts can do.
To define the actions your ERC-20 token smart contract should carry out, you need to define its functions and provide the code for the body of each function. Remember that your ERC-20 token smart contract implements an interface, so you have to at least define the functions required in the interface. Note that you can define more functions than those in the interface.
Click or tap the erc20Token.sol tab in VS Code and enter the code for each of the following functions. (Start entering function source code after the symbol state variable definition.)
A constructor is a special type of function that runs when the smart contract is deployed to the blockchain. In the constructor, you place initialization steps that are executed only when the contract is first stored in the blockchain. In the case of the ERC-20 token, the constructor initializes the token's attributes and allocates the supply of tokens to the Ethereum address that deploys the smart contract.
Enter the following code to define the smart contract’s constructor:
constructor(uint256 _initialAmount, string _tokenName,
uint8 _decimalUnits, string _tokenSymbol) public {
balances[msg.sender] = _initialAmount; // The creator owns all tokens
totalSupply = _initialAmount; // Update total token supply
name = _tokenName; // Token name
decimals = _decimalUnits; // Number of decimals
symbol = _tokenSymbol; // Token symbol
}
The constructor code for the ERC-20 token is simple. When you deploy the smart contract code to the blockchain, the smart contract assigns the token data items to the provided parameters and assigns all initial tokens to the Ethereum account address that deployed the smart contract.
transfer()
functionThe transfer()
function transfers tokens from the calling address to a specified address. Enter the following code after the constructor:
function transfer(address _to, uint256 _value) public returns (bool success) {
require(_value >= 0,"Cannot transfer negative amount.");
require(balances[msg.sender] >= _value,"Insufficient funds.");
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
The transfer()
function introduces the Solidity require()
function, which prevents functions from continuing unless a specific condition is met. If the require()
condition is not satisfied, it returns a message to the caller and refunds any unused gas. It provides a polite way to stop smart contract execution. In the case of the transfer()
function, you can transfer tokens only if the sender has a sufficient balance to transfer. The require()
condition validates that the sender has at least as many tokens as the transfer requires; otherwise, it returns the "Insufficient funds."
string.
If the sender does have the required funds, you decrease the balance of the sender and increase the balance of the receiver by the amount to transfer, and then return a true
value that tells the caller that the transfer was completed successfully.
transferFrom
()
functionThe transferFrom()
function transfers tokens from one specified address to another specified address. Enter the following code after the transfer()
function:
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
uint256 allowance = allowed[_from][msg.sender];
require(balances[_from] >= _value && allowance >= _value,"Insufficient funds.");
balances[_from] -= _value;
balances[_to] += _value;
if (allowance < MAX_UINT256) {
allowed[_from][msg.sender] -= _value;
}
return true;
}
The transferFrom()
function transfers up to a pre-approved amount from one address to another. The function looks up the pre-approved amount from the allowed
mapping and stores that value in the allowance
variable. The function calls the require()
function to verify that the sender has a sufficient token balance to transfer, and then adjusts the balances of the sender and receiver. The last step is to query the allowance
variable and, if it is set, subtract the amount transferred from the remaining allowance.
balanceOf
()
functionThe balanceOf()
function returns the number of tokens owned by a specified address. Enter the following code after the transferFrom()
function:
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
approve()
functionThe approve()
function grants permission to transfer a specified number of tokens from one address to another specified address. Enter the following code after the balanceOf()
function:
function approve(address _spender, uint256 _value) public returns (bool success) {
allowed[msg.sender][_spender] = _value;
return true;
}
allowance()
functionTh allowance()
function returns the remaining number of approved tokens that can be transferred from one address to another specified address. Enter the following code after the approve()
function:
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
totalsupply()
functionThe totalSupply()
function returns the total number of tokens in circulation. Enter the following code after the allowance()
function:
function totalSupply() public view returns (uint256 totSupp) {
return totalSupply;
}
To define the functionality of your supply chain smart contract, you need to define its functions and provide the code for the body of each function.
Click or tap the SupplyChain.sol tab in VS Code and enter the code for each of the following functions. (Start entering function source code after the productTrack
mapping definition.)
The createParticipant()
function increments the participant ID (u_id
), creates a new participant, and sets its attributes to the passed-in parameters. Enter the following code after the productTrack
mapping:
function createParticipant(string _name, string _pass, address _pAdd, string _pType) public returns (uint32){
uint32 userId = u_id++;
participants[userId].userName = _name ;
participants[userId].password = _pass;
participants[userId].participantAddress = _pAdd;
participants[userId].participantType = _pType;
return userId;
}
The getParticipantDetails()
function returns the attributes of the specified participant (p_id
). Enter the following code after the createParticipant()
function:
function getParticipantDetails(uint32 _p_id) public view returns (string,address,string) {
return (participants[_p_id].userName,participants[_p_id].participantAddress,participants[_p_id].participantType);
}
The createProduct()
function increments the product ID (p_id
), creates a new product, and sets its attributes to the passed-in values. Enter the following code after the getParticipantDetails()
function:
function createProduct(uint32 _ownerId, string _modelNumber, string _partNumber, string _serialNumber, uint32 _productCost) public returns (uint32) {
if(keccak256(abi.encodePacked(participants[_ownerId].participantType)) == keccak256("Manufacturer")) {
uint32 productId = p_id++;
products[productId].modelNumber = _modelNumber;
products[productId].partNumber = _partNumber;
products[productId].serialNumber = _serialNumber;
products[productId].cost = _productCost;
products[productId].productOwner = participants[_ownerId].participantAddress;
products[productId].mfgTimeStamp = uint32(now);
return productId;
}
return 0;
}
The getProductDetails()
function returns the attributes of the specified product (p_id
). Enter the following code after the createProduct()
function:
function getProductDetails(uint32 _productId) public view returns (string,string,string,uint32,address,uint32){
return (products[_productId].modelNumber,products[_productId].partNumber,
products[_productId].serialNumber,products[_productId].cost,
products[_productId].productOwner,products[_productId].mfgTimeStamp);
}
The transferToOwner()
function records movement along the supply chain. This function transfers the ownership of a specified product from one supply chain participant to another. It creates a new registrations struct, based on r_id
, assigns its data items from the passed-in parameters, and pushes the new struct onto the productTrack
list. Enter the following code after the getProductDetails()
function:
function transferToOwner(uint32 _user1Id ,uint32 _user2Id, uint32 _prodId) public returns(bool) {
participant memory p1 = participants[_user1Id];
participant memory p2 = participants[_user2Id];
uint32 registration_id = r_id++;
registrations[registration_id].productId = _prodId;
registrations[registration_id].productOwner = p2.participantAddress;
registrations[registration_id].ownerId = _user2Id;
registrations[registration_id].trxTimeStamp = uint32(now);
products[_prodId].productOwner = p2.participantAddress;
productTrack[_prodId].push(registration_id);
return (true);
}
The getProductTrack()
function returns the registration history for a specified product. This function shows the path a product has taken along the supply chain from its original producer. This function provides the current status of any product in the supply chain. Enter the following code after the transferToOwner()
function:
function getProductTrack(uint32 _prodId) external view returns (uint32[]) {
return productTrack[_prodId];
}
The getRegistrationDetails()
function returns the attributes of the specified registration (r_id
). Enter the following code after the getProductTrack()
function:
function getRegistrationDetails(uint32 _regId) public view returns (uint32,uint32,address,uint32) {
registration memory r = registrations[_regId];
return (r.productId,r.ownerId,r.productOwner,r.trxTimeStamp);
}
Smart contract code runs on each EVM across the Ethereum network. It is essentially server-side code. One of the difficulties you encounter when running server-side code is communicating with the client. Smart contracts don't just run arbitrarily — they have to be called by a client or another smart contract. Communication is pretty easy when a client or smart contract calls another smart contract function. The caller sends input parameters and waits to receive any return values.
Sometimes the caller doesn't want to wait around for a function to finish. Many programs operate under a different flow control model. In an event-driven model, one program waits for something to happen and then responds. VS Code operates in an event-driven model. Figure 9-2 shows VS Code as you edit the SupplyChain.sol smart contract.
Your VS Code window should look like the one in the figure. The program is running, but it isn’t doing much right now. In fact, VS Code is waiting on you to do something. After you launch VS Code and open SupplyChain.sol, VS Code just waits for you to tell it what to do. You can type text into the editor, click or tap menu items or buttons, or press function keys of control key combinations.
You can code event-driven programs in Solidity, too. Solidity allows you to define events in your smart contracts and then trigger those events whenever you want to. If your calling program is waiting, or listening, for these events, it can respond to the events and carry out some of its own functions. In the case of VS Code, it is listening for many events, including the Ctrl+S keystroke event. When VS Code sees that the Ctrl-S event has occurred, it runs its Save File function. You can code your smart contracts the same way.
There are three steps to implanting events in Solidity:
Use the Solidity event statement to define the event, give it a name, and define the data it passes when it triggers.
Trigger the event.
Use the Solidity emit
statement to trigger a previously defined event and pass data to it.
Receive and respond to the event.
You learn how to receive and respond to events in Chapter 10.
Your supply chain smart contracts will use three events. The ERC-20 token interface requires that you implement two of the events, and you’ll define the other one in the main supply chain smart contract.
Click or tap the erc20Interface.sol tab in VS Code to switch to the ERC-20 interface code. Look at the last two lines of code:
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
These lines define the Transfer
and Approval
events. When the Transfer
event triggers, it tells any program that is listening for this event that a transfer has just occurred, and that the transfer consisted of send tokens from one address to another. When the Approval
event triggers, it tells any program that is listening for this event that a new approval was just authorized for a token owner to transfer up to some number of tokens to a specific sender. It is up to the listening programs to do something with these events. Your smart contracts need to only define and trigger the events.
Click or tap the SupplyChain.sol tab in VS Code to switch to the supply chain smart contract code. Scroll down to the definitions for the two mappings after the registrations
struct. This should be around line 35. Type the following text on a new line:
event Transfer(uint32 productId);
After you enter this new code, the last few lines of your SupplyChain.sol code should look like Figure 9-3.
You will use your new Transfer
event every time you create a new registration. That means you'll use the event to let any listening program know that a product has just been transferred from one supply chain participant to another. The Transfer
event sends the transferred product ID to the listening program to tell it what product just moved. The event mechanism makes it easy for external programs to monitor changes that your smart contracts carry out.
After you define the events you’ll need in your smart contracts, the next step is to trigger each event at the right time. All you really have to do here is trigger the event in your code when you carry out the action you want to communicate. In other words, you should trigger the Transfer
event at the point where your code carries out the transfer action.
In Solidity, you use the emit
statement to trigger an event. When you use the emit
statement, it is like calling a function. You tell Solidity what event to trigger, and then you provide parameter values, as you do with a function.
Click or tap the erc20Interface.sol tab in VS Code to switch to the ERC-20 smart contract code. Update your transfer()
, transferFrom(),
and approve()
functions to include the emit
statements shown here (new code is in bold):
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balances[msg.sender] >= _value,"Insufficient funds for transfer source.");
balances[msg.sender] -= _value;
balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
uint256 allowance = allowed[_from][msg.sender];
require(balances[_from] >= _value && allowance >= _value,"Insufficient allowed funds for transfer source.");
balances[_to] += _value;
balances[_from] -= _value;
if (allowance < MAX_UINT256) {
allowed[_from][msg.sender] -= _value;
}
emit Transfer(_from, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
You can call the emit
statement anywhere in your smart contract code. In your ERC-20 token smart contract, you have added the emit
statement in three places to signal that some notable action has just occurred.
Click or tap the SupplyChain.sol tab in VS Code to switch to the supply chain smart contract code. Update your transferToOwner()
function to include the emit
statement shown here (new code is in bold):
function transferToOwner(uint32 _user1Id ,uint32 _user2Id, uint32 _prodId) onlyOwner(_prodId) public returns(bool) {
participant memory p1 = participants[_user1Id];
participant memory p2 = participants[_user2Id];
uint32 registration_id = r_id++;
registrations[registration_id].productId = _prodId;
registrations[registration_id].productOwner = p2.participantAddress;
registrations[registration_id].ownerId = _user2Id;
registrations[registration_id].trxTimeStamp = uint32(now);
products[_prodId].productOwner = p2.participantAddress;
productTrack[_prodId].push(registration_id);
emit Transfer(_prodId);
return (true);
}
One of the difficulties you'll encounter when developing blockchain applications is restricting the execution of sensitive functions. Remember that your smart contract code runs on all EVMs. All EVMs have the complete code for your smart contracts, so limiting execution requires careful planning.
Modifiers can make data items and functions unavailable for external entities to access or run. Don’t make the mistake of thinking modifiers make smart contracts secure. They can help reduce the availability to external programs, but they don’t provide complete security. Remember that data on a public blockchain is there for anyone to see.
Every smart contract invocation has a caller address. Each EVM knows which account carries out each action. In Solidity, you can access the calling account by referencing msg.sender
. Open the erc20Token.sol smart contract and look at the first line of the constructor body:
constructor(uint256 _initialAmount, string _tokenName,
uint8 _decimalUnits, string _tokenSymbol) public {
balances[msg.sender] = _initialAmount; // The creator owns all tokens
totalSupply = _initialAmount; // Update total token supply
name = _tokenName; // Token name
decimals = _decimalUnits; // Number of decimals
symbol = _tokenSymbol; // Token symbol
}
The first thing you do when the constructor executes (that is, when you deploy this smart contract), is to assign the total initial number of tokens to the calling address’s balance value. Solidity stores the value of the caller’s address in the msg.sender
value, so you can use that to refer to the sender in your code.
You can use the msg.sender
value also to define ownership and enforce access restrictions in your smart contract code. In your supply chain smart contract, you don't want anyone to be able to transfer a product to another participant. Doing so would allow one participant to steal products from others. The transfer process in your supply chain smart contract transfers the ownership of a product as the product moves along the supply chain. It makes sense that only the current owner should be allowed to transfer a product to another owner.
Solidity provides modifiers to help make the task of enforcing ownership easier. A modifier is like a lightweight function. It has a name, input parameters, and a body, but it doesn’t support return values.
Open the SupplyChain.sol smart contract in VS Code and add the following code after the createProduct()
function:
modifier onlyOwner(uint32 _productId) {
require(msg.sender == products[_productId].productOwner );
_;
}
This code defines the onlyOwner
modifier. You can use this modifier on any function to allow only a product's current owner to execute that function. The modifier takes a product ID as an input parameter and checks to see if the msg.sender
is the same address as the product’s current owner address. If the two values match, the modifier is satisfied and the function proceeds. If the two address values do not match, msg.sender
is not allowed to run the function and control returns to the caller.
You invoke modifiers by adding them to existing functions. Scroll to the transferToOwner()
function in the supply chain smart contract code and add the onlyOwner(_prodId)
modifier to the function
header. Your modified function header should look like this:
function transferToOwner(uint32 _user1Id ,uint32 _user2Id, uint32 _prodId) onlyOwner(_prodId) public returns(bool) {
This modifier tells Solidity that before you execute the transferToOwner()
function, execute the onlyOwner()
modifier. The modifier determines whether the function caller (msg.sender
) is the owner of product prodId
. If msg.sender
is the product owner, the transferToOwner()
function proceeds. If not, it just returns without transferring the product.
You can use modifiers to carry out any validation steps that should occur before you run code in a function. Validating ownership is one of the more common uses of modifiers.
Solidity smart contract code can be as insecure as any other software. Just because your code runs in a blockchain environment doesn't mean that it’s secure. You have to keep security in mind throughout the entire software development process. Although you need to consider many things when developing secure software in any environment, developing blockchain dApps requires that you pay special attention to the distributed nature of the blockchain.
When you’re developing Ethereum dApps, you’ll have to avoid many security weaknesses. Table 9-1 lists a few of the most common security mistakes new Ethereum developers tend to overlook.
TABLE 9-1 Common Ethereum Security Mistakes
Security Mistake |
Description |
How to Avoid |
Lack of randomness |
Because smart contract code runs on every EVM, generating random numbers can cause code to run differently on different EVMs. |
Use only random numbers that do not affect stored data or smart contract execution flow. |
Allowing re-entrancy |
The |
Always update state data before transferring control to another function. |
Not checking for overflow and underflow |
Incrementing an integer larger than its maximum value or decrementing an integer smaller than the minimum value causes an error that reverts a smart contract. |
Always check to make sure that increment and decrement operations do not overflow or underflow. |
Permitting |
|
Limit the use of public and external functions that modify state data. |
If you’ve followed the discussion throughout this chapter, your ERC-20 and supply chain smart contracts should be complete and clean. That is, they should compile with no errors. In VS Code, press the F5 key to start the Solidity compiler. Figure 9-4 shows the results of the Solidity compiler.
Note that the Output tab shows a warning. In the SupplyChain.sol smart contract’s transferToOwner()
function, you defined the p1
variable but never used it in the function.
Click or tap the Problems tab to see a list of all problems the compile encountered, as shown in Figure 9-5. Click or tap any problem. VS Code highlights the problem and highlights the line in your source code that corresponds to the problem. If you hover your cursor over the problem or the line of code, more help appears in a pop-up. VS Code provides multiple ways to help you identify and fix problems with your code.
If your smart contracts don’t compile cleanly, review the code to make sure that what you entered is exactly what you see in the book. Pay attention to capitalization — it matters. Another common problem is copying code and pasting it into an editor. Make sure that you copied just what you expected, and look closely at copied quotes. In many cases, copied and pasted quotes result in a quote that won’t compile. If you suspect that problem, just backspace over any pasted quotes and type them directly.
Because it is so easy to introduce minor errors that can stop code from compiling, keep your code as simple as possible As you develop your own smart contracts, implement only the minimal functionality first. You can always add more features after you get the simple code working.