ERC-20 Contract Walk-Through
Introduction
One of the most common uses for Ethereum is for a group to create a tradable token, in a sense their own currency. These tokens typically follow a standard, ERC-20. This standard makes it possible to write tools, such as liquidity pools and wallets, that work with all ERC-20 tokens. In this article we will analyze the OpenZeppelin Solidity ERC20 implementation(opens in a new tab), as well as the interface definition(opens in a new tab).
This is annotated source code. If you want to implement ERC-20, read this tutorial(opens in a new tab).
The Interface
The purpose of a standard like ERC-20 is to allow many tokens implementations that are interoperable across applications, like wallets and decentralized exchanges. To achieve that, we create an interface(opens in a new tab). Any code that needs to use the token contract can use the same definitions in the interface and be compatible with all token contracts that use it, whether it is a wallet such as MetaMask, a dapp such as etherscan.io, or a different contract such as liquidity pool.
If you are an experienced programmer, you probably remember seeing similar constructs in Java(opens in a new tab) or even in C header files(opens in a new tab).
This is a definition of the ERC-20 Interface(opens in a new tab) from OpenZeppelin. It is a translation of the human readable standard(opens in a new tab) into Solidity code. Of course, the interface itself does not define how to do anything. That is explained in the contract source code below.
1// SPDX-License-Identifier: MITKopyahin
Solidity files are supposed to includes a license identifier. You can see the list of licenses here(opens in a new tab). If you need a different license, just explain it in the comments.
1pragma solidity >=0.6.0 <0.8.0;Kopyahin
The Solidity language is still evolving quickly, and new versions may not be compatible with old code (see here(opens in a new tab)). Therefore, it is a good idea to specify not just a minimum version of the language, but also a maximum version, the latest with which you tested the code.
1/**2 * @dev Interface of the ERC20 standard as defined in the EIP.3 */Kopyahin
The @dev
in the comment is part of the NatSpec format(opens in a new tab), used to produce
documentation from the source code.
1interface IERC20 {Kopyahin
By convention, interface names start with I
.
1 /**2 * @dev Returns the amount of tokens in existence.3 */4 function totalSupply() external view returns (uint256);Kopyahin
This function is external
, meaning it can only be called from outside the contract(opens in a new tab).
It returns the total supply of tokens in the contract. This value is returned using the most common type in Ethereum, unsigned 256 bits (256 bits is the
native word size of the EVM). This function is also a view
, which means that it does not change the state, so it can be executed on a single node instead of having
every node in the blockchain run it. This kind of function does not generate a transaction and does not cost gas.
Note: In theory it might appear that a contract's creator could cheat by returning a smaller total supply than the real value, making each token appear more valuable than it actually is. However, that fear ignores the true nature of the blockchain. Everything that happens on the blockchain can be verified by every node. To achieve this, every contract's machine language code and storage is available on every node. While you are not required to publish the Solidity code for your contract, nobody would take you seriously unless you publish the source code and the version of Solidity with which it was complied, so it can be verified against the machine language code you provided. For example, see this contract(opens in a new tab).
1 /**2 * @dev Returns the amount of tokens owned by `account`.3 */4 function balanceOf(address account) external view returns (uint256);Kopyahin
As the name says, balanceOf
returns the balance of an account. Ethereum accounts are identified in Solidity using the address
type, which holds 160 bits.
It is also external
and view
.
1 /**2 * @dev Moves `amount` tokens from the caller's account to `recipient`.3 *4 * Returns a boolean value indicating whether the operation succeeded.5 *6 * Emits a {Transfer} event.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);Kopyahin
The transfer
function transfers a tokens from the caller to a different address. This involves a change of state, so it isn't a view
.
When a user calls this function it creates a transaction and costs gas. It also emits an event, Transfer
, to inform everybody on
the blockchain of the event.
The function has two types of output for two different types of callers:
- Users that call the function directly from a user interface. Typically the user submits a transaction
and does not wait for a response, which could take an indefinite amount of time. The user can see what happened
by looking for the transaction receipt (which is identified by the transaction hash) or by looking for the
Transfer
event. - Other contracts, which call the function as part of an overall transaction. Those contracts get the result immediately, because they run in the same transaction, so they can use the function return value.
The same type of output is created by the other functions that change the contract's state.
Allowances permit an account to spend some tokens that belong to a different owner. This is useful, for example, for contracts that act as sellers. Contracts cannot monitor for events, so if a buyer were to transfer tokens to the seller contract directly that contract wouldn't know it was paid. Instead, the buyer permits the seller contract to spend a certain amount, and the seller transfers that amount. This is done through a function the seller contract calls, so the seller contract can know if it was successful.
1 /**2 * @dev Returns the remaining number of tokens that `spender` will be3 * allowed to spend on behalf of `owner` through {transferFrom}. This is4 * zero by default.5 *6 * This value changes when {approve} or {transferFrom} are called.7 */8 function allowance(address owner, address spender) external view returns (uint256);Kopyahin
The allowance
function lets anybody query to see what is the allowance that one
address (owner
) lets another address (spender
) spend.
1 /**2 * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.3 *4 * Returns a boolean value indicating whether the operation succeeded.5 *6 * IMPORTANT: Beware that changing an allowance with this method brings the risk7 * that someone may use both the old and the new allowance by unfortunate8 * transaction ordering. One possible solution to mitigate this race9 * condition is to first reduce the spender's allowance to 0 and set the10 * desired value afterwards:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emits an {Approval} event.14 */15 function approve(address spender, uint256 amount) external returns (bool);Ipakita lahatKopyahin
The approve
function creates an allowance. Make sure to read the message about
how it can be abused. In Ethereum you control the order of your own transactions,
but you cannot control the order in which other people's transactions will
be executed, unless you don't submit your own transaction until you see the
other side's transaction had happened.
1 /**2 * @dev Moves `amount` tokens from `sender` to `recipient` using the3 * allowance mechanism. `amount` is then deducted from the caller's4 * allowance.5 *6 * Returns a boolean value indicating whether the operation succeeded.7 *8 * Emits a {Transfer} event.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Ipakita lahatKopyahin
Finally, transferFrom
is used by the spender to actually spend the allowance.
12 /**3 * @dev Emitted when `value` tokens are moved from one account (`from`) to4 * another (`to`).5 *6 * Note that `value` may be zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by12 * a call to {approve}. `value` is the new allowance.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Ipakita lahatKopyahin
These events are emitted when the state of the ERC-20 contract changes.
The Actual Contract
This is the actual contract that implements the ERC-20 standard, taken from here(opens in a new tab). It is not meant to be used as-is, but you can inherit(opens in a new tab) from it to extend it to something usable.
1// SPDX-License-Identifier: MIT2pragma solidity >=0.6.0 <0.8.0;Kopyahin
Import Statements
In addition to the interface definitions above, the contract definition imports two other files:
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";Kopyahin
GSN/Context.sol
is the definitions required to use OpenGSN(opens in a new tab), a system that allows users without ether to use the blockchain. Note that this is an old version, if you want to integrate with OpenGSN use this tutorial(opens in a new tab).- The SafeMath library(opens in a new tab), which is used to make addition and subtraction without overflows. This is necessary because otherwise a person might somehow have one token, spend two tokens, and then have 2^256-1 tokens.
This comment explains the purpose of the contract.
1/**2 * @dev Implementation of the {IERC20} interface.3 *4 * This implementation is agnostic to the way tokens are created. This means5 * that a supply mechanism has to be added in a derived contract using {_mint}.6 * For a generic mechanism see {ERC20PresetMinterPauser}.7 *8 * TIP: For a detailed writeup see our guide9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How10 * to implement supply mechanisms].11 *12 * We have followed general OpenZeppelin guidelines: functions revert instead13 * of returning `false` on failure. This behavior is nonetheless conventional14 * and does not conflict with the expectations of ERC20 applications.15 *16 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.17 * This allows applications to reconstruct the allowance for all accounts just18 * by listening to said events. Other implementations of the EIP may not emit19 * these events, as it isn't required by the specification.20 *21 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}22 * functions have been added to mitigate the well-known issues around setting23 * allowances. See {IERC20-approve}.24 */25Ipakita lahatKopyahin
Contract Definition
1contract ERC20 is Context, IERC20 {Kopyahin
This line specifies the inheritance, in this case from IERC20
from above and Context
, for OpenGSN.
12 using SafeMath for uint256;3Kopyahin
This line attaches the SafeMath
library to the uint256
type. You can find this library
here(opens in a new tab).
Variable Definitions
These definitions specify the contract's state variables. These variables are declared private
, but
that only means that other contracts on the blockchain can't read them. There are no
secrets on the blockchain, the software on every node has the state of every contract
at every block. By convention, state variables are named _<something>
.
The first two variables are mappings(opens in a new tab), meaning they behave roughly the same as associative arrays(opens in a new tab), except that the keys are numeric values. Storage is only allocated for entries that have values different from the default (zero).
1 mapping (address => uint256) private _balances;Kopyahin
The first mapping, _balances
, is addresses and their respective balances of this token. To access
the balance, use this syntax: _balances[<address>]
.
1 mapping (address => mapping (address => uint256)) private _allowances;Kopyahin
This variable, _allowances
, stores the allowances explained earlier. The first index is the owner
of the tokens, and the second is the contract with the allowance. To access the amount address A can
spend from address B's account, use _allowances[B][A]
.
1 uint256 private _totalSupply;Kopyahin
As the name suggests, this variable keeps track of the total supply of tokens.
1 string private _name;2 string private _symbol;3 uint8 private _decimals;Kopyahin
These three variables are used to improve readability. The first two are self-explanatory, but _decimals
isn't.
On one hand, ethereum does not have floating point or fractional variables. On the other hand, humans like being able to divide tokens. One reason people settled on gold for currency was that it was hard to make change when somebody wanted to buy a duck's worth of cow.
The solution is to keep track of integers, but count instead of the real token a fractional token that is nearly worthless. In the case of ether, the fractional token is called wei, and 10^18 wei is equal to one ETH. At writing, 10,000,000,000,000 wei is approximately one US or Euro cent.
Applications need to know how to display the token balance. If a user has 3,141,000,000,000,000,000 wei, is that
3.14 ETH? 31.41 ETH? 3,141 ETH? In the case of ether it is defined 10^18 wei to the ETH, but for your
token you can select a different value. If dividing the token doesn't make sense, you can use a
_decimals
value of zero. If you want to use the same standard as ETH, use the value 18.
The Constructor
1 /**2 * @dev Sets the values for {name} and {symbol}, initializes {decimals} with3 * a default value of 18.4 *5 * To select a different value for {decimals}, use {_setupDecimals}.6 *7 * All three of these values are immutable: they can only be set once during8 * construction.9 */10 constructor (string memory name_, string memory symbol_) public {11 _name = name_;12 _symbol = symbol_;13 _decimals = 18;14 }Ipakita lahatKopyahin
The constructor is called when the contract is first created. By convention, function parameters are named <something>_
.
User Interface Functions
1 /**2 * @dev Returns the name of the token.3 */4 function name() public view returns (string memory) {5 return _name;6 }78 /**9 * @dev Returns the symbol of the token, usually a shorter version of the10 * name.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**17 * @dev Returns the number of decimals used to get its user representation.18 * For example, if `decimals` equals `2`, a balance of `505` tokens should19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).20 *21 * Tokens usually opt for a value of 18, imitating the relationship between22 * ether and wei. This is the value {ERC20} uses, unless {_setupDecimals} is23 * called.24 *25 * NOTE: This information is only used for _display_ purposes: it in26 * no way affects any of the arithmetic of the contract, including27 * {IERC20-balanceOf} and {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }Ipakita lahatKopyahin
These functions, name
, symbol
, and decimals
help user interfaces know about your contract so they'll be able to display it properly.
The return type is string memory
, meaning return a string that is stored in memory. Variables, such as
strings, can be stored in three locations:
Lifetime | Contract Access | Gas Cost | |
---|---|---|---|
Memory | Function call | Read/Write | Tens or hundreds (higher for higher locations) |
Calldata | Function call | Read Only | Can't be used as a return type, only a function parameter type |
Storage | Until changed | Read/Write | High (800 for read, 20k for write) |
In this case, memory
is the best choice.
Read Token Information
These are functions that provide information about the token, either the total supply or an account's balance.
1 /**2 * @dev See {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }Kopyahin
The totalSupply
function returns the total supply of tokens.
1 /**2 * @dev See {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }Kopyahin
Read an account's balance. Note that anybody is allowed to get anybody else's account balance. There is no point in trying to hide this information, because it is available on every node anyway. There are no secrets on the blockchain.
Transfer Tokens
1 /**2 * @dev See {IERC20-transfer}.3 *4 * Requirements:5 *6 * - `recipient` cannot be the zero address.7 * - the caller must have a balance of at least `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {Ipakita lahatKopyahin
The transfer
function is called to transfer tokens from the sender's account to a different one. Note
that even though it returns a boolean value, that value is always true. If the transfer
fails the contract reverts the call.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }Kopyahin
The _transfer
function does the actual work. It is a private function that can only be called by
other contract functions. By convention private functions are named _<something>
, same as state
variables.
Normally in Solidity we use msg.sender
for the message sender. However, that breaks
OpenGSN(opens in a new tab). If we want to allow etherless transactions with our token, we
need to use _msgSender()
. It returns msg.sender
for normal transactions, but for etherless ones
return the original signer and not the contract that relayed the message.
Allowance Functions
These are the functions that implement the allowance functionality: allowance
, approve
, transferFrom
,
and _approve
. Additionally, the OpenZeppelin implementation goes beyond the basic standard to include some features that improve
security: increaseAllowance
, and decreaseAllowance
.
The allowance function
1 /**2 * @dev See {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }Kopyahin
The allowance
function allows everybody to check any allowance.
The approve function
1 /**2 * @dev See {IERC20-approve}.3 *4 * Requirements:5 *6 * - `spender` cannot be the zero address.7 */8 function approve(address spender, uint256 amount) public virtual override returns (bool) {Kopyahin
This function is called to create an allowance. It is similar to the transfer
function above:
- The function just calls an internal function (in this case,
_approve
) that does the real work. - The function either returns
true
(if successful) or reverts (if not).
1 _approve(_msgSender(), spender, amount);2 return true;3 }Kopyahin
We use internal functions to minimize the number of places where state changes happen. Any function that changes the state is a potential security risk that needs to be audited for security. This way we have less chances to get it wrong.
The transferFrom function
This is the function that a spender calls to spend an allowance. This requires two operations: transfer the amount being spent and reduce the allowance by that amount.
1 /**2 * @dev See {IERC20-transferFrom}.3 *4 * Emits an {Approval} event indicating the updated allowance. This is not5 * required by the EIP. See the note at the beginning of {ERC20}.6 *7 * Requirements:8 *9 * - `sender` and `recipient` cannot be the zero address.10 * - `sender` must have a balance of at least `amount`.11 * - the caller must have allowance for ``sender``'s tokens of at least12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Ipakita lahatKopyahin
The a.sub(b, "message")
function call does two things. First, it calculates a-b
, which is the new allowance.
Second, it checks that this result is not negative. If it is negative the call reverts with the provided message. Note that when a call reverts any processing done previously during that call is ignored so we don't need to
undo the _transfer
.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: transfer amount exceeds allowance"));3 return true;4 }Kopyahin
OpenZeppelin safety additions
It is dangerous to set a non-zero allowance to another non-zero value, because you only control the order of your own transactions, not anybody else's. Imagine you have two users, Alice who is naive and Bill who is dishonest. Alice wants some service from Bill, which she thinks costs five tokens - so she gives Bill an allowance of five tokens.
Then something changes and Bill's price rises to ten tokens. Alice, who still wants the service, sends a transaction that sets Bill's allowance to ten. The moment Bill sees this new transaction in the transaction pool he sends a transaction that spends Alice's five tokens and has a much higher gas price so it will be mined faster. That way Bill can spend first five tokens and then, once Alice's new allowance is mined, spend ten more for a total price of fifteen tokens, more than Alice meant to authorize. This technique is called front-running(opens in a new tab)
Alice Transaction | Alice Nonce | Bill Transaction | Bill Nonce | Bill's Allowance | Bill Total Income from Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
approve(Bill, 10) | 11 | 10 | 5 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
To avoid this problem, these two functions (increaseAllowance
and decreaseAllowance
) allow you
to modify the allowance by a specific amount. So if Bill had already spent five tokens, he'll just
be able to spend five more. Depending on the timing, there are two ways this can work, both of
which end with Bill only getting ten tokens:
A:
Alice Transaction | Alice Nonce | Bill Transaction | Bill Nonce | Bill's Allowance | Bill Total Income from Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
Alice Transaction | Alice Nonce | Bill Transaction | Bill Nonce | Bill's Allowance | Bill Total Income from Alice |
---|---|---|---|---|---|
approve(Bill, 5) | 10 | 5 | 0 | ||
increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev Atomically increases the allowance granted to `spender` by the caller.3 *4 * This is an alternative to {approve} that can be used as a mitigation for5 * problems described in {IERC20-approve}.6 *7 * Emits an {Approval} event indicating the updated allowance.8 *9 * Requirements:10 *11 * - `spender` cannot be the zero address.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }Ipakita lahatKopyahin
The a.add(b)
function is a safe add. In the unlikely case that a
+b
>=2^256
it does not wrap around
the way normal addition does.
12 /**3 * @dev Atomically decreases the allowance granted to `spender` by the caller.4 *5 * This is an alternative to {approve} that can be used as a mitigation for6 * problems described in {IERC20-approve}.7 *8 * Emits an {Approval} event indicating the updated allowance.9 *10 * Requirements:11 *12 * - `spender` cannot be the zero address.13 * - `spender` must have allowance for the caller of at least14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }Ipakita lahatKopyahin
Functions that Modify Token Information
These are the four functions that do the actual work: _transfer
, _mint
, _burn
, and _approve
.
The _transfer function {#_transfer}
1 /**2 * @dev Moves tokens `amount` from `sender` to `recipient`.3 *4 * This is internal function is equivalent to {transfer}, and can be used to5 * e.g. implement automatic token fees, slashing mechanisms, etc.6 *7 * Emits a {Transfer} event.8 *9 * Requirements:10 *11 * - `sender` cannot be the zero address.12 * - `recipient` cannot be the zero address.13 * - `sender` must have a balance of at least `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {Ipakita lahatKopyahin
This function, _transfer
, transfers tokens from one account to another. It is called by both
transfer
(for transfers from the sender's own account) and transferFrom
(for using allowances
to transfer from somebody else's account).
1 require(sender != address(0), "ERC20: transfer from the zero address");2 require(recipient != address(0), "ERC20: transfer to the zero address");Kopyahin
Nobody actually owns address zero in Ethereum (that is, nobody knows a private key whose matching public key is transformed to the zero address). When people use that address, it is usually a software bug - so we fail if the zero address is used as the sender or the recipient.
1 _beforeTokenTransfer(sender, recipient, amount);2Kopyahin
There are two ways to use this contract:
- Use it as a template for your own code
- Inherit from it(opens in a new tab), and override only those functions that you need to modify
The second method is much better because the OpenZeppelin ERC-20 code has already been audited and shown to be secure. When you use inheritance it is clear what are the functions you modify, and to trust your contract people only need to audit those specific functions.
It is often useful to perform a function each time tokens change hands. However,_transfer
is a very important function and it is
possible to write it insecurely (see below), so it is best not to override it. The solution is _beforeTokenTransfer
, a
hook function(opens in a new tab). You can override this function, and it will be called on each transfer.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _balances[recipient] = _balances[recipient].add(amount);Kopyahin
These are the lines that actually do the transfer. Note that there is nothing between them, and that we subtract the transferred amount from the sender before adding it to the recipient. This is important because if there was a call to a different contract in the middle, it could have been used to cheat this contract. This way the transfer is atomic, nothing can happen in the middle of it.
1 emit Transfer(sender, recipient, amount);2 }Kopyahin
Finally, emit a Transfer
event. Events are not accessible to smart contracts, but code running outside the blockchain
can listen for events and react to them. For example, a wallet can keep track of when the owner gets more tokens.
The _mint and _burn functions {#_mint-and-_burn}
These two functions (_mint
and _burn
) modify the total supply of tokens.
They are internal and there is no function that calls them in this contract,
so they are only useful if you inherit from the contract and add your own
logic to decide under what conditions to mint new tokens or burn existing
ones.
NOTE: Every ERC-20 token has its own business logic that dictates token management.
For example, a fixed supply contract might only call _mint
in the constructor and never call _burn
. A contract that sells tokens
will call _mint
when it is paid, and presumably call _burn
at some point
to avoid runaway inflation.
1 /** @dev Creates `amount` tokens and assigns them to `account`, increasing2 * the total supply.3 *4 * Emits a {Transfer} event with `from` set to the zero address.5 *6 * Requirements:7 *8 * - `to` cannot be the zero address.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }Ipakita lahatKopyahin
Make sure to update _totalSupply
when the total number of tokens changes.
1 /**2 * @dev Destroys `amount` tokens from `account`, reducing the3 * total supply.4 *5 * Emits a {Transfer} event with `to` set to the zero address.6 *7 * Requirements:8 *9 * - `account` cannot be the zero address.10 * - `account` must have at least `amount` tokens.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");1415 _beforeTokenTransfer(account, address(0), amount);1617 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }Ipakita lahatKopyahin
The _burn
function is almost identical to _mint
, except it goes in the other direction.
The _approve function {#_approve}
This is the function that actually specifies allowances. Note that it allows an owner to specify an allowance that is higher than the owner's current balance. This is OK because the balance is checked at the time of transfer, when it could be different from the balance when the allowance is created.
1 /**2 * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.3 *4 * This internal function is equivalent to `approve`, and can be used to5 * e.g. set automatic allowances for certain subsystems, etc.6 *7 * Emits an {Approval} event.8 *9 * Requirements:10 *11 * - `owner` cannot be the zero address.12 * - `spender` cannot be the zero address.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");1718 _allowances[owner][spender] = amount;Ipakita lahatKopyahin
Emit an Approval
event. Depending on how the application is written, the spender contract can be told about the
approval either by the owner or by a server that listens to these events.
1 emit Approval(owner, spender, amount);2 }3Kopyahin
Modify The Decimals Variable
123 /**4 * @dev Sets {decimals} to a value other than the default one of 18.5 *6 * WARNING: This function should only be called from the constructor. Most7 * applications that interact with token contracts will not expect8 * {decimals} to ever change, and may work incorrectly if it does.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Ipakita lahatKopyahin
This function modifies the _decimals
variable which is used to tell user interfaces how to interpret the amount.
You should call it from the constructor. It would be dishonest to call it at any subsequent point, and applications
are not designed to handle it.
Hooks
12 /**3 * @dev Hook that is called before any transfer of tokens. This includes4 * minting and burning.5 *6 * Calling conditions:7 *8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens9 * will be to transferred to `to`.10 * - when `from` is zero, `amount` tokens will be minted for `to`.11 * - when `to` is zero, `amount` of ``from``'s tokens will be burned.12 * - `from` and `to` are never both zero.13 *14 * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}Ipakita lahatKopyahin
This is the hook function to be called during transfers. It is empty here, but if you need it to do something you just override it.
Conclusion
For review, here are some of the most important ideas in this contract (in my opinion, yours is likely to vary):
- There are no secrets on the blockchain. Any information that a smart contract can access is available to the whole world.
- You can control the order of your own transactions, but not when other people's transaction happen. This is the reason that changing an allowance can be dangerous, because it lets the spender spend the sum of both allowances.
- Values of type
uint256
wrap around. In other words, 0-1=2^256-1. If that is not desired behavior, you have to check for it (or use the SafeMath library that does it for you). Note that this changed in Solidity 0.8.0(opens in a new tab). - Do all state changes of a specific type in a specific place, because it makes auditing easier.
This is the reason that we have, for example,
_approve
, which is called byapprove
,transferFrom
,increaseAllowance
, anddecreaseAllowance
- State changes should be atomic, without any other action in their middle (as you can see
in
_transfer
). This is because during the state change you have an inconsistent state. For example, between the time you deduct from the balance of the sender and the time you add to the balance of the recipient there are less token in existence than there should be. This could be potentially abused if there are operations between them, especially calls to a different contract.
Now that you've seen how the OpenZeppelin ERC-20 contract is written, and especially how it is made more secure, go and write your own secure contracts and applications.
Huling pag-edit: @nhsz(opens in a new tab), Pebrero 18, 2024