|
| 1 | +--- |
| 2 | +title: "Some tricks used by scam tokens and how to detect them" |
| 3 | +description: In this tutorial we dissect a scam token to see some of the tricks that scammers play, how they implement them, and how we can detect them. |
| 4 | +author: Ori Pomerantz |
| 5 | +tags: ["scam", "solidity", "erc-20"] |
| 6 | +skill: intermediate |
| 7 | +published: 2023-09-15 |
| 8 | +lang: en |
| 9 | +--- |
| 10 | + |
| 11 | +In this tutorial we dissect [a scam token](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code) to see some of the tricks that scammers play and how they implement them. By the end of the tutorial you will have a more comprehensive view of ERC-20 token contracts, their capabilities, and why skepticism is necessary. |
| 12 | + |
| 13 | +## Scam tokens - what are they, why do people do them, and how to avoid them {#scam-tokens} |
| 14 | + |
| 15 | +One of the most common uses for Ethereum is for a group to create a tradable token, in a sense their own currency. However, anywhere there are legitimate use cases that bring value, there are also criminals who try to steal that value for themselves. |
| 16 | + |
| 17 | +You can read more about this subject [elsewhere on ethereum.org](/guides/how-to-id-scam-tokens/) from a user perspective. This tutorial focuses on dissecting a scam token to see how it's done and how it can be detected. |
| 18 | + |
| 19 | + |
| 20 | +### How do I know wARB is a scam? {#warb-scam} |
| 21 | + |
| 22 | +The token we dissect is [wARB](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code), which pretends to be equivalent to the legitimate [ARB token](https://etherscan.io/token/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1). |
| 23 | + |
| 24 | +The easiest way to know which is the legitimate token is looking at the originating organization, [Arbitrum](https://arbitrum.foundation/). The legitimate addresses are specified [in their documentation](https://docs.arbitrum.foundation/deployment-addresses#token). |
| 25 | + |
| 26 | + |
| 27 | +### Why is the source code available? {#why-source} |
| 28 | + |
| 29 | +Normally we'd expect people who try to scam others to be secretive, and indeed many scam tokens do not have their code available (for example, [this one](https://optimistic.etherscan.io/token/0x15992f382d8c46d667b10dc8456dc36651af1452#code) and [this one](https://optimistic.etherscan.io/token/0x026b623eb4aada7de37ef25256854f9235207178#code)). |
| 30 | + |
| 31 | +However, legitimate tokens usually publish their source code, so to appear legitimate scam tokens' authors' sometimes do the same. [wARB](https://etherscan.io/token/0xb047c8032b99841713b8e3872f06cf32beb27b82#code) is one of those tokens with source code available, which makes it easier to understand it. |
| 32 | + |
| 33 | +While contract deployers can choose whether or not to publish the source code, they *can't* publish the wrong source code. The block explorer compiles the provided source code independently, and if doesn't get the exact same bytecode, it rejects that source code. [You can read more about this on the Etherscan site](https://etherscan.io/verifyContract). |
| 34 | + |
| 35 | + |
| 36 | +## Comparison to legitimate ERC-20 tokens {#compare-legit-erc20} |
| 37 | + |
| 38 | +We are going to compare this token to legitimate ERC-20 tokens. If you are not familiar with how legitimate ERC-20 tokens are typically written, [see this tutorial](/developers/tutorials/erc20-annotated-code/). |
| 39 | + |
| 40 | + |
| 41 | +### Constants for privileged addresses |
| 42 | + |
| 43 | +Contracts sometimes need privileged addresses. Contracts that are designed for long term use allow some privileged address to change those addresses, for example to enable the use of a new multisig contract. There are several ways to do this. |
| 44 | + |
| 45 | +The [`HOP` token contract](https://etherscan.io/address/0xc5102fe9359fd9a28f877a67e36b0f050d81a3cc#code) uses the [`Ownable`](https://docs.openzeppelin.com/contracts/2.x/access-control#ownership-and-ownable) pattern. The privileged address is kept in storage, in a field called `_owner` (see the third file, `Ownable.sol`). |
| 46 | + |
| 47 | +```solidity |
| 48 | +abstract contract Ownable is Context { |
| 49 | + address private _owner; |
| 50 | + . |
| 51 | + . |
| 52 | + . |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +The [`ARB` token contract](https://etherscan.io/address/0xad0c361ef902a7d9851ca7dcc85535da2d3c6fc7#code) does not have a privileged address directly. However, it does not need one. It sits behind a [`Proxy`](https://docs.openzeppelin.com/contracts/4.x/api/proxy) at [address `0xb50721bcf8d664c30412cfbc6cf7a15145234ad1`](https://etherscan.io/address/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1#code). That contract has an privileged address (see the fourth file, `ERC1967Upgrade.sol`) that be used for upgrades. |
| 57 | + |
| 58 | +```solidity |
| 59 | + /** |
| 60 | + * @dev Stores a new address in the EIP1967 admin slot. |
| 61 | + */ |
| 62 | + function _setAdmin(address newAdmin) private { |
| 63 | + require(newAdmin != address(0), "ERC1967: new admin is the zero address"); |
| 64 | + StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; |
| 65 | + } |
| 66 | +``` |
| 67 | + |
| 68 | +In contrast, the `wARB` contract has a hard coded `contract_owner`. |
| 69 | + |
| 70 | +```solidity |
| 71 | +contract WrappedArbitrum is Context, IERC20 { |
| 72 | + . |
| 73 | + . |
| 74 | + . |
| 75 | + address deployer = 0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1; |
| 76 | + address public contract_owner = 0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33; |
| 77 | + . |
| 78 | + . |
| 79 | + . |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +[This contract owner](https://etherscan.io/address/0xb40dE7b1beE84Ff2dc22B70a049A07A13a411A33) is not a contract that could be controlled by different accounts at different times, but an [externally owned account](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs). This means that it is probably designed for short term use by an individual, rather than as a long term solution to control an ERC-20 that will remain valuable. |
| 84 | + |
| 85 | +And indeed, if we look in Etherscan we see that the scammer only used this contract for only 12 hours ([first transaction](https://etherscan.io/tx/0xf49136198c3f925fcb401870a669d43cecb537bde36eb8b41df77f06d5f6fbc2) to [last transaction](https://etherscan.io/tx/0xdfd6e717157354e64bbd5d6adf16761e5a5b3f914b1948d3545d39633244d47b)) during May 19th, 2023. |
| 86 | + |
| 87 | + |
| 88 | +### The `mount` function |
| 89 | + |
| 90 | +While it is not specified in [the standard](https://eips.ethereum.org/EIPS/eip-20), generally speaking the function that creates new tokens is called [`mint`](https://ethereum.org/el/developers/tutorials/erc20-annotated-code/#the-_mint-and-_burn-functions-_mint-and-_burn). |
| 91 | + |
| 92 | +If we look in the `wARB` constructor, we see the time mint function has been renamed to `mount` for some reason, and is called five times with a fifth of the initial supply, instead of once for the entire amount for efficiency. |
| 93 | + |
| 94 | +```solidity |
| 95 | + constructor () public { |
| 96 | +
|
| 97 | + _name = "Wrapped Arbitrum"; |
| 98 | + _symbol = "wARB"; |
| 99 | + _decimals = 18; |
| 100 | + uint256 initialSupply = 1000000000000; |
| 101 | +
|
| 102 | + mount(deployer, initialSupply*(10**18)/5); |
| 103 | + mount(deployer, initialSupply*(10**18)/5); |
| 104 | + mount(deployer, initialSupply*(10**18)/5); |
| 105 | + mount(deployer, initialSupply*(10**18)/5); |
| 106 | + mount(deployer, initialSupply*(10**18)/5); |
| 107 | + } |
| 108 | +``` |
| 109 | + |
| 110 | +The `mount` function itself is also suspicious. |
| 111 | + |
| 112 | +```solidity |
| 113 | + function mount(address account, uint256 amount) public { |
| 114 | + require(msg.sender == contract_owner, "ERC20: mint to the zero address"); |
| 115 | +``` |
| 116 | + |
| 117 | +Looking at the `require`, we see that only the contract owner is allowed to mint. That is legitimate. But the error message should be *only owner is allowed to mint* or something like that. Instead, it is the irrelevant *ERC20: mint to the zero address*. The correct test for minting to the zero address is `require(account != address(0), "<error message>")`, which the contract never bothers to check. |
| 118 | + |
| 119 | +```solidity |
| 120 | + _totalSupply = _totalSupply.add(amount); |
| 121 | + _balances[contract_owner] = _balances[contract_owner].add(amount); |
| 122 | + emit Transfer(address(0), account, amount); |
| 123 | + } |
| 124 | +``` |
| 125 | + |
| 126 | +There are two more suspicious facts, directly related to minting: |
| 127 | + |
| 128 | +- There is an `account` parameter, which is presumably the account that should receive the minted amount. But the balance that increases is actually `contract_owner`. |
| 129 | + |
| 130 | +- While the balance increased belongs to `contract_owner`, the event emitted shows a transfer to `account`. |
| 131 | + |
| 132 | +These code quality issues don't *prove* that this code is a scam, but they make it appear suspicious. Organized companies such as Arbitrum don't usually release code this bad. |
| 133 | + |
| 134 | + |
| 135 | +### The fake `_transfer` function |
| 136 | + |
| 137 | +It is standard to have actual transfers happen using [an internal `_transfer` function](/developers/tutorials/erc20-annotated-code/#the-_transfer-function-_transfer). |
| 138 | + |
| 139 | +In `wARB` this function looks almost legitimate: |
| 140 | + |
| 141 | +```solidity |
| 142 | + function _transfer(address sender, address recipient, uint256 amount) internal virtual{ |
| 143 | + require(sender != address(0), "ERC20: transfer from the zero address"); |
| 144 | + require(recipient != address(0), "ERC20: transfer to the zero address"); |
| 145 | +
|
| 146 | + _beforeTokenTransfer(sender, recipient, amount); |
| 147 | + |
| 148 | + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); |
| 149 | + _balances[recipient] = _balances[recipient].add(amount); |
| 150 | + if (sender == contract_owner){ |
| 151 | + sender = deployer; |
| 152 | + } |
| 153 | + emit Transfer(sender, recipient, amount); |
| 154 | + } |
| 155 | +``` |
| 156 | + |
| 157 | +The one suspicious part is: |
| 158 | + |
| 159 | +```solidity |
| 160 | + if (sender == contract_owner){ |
| 161 | + sender = deployer; |
| 162 | + } |
| 163 | + emit Transfer(sender, recipient, amount); |
| 164 | +``` |
| 165 | + |
| 166 | +If the contract owner sends tokens, why does the `Transfer` event show they come from `deployer`? |
| 167 | + |
| 168 | +However, there is a more important issue. Who calls this `_transfer` function? It can't be called from the outside, it is marked `interval`. And the code we habve doesn't include any calls to `_transfer`. Clearly, it is here as a decoy. |
| 169 | + |
| 170 | +```solidity |
| 171 | + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { |
| 172 | + _f_(_msgSender(), recipient, amount); |
| 173 | + return true; |
| 174 | + } |
| 175 | +
|
| 176 | + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { |
| 177 | + _f_(sender, recipient, amount); |
| 178 | + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); |
| 179 | + return true; |
| 180 | + } |
| 181 | +``` |
| 182 | + |
| 183 | +When we look at the functions that are called to transfer tokens, `transfer` and `transferFrom`, we see that they call a completely different function, `_f_`. |
| 184 | + |
| 185 | + |
| 186 | +### The real `_f_` function |
| 187 | + |
| 188 | + |
| 189 | +```solidity |
| 190 | + function _f_(address sender, address recipient, uint256 amount) internal _mod_(sender,recipient,amount) virtual { |
| 191 | + require(sender != address(0), "ERC20: transfer from the zero address"); |
| 192 | + require(recipient != address(0), "ERC20: transfer to the zero address"); |
| 193 | +
|
| 194 | + _beforeTokenTransfer(sender, recipient, amount); |
| 195 | + |
| 196 | + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); |
| 197 | + _balances[recipient] = _balances[recipient].add(amount); |
| 198 | + if (sender == contract_owner){ |
| 199 | + |
| 200 | + sender = deployer; |
| 201 | + } |
| 202 | + emit Transfer(sender, recipient, amount); |
| 203 | + } |
| 204 | +``` |
| 205 | + |
| 206 | +There are only two potentially red flags in this function. |
| 207 | + |
| 208 | +- The use of the [function modifier](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm) `_mod_`. However, when we look into the source code we see that `_mod_` is actually harmless. |
| 209 | + |
| 210 | + ```solidity |
| 211 | + modifier _mod_(address sender, address recipient, uint256 amount){ |
| 212 | + _; |
| 213 | + } |
| 214 | + ``` |
| 215 | + |
| 216 | +- The same issue we saw in `_transfer`, which is when `contract_owner` sends tokens they appear to come from `deployer`. |
| 217 | + |
| 218 | + |
| 219 | +### The fake events function `dropNewTokens` |
| 220 | +### Why both auth and approver? Why the mod that does nothing? |
| 221 | +### The burning Approve function. |
| 222 | + |
| 223 | +## What can we detect automatically? |
| 224 | +### Weird transfers |
| 225 | +### Events that don't make sense together |
| 226 | + |
| 227 | +## Conclusions |
| 228 | +### Always get the token address from a trusted source |
| 229 | +### Code quality and readability matter |
0 commit comments