|
| 1 | +--- |
| 2 | +title: ERC-20 with Safety Rails |
| 3 | +description: How to help people avoid silly mistakes |
| 4 | +author: Ori Pomerantz |
| 5 | +lang: en |
| 6 | +sidebar: true |
| 7 | +tags: ["erc-20"] |
| 8 | +skill: beginner |
| 9 | +published: 2022-08-15 |
| 10 | +--- |
| 11 | + |
| 12 | +## Introduction {#introduction} |
| 13 | + |
| 14 | +One of the great things about Ethereum is that there is no central authority that can modify or undo your transactions. One of the great problems with Ethereum is that ther is no central authority with the power to undo user mistakes or illicit transactions. In this article you learn about some of the common mistakes that users commit with [ERC-20](/developers/docs/standards/tokens/erc-20/) tokens, as well as how to create ERC-20 contracts that help users to avoid those mistakes, or that give a central authority some power (for example to freeze accounts). |
| 15 | + |
| 16 | +Note that while we will use the [OpenZeppelin ERC-20 token contract](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20), this article does not explain it in great details. You can find this information [here](/developers/tutorials/erc20-annotated-code). |
| 17 | + |
| 18 | +If you want to see the complete source code: |
| 19 | + |
| 20 | +1. Open the [Remix IDE](https://remix.ethereum.org/). |
| 21 | +2. Click the clone github icon (). |
| 22 | +3. Clone the github repository `https://github.com/qbzzt/20220815-erc20-safety-rails`. |
| 23 | +4. Open **contracts > erc20-safety-rails.sol**. |
| 24 | + |
| 25 | +## Creating an ERC-20 contract {#creating-an-erc-20-contract} |
| 26 | + |
| 27 | +Before we can add the safety rail functionality we need an ERC-20 contract. In this article we'll use [the OpenZeppelin Contracts Wizard](https://docs.openzeppelin.com/contracts/4.x/wizard). Open it in another browser and follow these instructions: |
| 28 | + |
| 29 | +1. Select **ERC20**. |
| 30 | +2. Enter these settings: |
| 31 | + |
| 32 | + | Parameter | Value | |
| 33 | + | - | - | |
| 34 | + | Name | SafetyRailsToken |
| 35 | + | Symbol | SAFE |
| 36 | + | Premint | 1000 |
| 37 | + | Features | None |
| 38 | + | Access Control | Ownable |
| 39 | + | Upgradability | None |
| 40 | + |
| 41 | +3. Scroll up and click **Open in Remix** (for Remix) or **Download** to use a different environment. I'm going to assume you're using Remix, if you use something else just make the appropriate changes. |
| 42 | +4. We now have a fully functional ERC-20 contract. You can expand `.deps` > `npm` to see the imported code. |
| 43 | +5. Compile, deploy, and play with the contract to see that it functions as an ERC-20 contract. If you need to learn how to use Remix, [use this tutorial](https://remix.ethereum.org/?#activate=udapp,solidity,LearnEth). |
| 44 | + |
| 45 | + |
| 46 | +## Common mistakes {#common-mistakes} |
| 47 | + |
| 48 | +### The mistakes {#the-mistakes} |
| 49 | + |
| 50 | +Users sometimes send tokens to the wrong address. While we cannot read their minds to know what they meant to do, there are two error types that happen a lot and are easy to detect: |
| 51 | + |
| 52 | +1. Sending the tokens to the contract's own address. For example, [Optimism's OP token](https://optimism.mirror.xyz/qvd0WfuLKnePm1Gxb9dpGchPf5uDz5NSMEFdgirDS4c) managed to accumulate [over 120,000](https://optimistic.etherscan.io/address/0x4200000000000000000000000000000000000042#tokentxns) OP tokens in less than two months. This represents a significant amount of wealth that presumably people just lost. |
| 53 | + |
| 54 | +2. Sending the tokens to an empty address, one that doesn't correspond to an [externally owned account](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs) or a [smart contract](/developers/docs/smart-contracts). While I don't have statistics on how often this happens, [one incident could have cost 20,000,000 tokens](https://gov.optimism.io/t/message-to-optimism-community-from-wintermute/2595). |
| 55 | + |
| 56 | + |
| 57 | +### Preventing transfers {#preventing-transfers} |
| 58 | + |
| 59 | +The OpenZeppelin ERC-20 contract includes [a hook, `_beforeTokenTransfer`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L364-L368), that is called before a token is transferred. By default this hook does not do anything, but we can hang our own functionality on it, such as checks that revert if there's a problem. |
| 60 | + |
| 61 | +To use the hook, add this function after the constructor: |
| 62 | + |
| 63 | +```solidity |
| 64 | + function _beforeTokenTransfer(address from, address to, uint256 amount) |
| 65 | + internal virtual |
| 66 | + override(ERC20) |
| 67 | + { |
| 68 | + super._beforeTokenTransfer(from, to, amount); |
| 69 | + } |
| 70 | +``` |
| 71 | + |
| 72 | +Some parts of this function may be new if you aren't very familiar with Solidity: |
| 73 | + |
| 74 | +```solidity |
| 75 | + internal virtual |
| 76 | +``` |
| 77 | + |
| 78 | +The `virtual` keyword means that just as we inherited functionality from `ERC20` and overrode this function, other contracts can inherit from us and override this function. |
| 79 | + |
| 80 | +```solidity |
| 81 | + override(ERC20) |
| 82 | +``` |
| 83 | + |
| 84 | +We have to specify explicitly that we're [overriding](https://docs.soliditylang.org/en/v0.8.15/contracts.html#function-overriding) the ERC20 token definition of `_beforeTokenTransfer`. In general, explicit definitions are a lot better, from the security standpoint, than implicit ones - you cannot forget that you've done something if it's right in front of you. That is also the reason we need to specify which superclass's `_beforeTokenTransfer` we are overriding. |
| 85 | + |
| 86 | +```solidity |
| 87 | + super._beforeTokenTransfer(from, to, amount); |
| 88 | +``` |
| 89 | + |
| 90 | +This line calls the `_beforeTokenTransfer` function of the contract or contracts from which we inherited which have it. In this case, that is only `ERC20`, `Ownable` does not have this hook. Even though currently `ERC20._beforeTokenTransfer` doesn't do anything, we call it in case functionality is added in the future (and we then decide to redeploy the contract, because contracts don't change after deployment). |
| 91 | + |
| 92 | +### Coding the requirements {#coding-the-requirements} |
| 93 | + |
| 94 | +We want to add these requirements to the function: |
| 95 | + |
| 96 | +- The `to` address cannot equal `address(this)`, the address of the ERC-20 contract itself. |
| 97 | +- The `to` address cannot be empty, it has to be either: |
| 98 | + - An externally owned accounts (EOA). We can't check if an address is an EOA directly, but we can check an address's ETH balance. EOAs almost always have a balance, even if they are no longer used - it's difficult to clear them to the last wei. |
| 99 | + - A smart contract. Testing if an address is a smart contract is a bit harder. There is an opcode that checks the external code length, called [`EXTCODESIZE`](https://www.evm.codes/#3b), but it is not available directly in Solidity. We have to use [Yul](https://docs.soliditylang.org/en/v0.8.15/yul.html), which is EVM assembly, for it. There are other values we could use from Solidity ([`<address>.code` and `<address>.codehash`](https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#members-of-address-types)), but they cost more. |
| 100 | + |
| 101 | +Lets go over the new code line by line: |
| 102 | + |
| 103 | +```solidity |
| 104 | + require(to != address(this), "Can't send tokens to the contract address"); |
| 105 | +``` |
| 106 | + |
| 107 | +This is the first requirement, check that `to` and `this(address)` are not the same thing. |
| 108 | + |
| 109 | +```solidity |
| 110 | + bool isToContract; |
| 111 | + assembly { |
| 112 | + isToContract := gt(extcodesize(to), 0) |
| 113 | + } |
| 114 | +``` |
| 115 | + |
| 116 | +This is how we check if an address is a contract. We cannot receive output directly from Yul, so instead we define a variable to hold the result (`isToContract` in this case). The way Yul works is that every opcode is considered a function. So first we call [`EXTCODESIZE`](https://www.evm.codes/#3b) to get the contract size, and then use [`GT`](https://www.evm.codes/#11) to check it is not zero (we are dealing with unsigned integers, so of course it can't be negative). We then write the result to `isToContract`. |
| 117 | + |
| 118 | +```solidity |
| 119 | + require(to.balance != 0 || isToContract, "Can't send tokens to an empty address"); |
| 120 | +``` |
| 121 | + |
| 122 | +And finally, we have the actual check for empty addresses. |
| 123 | + |
| 124 | + |
| 125 | + |
| 126 | +## Administrative access {#admin-access} |
| 127 | + |
| 128 | +Sometimes it is useful to have an administrator that can undo mistakes. To reduce the potential for abuse, this administrator can be a [multisig](https://blog.logrocket.com/security-choices-multi-signature-wallets/) so multiple people have to agree on an action. In this article we'll have two administrative features: |
| 129 | + |
| 130 | +1. Freezing and unfreezing accounts. This can be useful, for example, when an account might be compromised. |
| 131 | +2. Asset cleanup. |
| 132 | + |
| 133 | + Sometimes frauds send fraudulant tokens to the real token's contract to gain legitimacy. For example, [see here](https://optimistic.etherscan.io/token/0x2348b1a1228ddcd2db668c3d30207c3e1852fbbe?a=0x4200000000000000000000000000000000000042). The legitimate ERC-20 contract is [0x4200....0042](https://optimistic.etherscan.io/address/0x4200000000000000000000000000000000000042). The scam that pretends to be it is [0x234....bbe](https://optimistic.etherscan.io/address/0x2348b1a1228ddcd2db668c3d30207c3e1852fbbe). |
| 134 | + |
| 135 | + It is also possible that people send legitimate ERC-20 tokens to our contract by mistake, which is anotehr reason to want to have a way to get them out. |
| 136 | + |
| 137 | +OpenZeppelin provides two mechanisms to enable administrative access: |
| 138 | + |
| 139 | +- [`Ownable`](https://docs.openzeppelin.com/contracts/4.x/access-control#ownership-and-ownable) contracts have a single owner. Functions that have the `onlyOwner` [modifier](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm) can only be called by that owner. Owners can transfer ownership to somebody else or renounce it completely. The rights of all other accounts are typically identical. |
| 140 | +- [`AccessControl`](https://docs.openzeppelin.com/contracts/4.x/access-control#role-based-access-control) contracts have [role based access control (RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control). |
| 141 | + |
| 142 | +For the sake of simplicity, in this article we use `Ownable`. |
| 143 | + |
| 144 | + |
| 145 | +### Freezing and thawing contracts {#freezing-and-thawing-contracts} |
| 146 | + |
| 147 | +Freezing and thawing contracts requires several changes: |
| 148 | + |
| 149 | +- A [mapping](https://www.tutorialspoint.com/solidity/solidity_mappings.htm) from addresses to [booleans](https://en.wikipedia.org/wiki/Boolean_data_type) to keep track of which addresses are frozen. All values are initially zero, which for boolean values is interpreted as false. This is what we want because by default accounts are not frozen. |
| 150 | + |
| 151 | + ```solidity |
| 152 | + mapping(address => bool) public frozenAccounts; |
| 153 | + ``` |
| 154 | + |
| 155 | +- [Events](https://www.tutorialspoint.com/solidity/solidity_events.htm) to inform anybody interested when an account is frozen or thawed. Technically speaking events are not required for these actions, but it helps off chain code to be able to listen to these events and know what is happening. It's considered good manners for a smart contract to emit them when something that miught be relevant to somebody else happens. |
| 156 | + |
| 157 | + The events are indexed so will be possible to search for all the times an account has been frozen or thawed. |
| 158 | + |
| 159 | + ```solidity |
| 160 | + // When accounts are frozen or unfrozen |
| 161 | + event AccountFrozen(address indexed _addr); |
| 162 | + event AccountThawed(address indexed _addr); |
| 163 | + ``` |
| 164 | + |
| 165 | +- Functions for freezing and thawing accounts. These two functions are nearly identical, so we'll only go over the freeze function. |
| 166 | + |
| 167 | + ```solidity |
| 168 | + function freezeAccount(address addr) |
| 169 | + public |
| 170 | + onlyOwner |
| 171 | + ``` |
| 172 | + |
| 173 | + Functions marked [`public`](https://www.tutorialspoint.com/solidity/solidity_contracts.htm) can be called from other smart contracts or directly by a transaction. |
| 174 | + |
| 175 | + ```solidity |
| 176 | + { |
| 177 | + require(!frozenAccounts[addr], "Account already frozen"); |
| 178 | + frozenAccounts[addr] = true; |
| 179 | + emit AccountFrozen(addr); |
| 180 | + } // freezeAccount |
| 181 | + ``` |
| 182 | + |
| 183 | + If the account is already frozen, revert. Otherwise, freeze it and `emit` an event. |
| 184 | + |
| 185 | +- Change `_beforeTokenTransfer` to prevent money being moved from a frozen account. Note that money can still be transferred into the frozen account. |
| 186 | + |
| 187 | + ```solidity |
| 188 | + require(!frozenAccounts[from], "The account is frozen"); |
| 189 | + ``` |
| 190 | + |
| 191 | + |
| 192 | +### Asset cleanup {#asset-cleanup} |
| 193 | + |
| 194 | +To release ERC-20 tokens held by this contract we need to call a function on the token contract to which they belong, either [`transfer`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#transfer) or [`approve`](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#approve). There's no point wasting gas in this case on allowances, we might as well transfer directly. |
| 195 | + |
| 196 | +```solidity |
| 197 | + function cleanupERC20( |
| 198 | + address erc20, |
| 199 | + address dest |
| 200 | + ) |
| 201 | + public |
| 202 | + onlyOwner |
| 203 | + { |
| 204 | + IERC20 token = IERC20(erc20); |
| 205 | +``` |
| 206 | + |
| 207 | +This is the syntax to create an object for a contract when we receive the address. We can do this because we have the definition for ERC20 tokens as part of the source code (see line 4), and that file includes [the definition for IERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol), the interface for an OpenZeppelin ERC-20 contract. |
| 208 | + |
| 209 | +```solidity |
| 210 | + uint balance = token.balanceOf(address(this)); |
| 211 | + token.transfer(dest, balance); |
| 212 | + } |
| 213 | +``` |
| 214 | + |
| 215 | +This is a cleanup function, so presumably we don't want to leave any tokens. Instead of getting the balance from the user manually, we might as well automate the process. |
| 216 | + |
| 217 | + |
| 218 | +## Conclusion {#conclusion} |
| 219 | + |
| 220 | +This is not a perfect solution, there is no perfect solution to the "user made a mistake" problem. However, using this kind of check can at least prevent some mistakes. The ability to freeze accounts, while dangerous, can be used to limit the damage of certain hacks by denying the hacker the stolen funds. |
0 commit comments