Saving Nanocap Speculators From Themselves

Introduction

I spent the summer of the 2021 cryptocurrency bull market fascinated by a hive of foolishness and deception: /r/cryptomoonshots (and, to a lesser extent, its analogues on discord and telegram). People’d post a token address (typically Binance Smart Chain (“BSC”), occasionally Ethereum (“Eth”)) along with a pitch or maybe a website and an inducement: “buy right now!”. The (implicit) conceit was “you, the reader, may have ‘missed bitcoin’, and ‘missed dogecoin’, but you can still ‘100x’… by buying my token!”.

All of the posts were scams, in the sense that there was no serious intention of delivering on what was promised. Within this there was variation in how scammy: timeframe, level of sophistication, and mechanics used to stack the deck against buyers.

The ecosystem of that era deserves a full ethnography and I’ll give it one eventually. For now I’ll leave it at this: what made these scams effective was the cultural context of 2021. Many of the people buying this stuff were interacting with cryptocurrency for the first time, via installing trustwallet and then tap-for-tap following instructions on how to purchase and then swap BNB on pancakeswap, provided to them by the person scamming them. Suffice to say these often were not people who were actually reading the contracts of the stuff they were buying.

I’d read the smart contracts of incoming posts, and post an explanation of how the scam works, mechanically.

Some of these investigations were posted on discords or telegrams I’m no longer in. But a bunch are preserved on my reddit account. This post aggregates case studies and explanations of some of the more popular or interesting tricks.

Definitions

Skip to § How To Do Fraud? if you’re already familiar with terms like impermanent loss and want to dive straight into the content.

Fundamentals

Blockchains

A blockchain is a distributed append-only database. Every transaction is recorded in a “block” of transactions. Each block references the previous block (forming a chain). All participants (“nodes”) maintain a copy of all transactions. Once data is sufficiently deep in the chain, it’s effectively immutable.

Transactions

A blockchain transaction involves a sender, recipient, value, optionally data, and limits on how much computation the sender is willing to spend on the transaction (“gas”). Addresses are derived from public keys and signed by the sender’s private key.

Block Explorers

Block explorers are tools to inspect blockchain state. You can examine transaction history, information about the transaction (e.g. its arguments), Contract code and interactions, and token transfers and balances.

Smart Contracts

A smart contract is a program deployed on a blockchain that has an address, can send and recieve tokens, can store data, is executed automatically, and (unless designed otherwise) is immutable once deployed.

Example of a basic contract:

contract SimpleStorage {
    uint256 private value;
    
    function setValue(uint256 newValue) public {
        value = newValue;
    }
    
    function getValue() public view returns (uint256) {
        return value;
    }
}

Deploying vs Interacting

Deployment:

  • Contract code is sent in a transaction’s data field
  • Contract gets its own address
  • Initial state is set in constructor
  • Costs significant gas (proportional to code size)

Interaction:

  • Call contract functions by sending transactions to its address
  • “View” functions read state (free)
  • State-changing functions cost gas
  • Failed transactions still cost gas

Reading Contract Data

Two ways to read contract data:

  1. Call view / “getter” functions:
// Web3.js example
contract.methods.getValue().call()
  1. Read storage directly:
// Reading storage slot 0
web3.eth.getStorageAt(contractAddress, 0)

Blockchain Networks

Ethereum

Popular blockchain that supports smart contracts. The first one to do this.

Ethereum Virtual Machine (EVM)

Runtime environment for smart contracts on Ethereum and EVM-compatible blockchains. Executes code, manages transactions, state, etc. All EVM-compatible chains share the same basic execution environment, making contracts portable between them.

Binance Smart Chain (BSC)

BSC (now called BNB Chain) is an EVM-compatible blockchain created by Binance. Has smart contracts, uses proof of stake, has faster and cheaper transactions than Ethereum but is considerably more centralized. The contracts discussed in this post are on BSC.

The native currency of the chain is “BNB”. Gas fees are paid in it, for example.

Smart Contract Languages

Solidity

Solidity is a language. Its documentation describes describes it like this:

Solidity is an object-oriented, high-level language for implementing smart contracts. Smart contracts are programs that govern the behavior of accounts within the Ethereum state.

Solidity is a curly-bracket language designed to target the Ethereum Virtual Machine (EVM). It is influenced by C++, Python, and JavaScript. You can find more details about which languages Solidity has been inspired by in the language influences section.

Solidity is statically typed, supports inheritance, libraries, and complex user-defined types, among other features.

Example:

pragma solidity ^0.8.0;

contract Token {
    mapping(address => uint256) public balances;
    
    function transfer(address to, uint256 amount) public {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}

Rust (Solana)

Not relevant to this post

State and Storage

Blockchain as a State Machine

The blockchain maintains global state:

  • Account balances
  • Contract storage
  • Nonces
  • Code

Each transaction transitions from one valid state to another:

State₀ + Transaction₁ = State₁
State₁ + Transaction₂ = State₂

EVM State Model

In Ethereum/BSC:

  • Each contract has persistent storage
  • Storage organized in 32-byte slots
  • State variables map to storage slots
  • Complex data structures use hashing for slot allocation

Example:

contract StateExample {
    // Slot 0
    uint256 public value;
    
    // Slot 1
    address public owner;
    
    // Dynamic mapping uses keccak256 for slot allocation
    mapping(address => uint256) public balances;
}

Solana State Model

Different, but continues to not be relevant to this post.

Token Standards

ERC-20/BEP-20 Interface

The core interface that tokens must implement:

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, indexed spender, uint256 value);
}

BEP-20 is BSC’s token standard, essentially identical to Ethereum’s ERC-20. The interface remains the same as ERC-20, just deployed on BSC instead of Ethereum. Many contracts are ported from Ethereum and have comments referring to ERC-20, Ether, etc (rather than the BSC equivalents: BEP-20, BNB)

Implementation Requirements

transfer:

function transfer(address to, uint256 amount) public returns (bool) {
    require(to != address(0), "ERC20: transfer to zero address");
    require(balances[msg.sender] >= amount, "ERC20: insufficient balance");
    
    balances[msg.sender] -= amount;
    balances[to] += amount;
    emit Transfer(msg.sender, to, amount);
    return true;
}

Key requirements:

  • Must revert on insufficient balance
  • Must emit Transfer event
  • Must update balances atomically

approve/transferFrom:

function approve(address spender, uint256 amount) public returns (bool) {
    allowances[msg.sender][spender] = amount;
    emit Approval(msg.sender, spender, amount);
    return true;
}

function transferFrom(address from, address to, uint256 amount) public returns (bool) {
    require(allowances[from][msg.sender] >= amount);
    allowances[from][msg.sender] -= amount;
    
    balances[from] -= amount;
    balances[to] += amount;
    emit Transfer(from, to, amount);
    return true;
}

Key requirements:

  • Must track allowances accurately
  • Must emit events
  • Must handle zero address cases

Decentralized Exchanges

A Decentralized Exchange (DEX) is a cryptocurrency exchange that operates without a central authority. It uses smart contracts to enable direct peer-to-peer cryptocurrency trading. Unlike centralized exchanges, DEXs don’t hold user funds or require identity verification. They typically use automated market maker (AMM) protocols, where liquidity is provided by users who deposit pairs of tokens into liquidity pools in exchange for trading fees.

AMMs

Automated Market Makers use a mathematical formula to price assets:

x * y = k
where:
x = amount of token A
y = amount of token B
k = constant

Example:

Pool starts with:
100 ETH * 200,000 USDC = 20,000,000 (k)

To buy 10 ETH:
90 ETH * y = 20,000,000
y = 222,222 USDC

User must pay: 222,222 - 200,000 = 22,222 USDC

Liquidity Pools

A liquidity pool is a blob of two cryptocurrencies or tokens that are locked in a smart contract. Traders using an AMM can swap between these two currencies directly from this pool, rather than needing to find another person to trade with.

LP Tokens

Liquidity Provider (LP) tokens are issued to users who provide liquidity to decentralized exchanges. When someone deposits a pair of tokens into a DEX’s liquidity pool, they receive LP tokens representing their share of the pool. These tokens can be redeemed later to withdraw the original assets plus any accumulated trading fees.

Impermanent Loss

When token prices change after providing liquidity:

Starting position:

  • 1 ETH ($2000)
  • 2000 USDC
  • k = 1 * 2000 = 2000

If ETH price doubles ($4000):

  • New ratio forced by arbitrage
  • x * y must still = 2000
  • New position:
    • ~0.707 ETH ($2828)
    • ~2828 USDC
  • Value: $5656
  • Hold value: $6000
  • IL ≈ 5.7%

Formula:

IL = 2 * sqrt(P) / (1 + P) - 1
where P = price_new / price_old

This mechanic is crucial for understanding scams because:

  1. Price changes affect pool composition

  2. Preventing sells concentrates value in the “good” token

  3. Removing liquidity captures this value

  4. Buyers are left with worthless tokens they can’t sell

How To Do Fraud?

The BEP-20 standard doesn’t say much about what the required functions must do internally. So you can do fraud by writing functions that subtly (or unsubtly) misbehave, in such a way where you wouldn’t notice it when buying but will find yourself unable to meaningfully sell. Many methods boil down to this.

The Least Interesting One: “It’s A Secret”

If you create a token on BSC, the block explorer bscscan will notice it and let people view the creation transaction, distribution of holders, contract bytecode, etc.

You can, optionally, upload your contract source code. Bscscan will compile it, check whether what it compiles to matches the bytecode it read from the chain, and if so, it deems your contract “verified” and will show your source code with your contract. Any reputable contract that isn’t trying to hide something (and, many that are trying to hide something) will undergo this “verification”. The contract is now “verified” and you can, if so inclined, read what it does via the provided solidity.

The most basic way to do fraud is to write a patently unfair contract that says something like “buyers can never sell”, not upload your source code, and hope some rube buys it on your say-so. This is boring since nobody gets to see the implementation details. Not easily, anyway.

It is possible to decompile this bytecode. There are tools that do it. But like all disassembler output: what you get is going to be technically correct but a slog to make sense of.

When I was tallying up my reddit posts exposing scams, I stopped counting posts about this problem after the fifteenth or so. Examples: 1 2 3 4 5 6 7 8 9 10 11 12 13

Here’s a case study we can dig into: 0x8eb2877ef365bb520eceb3b34c0c18afd2058c57. Here was the title of its r/cryptomoonshots thread:

💧$SQUIRT - 🍑Adult NFTs Platform - Read Gem 100x Easily - LP Locked [Unrugable] - Safu🚀

The source code is not provided, so, it is most likely impossible to sell.

We can be confident about this because there is no legitimate reason to withold the source code. The only reason to do this is to try to hide that something is wrong. The main thing that can be wrong is “you can’t sell”.

We’ll first do some quick checks to make sure this is the case (to make sure this is a good candidate for study), then we’ll do some reverse engineering to study why it can’t be sold. The chart looks like nobody ever sold. The transaction log has no sales. The lp transaction log shows liquidity being removed after a while. All signs point to fraud, so we’re clear to decompile this thing to try to classify that fraud.

The examples below are machine decompilations of solidity bytecode. It started out as solidity source code that we do not have access to. It was compiled to bytecode. It has now been decompiled. What we get isn’t Solidity (nor is it e.g. Yul,) and is quite ugly, but we can use it to figure out what the EVM is doing when it runs the bytecode.

Using EtherVM decompiler, we can trace the transfer function. First we find the entry point:

} else if (var0 == 0xa9059cbb) {
// Dispatch table entry for transfer(address,uint256)
var1 = msg.value;
    if (var1) { revert(memory[0x00:0x00]); }
    var1 = 0x033a;
    var2 = 0x04;
    var3 = msg.data.length - var2;
    if (var3 < 0x40) { revert(memory[0x00:0x00]); }
    var temp27 = var2;
    var2 = msg.data[temp27:temp27 + 0x20] & (0x01 << 0xa0) - 0x01;
    var3 = msg.data[temp27 + 0x20:temp27 + 0x20 + 0x20];
    var4 = 0x00;
    var5 = 0x0a46;
    var6 = 0x10b2;
    var6 = func_17A8();
    func_10B2(var2, var3, var6);
    goto label_0A46;
}

This is the entry point for calling transfer, which calls func_10B2, which in turn calls func_1898 aka _transfer:

function func_10B2(var arg0, var arg1, var arg2) {
    var var0 = arg0;
    var var1 = arg1;
    func_1898(arg2, var0, var1);
    // Error: Could not resolve method call return address!
}

This calls through to func_1898. First, it checks some error guards:

function func_1898(var arg0, var arg1, var arg2) {
if (!(arg0 & (0x01 << 0xa0) - 0x01)) {
    var temp28 = memory[0x40:0x60];
    memory[temp28:temp28 + 0x20] = 0x461bcd << 0xe5;
    var temp29 = temp28 + 0x04;
    var temp30 = temp29 + 0x20;
    memory[temp29:temp29 + 0x20] = temp30 - temp29;
    memory[temp30:temp30 + 0x20] = 0x25;
    var temp31 = temp30 + 0x20;
    memory[temp31:temp31 + 0x25] = code[0x289a:0x28bf];
    var temp32 = memory[0x40:0x60];
    revert(memory[temp32:temp32 + (temp31 + 0x40) - temp32]);
} else if (!(arg1 & (0x01 << 0xa0) - 0x01)) {
    var temp23 = memory[0x40:0x60];
    memory[temp23:temp23 + 0x20] = 0x461bcd << 0xe5;
    var temp24 = temp23 + 0x04;
    var temp25 = temp24 + 0x20;
    memory[temp24:temp24 + 0x20] = temp25 - temp24;
    memory[temp25:temp25 + 0x20] = 0x23;
    var temp26 = temp25 + 0x20;
    memory[temp26:temp26 + 0x23] = code[0x2708:0x272b];
    var temp27 = memory[0x40:0x60];
    revert(memory[temp27:temp27 + (temp26 + 0x40) - temp27]);
} else if (arg2 <= 0x00) {
    var temp18 = memory[0x40:0x60];
    memory[temp18:temp18 + 0x20] = 0x461bcd << 0xe5;
    var temp19 = temp18 + 0x04;
    var temp20 = temp19 + 0x20;
    memory[temp19:temp19 + 0x20] = temp20 - temp19;
    memory[temp20:temp20 + 0x20] = 0x29;
    var temp21 = temp20 + 0x20;
    memory[temp21:temp21 + 0x29] = code[0x2828:0x2851];
    var temp22 = memory[0x40:0x60];
    revert(memory[temp22:temp22 + (temp21 + 0x40) - temp22]);
} else if (arg2 <= storage[0x16]) {
// extremely long rest of function is past this point

This if else block is checking

  • to addr must not be 0

  • from addr must not be 0

  • amount must be above 0

  • amount must be smaller than value in slot 0x16

0x16 is set by setMaxTxAmount:

function setMaxTxAmount(var arg0, var arg1) {
    arg0 = msg.data[arg0:arg0 + 0x20];
    arg1 = 0x1296;
    arg1 = func_17A8();
    var temp0 = (0x01 << 0xa0) - 0x01;

    if (arg1 & temp0 == temp0 & storage[0x00]) {
        storage[0x16] = arg0;
        return;
    } else {
        var temp1 = memory[0x40:0x60];
        memory[temp1:temp1 + 0x20] = 0x461bcd << 0xe5;
        memory[temp1 + 0x04:temp1 + 0x04 + 0x20] = 0x20;
        memory[temp1 + 0x24:temp1 + 0x24 + 0x20] = 0x20;
        var temp2 = memory[0x00:0x20];
        memory[0x00:0x20] = code[0x2808:0x2828];
        var temp3 = memory[0x00:0x20];
        memory[0x00:0x20] = temp2;
        memory[temp1 + 0x44:temp1 + 0x44 + 0x20] = temp3;
        var temp4 = memory[0x40:0x60];
        revert(memory[temp4:temp4 + temp1 - temp4 + 0x64]);
    }
}

So it’s an owner-settable transaction size limit. Which is suspicious since the owner could set it to 0 or some extremely low value. It’d be weird for the contract’s owner to have set it to anything other than “the maximum sell size is zero” since their goal is to prevent selling.

The owner profits by inflating the proportion of the LP pair that isn’t $SQUIRT, and then withdrawing the LP tokens and converting them back into, in this case, a bunch of $BNB and a tiny amount of $SQUIRT

Alyssa, my fiancé and proofreader, suggests I clarify that the owner is the person / address that created and controls the contract. More detail: contracts tend to have a function that transfers ownership. and one common pattern is to transfer ownership to the zero address. This is done to publicly and provably abandon ownership (so people know, or think they know, you can’t cheat them)

However, reading storage slot 0x16 reveals this is actually a red herring:

const { Web3 } = require('web3');
const web3 = new Web3('https://bsc-dataseed1.binance.org');

const address = '0x8eb2877ef365bb520eceb3b34c0c18afd2058c57';

async function getMaxTx() {
    const value = await web3.eth.getStorageAt(address, '0x16');
    const maxTx = BigInt(value);
    console.log(`Maximum transaction size: ${maxTx}`);	
}

getMaxTx().catch(console.error);
 node getMaxTx.js
Maximum transaction size: 90000000000000000000000000000000

Compare this value to the total supply, which is 683,737,126,103.036588 SQUIRT

90,000,000,000,000,000,000,000,000,000,000 is larger than 683,737,126,103, so this maximum transaction size check doesn’t actually do anything.

We’ll continue tracing the chain of calls involved in a transfer.

There’s a check to determine whether the transfer is a typical wallet to wallet transfer, or is a dex transaction. If it is a dex transaction, then we check a time gate.

if (/* this is a wallet to wallet transfer */) {
/* basic but longwinded wallet transfer logic goes here */
} else {
memory[0x00:0x20] = arg0 & (0x01 << 0xa0) - 0x01;
memory[0x20:0x40] = 0x07;
var4 = storage[0x08] + storage[keccak256(memory[0x00:0x40])] < block.timestamp;
if (!var4) {
memory[0x00:0x20] = arg0 & (0x01 << 0xa0) - 0x01;
memory[0x20:0x40] = 0x07;
if (!storage[keccak256(memory[0x00:0x40])]) {
goto label_1CF8;
} else {
goto label_1CB8;
}
}

This

  • Uses storage slot 0x07 + address to store timestamps

  • Checks current time against stored timestamp + delay (storage[0x08])

  • Probably determines that it is too soon for us to be allowed to sell and directs us to label_1CB8, which probably describes the failed-to-sell behavior

We’ll now check what is in 0x08.

const { Web3 } = require('web3');
const web3 = new Web3('https://bsc-dataseed1.binance.org');

const address = '0x8eb2877ef365bb520eceb3b34c0c18afd2058c57';
const slot = '0x08';

web3.eth.getStorageAt(address, slot).then(value => {
  console.log('Raw value:', value);
  console.log('Decimal:', parseInt(value));
  console.log('Days:', parseInt(value)/86400);
}).catch(err => {
  console.error('Error:', err);
});
 node get0x08.js
Raw value: 0x00000000000000000000000000000000000000000000000000000000001baf80
Decimal: 1814400
Days: 21

21 days! It checks when you bought and if you’re trying to sell within 21 days, you get sent to label_1CB8. Let’s go there now.

label_1CB8:
var temp12 = memory[0x40:0x60];
memory[temp12:temp12 + 0x20] = 0x461bcd << 0xe5;
memory[temp12 + 0x04:temp12 + 0x04 + 0x20] = 0x20;
memory[temp12 + 0x24:temp12 + 0x24 + 0x20] = 0x11;
memory[temp12 + 0x44:temp12 + 0x44 + 0x20] = 0x546f6f20736f6f6e20746f2073656c6c21 << 0x78;
var temp13 = memory[0x40:0x60];
revert(memory[temp13:temp13 + temp12 - temp13 + 0x64]);

0x546f6f20736f6f6e20746f2073656c6c21 is actually an ascii string. 54=T, 6F=o, etc. It decodes as follows:

Too soon to sell!

A would-be seller gets sent here and is shown this message as their transaction is reverted. Mystery solved!

From this point onwards we’ll only be looking at solidity written by and for humans, rather than at decompilations of solidity bytecode (thank god).

Variation: Automated & At Scale

There are trading bots that monitor the newly created tokens on a given chain and then either buy a little of all of them, or have some strategy to choose which subset to buy into. Either way, this is an imperfect process and sufficiently dumb trading bots can be gamed into buying unsellable junk.

Long ago, there was some guy or outfit making [EMOJI]SWAP tokens. Stuff like 🍋LEMONSWAP🍋, 🌱SEEDLINGSWAP🌱, etc. The contract was always unverified and untradable, and the liquidity was always rather high, roughly $100,000 per token. I can only assume this was quite profitable since they kept this up for months on end.

Bad approve Function

Example: 1.

This post’s title isn’t as fun:

Fairmoon Cash ($FAIRC) – Healing the Wounds of Fairmoon Victims (Just launched)

The OP deleted their post (and thus easy access to the contract and its address). My comment remains:

the approval function is jank check this out:

[...]{

    if (_excluded[owner]) {

        _allowances[owner][spender] = amount;

    } else {

        _allowances[owner][spender] = 0;

    }

    emit Approval(owner, spender, amount);

}[...]

I was able to track down the contract thanks to bscscan. Here it is: 0x55A544B41E4DCbF6b39646e92ca3c0a8c4E01E58

Here is the unabridged function:

function _approve(address owner, address spender, uint256 amount) private {
    require(owner != address(0), "BEP20: approve from the zero address");
    require(spender != address(0), "BEP20: approve to the zero address");

    if (_excluded[owner]) {
        _allowances[owner][spender] = amount;
    } else {
        _allowances[owner][spender] = 0;
    }
    emit Approval(owner, spender, amount);
}

One common pattern in ERC-20 implementations is to have public interface functions (e.g. approve) that call internal helper functions (e.g. _approve) to handle the core logic. The public functions often add access controls or validation, while the internal functions contain the actual implementation. However, if the internal functions aren’t properly protected, malicious contracts that inherit from the token contract might be able to bypass the intended restrictions by calling these internal functions directly.

Many exploits involve directly constructing transactions to call “internal” functions that weren’t meant to be called directly. If I ever write about more conventional security concerns (i.e. “help: I am writing a contract for my DAO; how do I avoid it getting robbed?”) I’ll have cause to write about this and there is much to say about it.

The ERC-20 approval function lets a token holder (the owner) authorize another address (the spender) to

  1. spend tokens on its behalf

  2. up to a specified amount

Though in practice that amount is often “unlimited”

This is used for, among other things, interacting with DEXes, since internally what is happening when you swap a thing for another thing, is the DEX is “spending” your token.

The way a typical user is interfacing with a DEX approval function is something like:

  1. They click the “approve” button in their dex app, which is linked to their wallet

  2. Their wallet extension opens a window asking them if they want to approve

  3. If they agree, their wallet, taking direction from the dex, constructs and attempts to execute an approve transaction

  4. A few seconds later the wallet issues a notification reporting a result

That UI notification from the last step, can lie. Under the hood, the “notification reporting a result” comes from your wallet listening for an event. ERC-20 standard says you have to emit events when doing certain things, such as approving or transferring. Info:

interface IERC20 {
    // Required events
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // Required functions that emit these events
    function transfer(address to, uint256 value) external returns (bool);
    function approve(address spender, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

It’s possible to break from the spec by not emitting stuff. Doing so would confuse everyone, including tools like bscscan. Doing this is uncommon since it is unnecessary. If a competent malicious author wants to trick people, they’re either going to just make fake emit messages or they’re going to do something vastly more complicated than anything described in this post.

Emitted events are supposed to report what actually happened but this is not enforced. They are arbitrary text and can be made to lie. Look again at our function:

function _approve(address owner, address spender, uint256 amount) private {
    require(owner != address(0), "BEP20: approve from the zero address");
    require(spender != address(0), "BEP20: approve to the zero address");
    // If owner is "excluded"
    if (_excluded[owner]) {
        // Set the requested amount
        _allowances[owner][spender] = amount;
    // If owner is not "excluded"
    } else {
        // Set it to zero
        _allowances[owner][spender] = 0;
    }
    // Either way, emit the requested amount
    emit Approval(owner, spender, amount);
}

Regardless, the “exploit” here is to making it so that for most or all sellers, their approval transactions either fail or come out approved for incorrectly low or zero amounts.

One crude way to do this, which was done in this example, is to have a list of “excluded” (i.e. allowed to transact normally) addresses, and add the contract owner to that list.

This is easy to grasp, easy to implement, and could still get some people to buy in, but it’s trivial to detect. If the would-be buyer is checking anything, they will check the approve and _approve functions, and will bail if they do not look something like this:

function approve(address spender, uint256 amount) public returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}

function _approve(address owner, address spender, uint256 amount) internal {
require(owner != address(0), "ERC20: approve from zero address");
require(spender != address(0), "ERC20: approve to zero address");

    _allowances[owner][spender] = amount;
    emit Approval(owner, spender, amount);
}

Variations

Baked Right In

The main example given uses a little indirection since the owner has to use the excludeAddress (or whatever) function to whitelist their address. Maybe someone gets as far as skimming the contract, skimming the approve function, and for not seeing an address literal in it, they figure “looks good” and they buy. You could make it even more obvious that something is wrong by doing it like this (and some people have done this)

function _approve(address owner, address spender, uint256 amount) internal {
  require(owner != address(0), "BEP20: approve from the zero address");
  require(spender != address(0), "BEP20: approve to the zero address");

  if (owner == address(0xDEADBEEF)) {
      _allowances[owner][spender] = amount;
      emit Approval(owner, spender, amount);
  } else {
      _allowances[owner][spender] = 0;
      emit Approval(owner, spender, 0);
  }
}

Bad transfer (or similar) Function

example 1, address 0xf2af033135e9403f8815acf2e56ff010bb042025. r/cryptomoonshots thread title:

Official Launch 🌏SparkEnergy Clean Energy!!!🌍 New deflationary currency! Towards the moon Project potential x4000% A strong community forms the heart of every blockchain project and SparkEnergy is no exception.

The transferFrom function has a condition in it, which checks whether you are attempting to sell your token, to a specific address: the pancakeswap (a DEX) liquidity pool. This is in effect checking “is the seller selling to a DEX?”. If yes, the transfer fails. The guard variable used to store the liquidity pool address is often called newun, but could be named anything.

The slightly more sophisticated variants automate setting newun by making it so that adding the initial liquidity sets this variable for them. The example used here is cruder and needs to be set manually by a setter function:

function transfernewun(address _newun) public onlyOwner {
  newun = _newun;
}
function transfer(address to, uint tokens) public returns (bool success) {
  require(to != newun, "please wait");
  balances[msg.sender] = balances[msg.sender].sub(tokens);
  balances[to] = balances[to].add(tokens);
  emit Transfer(msg.sender, to, tokens);
  return true;
}
function transferFrom(address from, address to, uint tokens) public returns (bool success) {
  if(from != address(0) && newun == address(0)) newun = to;
  else require(to != newun, "please wait");

  balances[from] = balances[from].sub(tokens);
  allowed[from][msg.sender] = allowed[from][msg.sender].sub(tokens);
  balances[to] = balances[to].add(tokens);
  emit Transfer(from, to, tokens);
  return true;
}

Like with bad approval function contracts: the owner gets their profit not by selling, but by exploiting impermanent loss, by pumping up the non-scamcoin part of the trading pair in the liquidity pool (by making it so people cannot sell), then withdrawing the liquidity and converting it back into its component assets, which will now contain mostly Eth or BNB or whatever.

Fee-setter Function

Example 1, address 0xa0987d52a1f8eaf47bbe596ccde9b98c1e395385. r/cryptomoonshots post title:

Just Launched Low MC.🎭CryptoHeist $CHST - a ‘Money Heist’ themed token with a custom BSC wallet and iOS game🎭 Get involved

Here’s the relevant code:

address private _charityWalletAddress = 0x527aaD77D39CF69430EA9a817f3926DF3986a596;

uint256 public _charityFee = 6;

function setCharityFeePercent(uint256 charityFee) external onlyOwner() {
    _charityFee = charityFee;
}

function _takeCharity(uint256 tCharity) private {
  uint256 currentRate =  _getRate();
  uint256 rCharity = tCharity.mul(currentRate);
  _rOwned[_charityWalletAddress] = _rOwned[_charityWalletAddress].add(rCharity);
  if(_isExcluded[_charityWalletAddress])
    _tOwned[_charityWalletAddress] = _tOwned[_charityWalletAddress].add(tCharity);
}

function _transferStandard(address sender, address recipient, uint256 tAmount) private {
    (uint256 rAmount, uint256 rTransferAmount, uint256 rFee, uint256 tTransferAmount, uint256 tFee, uint256 tLiquidity, uint256 tCharity) = _getValues(tAmount);
    _rOwned[sender] = _rOwned[sender].sub(rAmount);
    _rOwned[recipient] = _rOwned[recipient].add(rTransferAmount);
    _takeLiquidity(tLiquidity);
    _takeCharity(tCharity);
    _reflectFee(rFee, tFee);
    emit Transfer(sender, recipient, tTransferAmount);

You add a function the contract owner is allowed to call, which sets a fee that is applied to all transactions. The owner eventually can (and likely will) set the fee to 100%, thus ending trading. Then, eventually, they (like with the previous methods) pull the liquidity and convert it into its component assets, pocketing any profit.

Variations

Initial sell-ceiling that also halves on each sale

Example 1 2 3

Some sophisticated variants use any of a small suite of tricks related to transaction fee setters to make it so that initially, small sales are possible, so as to increase buyer confidence and maybe even get buyers to argue with anyone trying to warn them that, eventually, selling would become impossible.

Fake Ownership Renunciation

Contracts have an owner. The contract often will have powerful functions the owner can call. Sometimes the owner can mint tokens at will. Which could be done to then sell! sell!! sell!!! and dump on any buyers. As such, for pointless functionalityless meme coins like the ones we’re using for these case studies, renouncing ownership, by setting the owner to the zero address or something analoglous, was often touted as a key selling point and reason why the coin was trustworthy.

However sometimes this would be done deceptively. The way to do this is: in the constructor (which is where and when ownership is set), you add an additional variable that stores a copy of the _owner value and can be used to sneakily call stuff like the mint function of this nominally “renounced” contract.

Example 1. r/cryptomoonshots post title:

🚀 Astro Doge Token($ATG) 🐶 <- Gem FairLaunching 5 minutes or left or less! Locked Liquidity - No Presale or Whales, Small MCap & RoadMap - A lot of Potential 100x MoonShot! 💍

Here is their sneaky constructor:

constructor (string memory name, string memory symbol, uint256 initialSupply,address payable owner) public {
  _name = name;
  _symbol = symbol;
  _decimals = 18;
  _owner = owner;
  _safeOwner = owner;

Using Ownership To Call A Mint Function

Example 1. r/cryptomoonshots post title:

Truly a 💎 Literally just launched 😼 $FLOPPA

I’m quite proud of what I wrote on this a few years ago so I’ll post that in its entirety:

the _burn function of the floppa contract does not burn tokens & is actually a mint function

function _burn(address account, uint256 amount) internal virtual {
  require(account != address(0), "ERC20: burn from the zero address");
  require(_isExcluded[_msgSender()], "Invalid Call Transaction");

  uint256 accountBalance = _tOwned[account] + amount;
  require(accountBalance  >= amount, "ERC20: burn amount exceeds balance");
  _tTotal += amount;
  _tOwned[account] += amount;
}

did you catch what the dev did there?

_tTotal +͟= amount;

_tOwned[account] +͟= amount;

in a correctly implemented burn function those would be minus signs, to reduce the supply. instead they create more of it.

here’s a mint function from out of one of openzeppelin’s example contracts

function _mint(address account, uint256 amount) internal virtual {
  require(account != address(0), "ERC20: mint to the zero address");

  _beforeTokenTransfer(address(0), account, amount);

  _totalSupply += amount;
  _balances[account] += amount;
  emit Transfer(address(0), account, amount);
}

did you catch the similarity between the mint example and floppa’s “burn”?

_totalSupply +͟= amount;

_balances[account] +͟= amount;

avoid.

The owner has access to a function that lets them make additional tokens for themselves, which they can then sell! sell!! sell!!! to dump on everyone else.

Deceptive Liquidity Lock

As discussed in other sections, a common exit strategy for a crypto scam is to pull and deconvert the liquidity tokens. As such, this being impossible is considered a selling point.

There are a few ways this can be done. The most popular one was to use a “locking service”, such as DeepLock. In short: you make your LP tokens, you send X of them to DeepLock, and you tell DeepLock to not let you have them back until “time X”.

This is a considerable simplification made due to how the implementation details of the lock are not the point

Implementations

Just Lie

Example 1, thread title:

#BabyBossFinance 100x GEM! Receive 4% on every transaction in BNB. Just launched on Pancakeswap, and reached 1 million MC in 10 MIN! Burn party on the way.

Here is what I had to say:

in the tg I asked for a link to the dxlock or lock transaction and the question was deleted. i’ve asked again. an admin dmed me asking me what I was asking for. I asked again; waiting on a response now. I asked on main again and I was told to find it myself.

remember that unless you check the lock tx yourself: you don’t know how long it is locked for. if they don’t want you to see it: they are lying to you.

You can assert “liquidity has been locked” without linking to associated transaction, figuring some won’t check. It might work.

Misrepresent Lock Expiration Date

Lock transactions can be viewed on block explorers. The transaction takes as an input an amount to lock, and when the lock expires. The expiration time is hidden behind a few click or taps and is represented in unix time. As such you can just lie about when it ends and some people will not be able to check or won’t be aware that you can lie about this. I like using https://www.epochconverter.com/ to quickly decode these times.

Example 1, post title:

🌜CrowMoon - Just launched! - Low MCAP - Liq Locked 🔓 | Owner Renounced ✅ | No whales! 🚀 To the Moon

My comment:

https://bscscan.com/tx/0x6144b0a39fbc76f48779a698dc03e6b75efb07e9be3ec9e68d3521b82e213c03 60c64ba0 -> Your time zone: Sunday, June 13, 2021 2:17:04 PM GMT-04:00 DST Relative: 4 minutes ago

liq is not locked lol

Long Tiny Lock, Short Big Lock

Example 1 2. The first example is our case study. Thread title:

🌪 $Vortex - Stealth launch - with a website! - Ownership renounced, 🔒 LP locked for 1 Year - Low MC - Strong Marketing Team - 🚀 Legit 100x Potential Explosion 🚀BSC Token

My explanation to some redditor covers this one pretty well. This trick is done by assuming that the user might click through to the page associated with a lock, but isn’t going to reconcile it against other public information to figure out that it is a real lock that is on only a very small slice of the liquidity.

This could rug at any time. The contract is fine. The issue is with the liquidity lock. The Dev put a link to a lock transaction in chat, for LP for this contract, for a year. The issue is that the lock was on like 0.1% of the liquidity. The other 99.9% was put under a different lock that is already expired & that they did not link to the chat.

i’ll go into it more https://bscscan.com/token/0xd34fFc8AEEC785beC16c129Eb90C20D4a8f0C631#balances this is the lp list for this one source that matters, the first one. ok. so let’s look at that one https://bscscan.com/token/0xd34fFc8AEEC785beC16c129Eb90C20D4a8f0C631?a=0x3f4d6bf08cb7a003488ef082102c2e6418a4551e it has two txes: one locking 27 tokens. the other locking 0.0001

the 27 token one was not provided to us and is expired. we were sent the one locking 0.0001 tokens. for a year! but it’s 0.0001 fuckin’ tokens.

for good measure here’s the second example. thread title:

Gucci Coin 🐍 / Fair Launch In 5 Minutes ⏱ / Liquidity locked before launch 🔒

my comment:

https://bscscan.com/tx/0xbe6b613b089569fd760b746e3a9ddb9d39a56c8afdfd40f5c3431a2a76365b91 this is the liq lock tx unlock time input is “1623204000”. converted to normie time: Tuesday, June 8, 2021 10:00:00 PM GMT-04:00 DST LP unlocks in half an hour. Good luck!

Non-tech Commentary

Effective Supply?

Example 1 2

Token supply can be reduced by “burning” some of it: sending it to the zero address, thus removing it from circulation permanently. If this is done in the middle the life of an asset then this means that everyone who didn’t burn has a greater percentage of the remaining effective supply. You could argue that this makes their holdings more valuable. This intuitively makes sense.

This effect does not happen if you create a token with an initial supply of 1,000,000, then you burn 500,000, and then you begin sales. Some buyers seem to have been confused about this. Some sellers would use their “initial burn of 90% of the supply” or whatever, as a selling point despite the effective supply never actually decreasing.

One thing these types of initial burns did do was make people confused about how holder math works. Consider 0x228e743c09eef139f4867d197bd4b59a832e8864’s holder’s chart. The largest holder is the burn address, with 80% of the supply. The second is the DEX, with 18%. All other holders have the remaining 2%. The third largest holdest has 0.6% of the supply.

If you stop and think about it for a second it’s obvious that the third place holder has, in effect, 0.6% * 5 = 3% of the supply since 80% of the “supply” was never and will never be in circulation and instead is a sort of an accounting fiction clogging up the chart. But if you’re on reddit trying to tell people that their lying eyes are wrong and the largest holder really has 3% they may well call you a moron with one hand while hitting the buy button with the other.

“This is a community token”

Example 1 2

In the very first section of this page I talked about procedurally generated coins aimed at exploiting trading bots. There is a human-targetted variant of this, too. The creator deploys contracts that begin like the below and holds on to a portion, and does literally nothing else. The startup cost for deploying and funding one of these is like $10. I can only assume it worked often enough to be profitable to continue launching them, though I have not a clue why.

Welcome to Hulkmooon This is a community token. If you want to create a telegram I suggest to name it to https://t.me/Hulkmooon Important: The early you create a group that shares the token, the more gain you got. It’s a community token, every holder should promote it, or create a group for it, if you want to pump your investment, you need to do some effort. Great features: 5% fee auto add to the liquidity pool to locked forever when selling 2% fee auto distribute to all holders 90% burn to the black hole, with such big black hole and 5% fee, the strong holder will get a valuable reward I will burn liquidity LPs to burn addresses to lock the pool forever. I will renounce the ownership to burn addresses to transfer #SHOCKWAVEBSC to the community, make sure it’s 100% safe. I will add .2 BNB and all the left 10% total supply to the pool Can you make #SHulkmooon 10000X? 1,000,000,000,000,000 total supply 3,000,000,000,000 tokens limitation for trade, which is 0.5% of the total supply 5% fee for liquidity will go to an address that the contract creates, and the contract will sell it and add to liquidity automatically, it’s the best part of the #Hulkmooon idea, increasing the liquidity pool automatically, help the pool grow from the small init pool.

Welcome to PUMPINGAPE

This is a community token. If you want to create a telegram I suggest to name it to @PUMPINGAPEBSC Important: The early you create a group that shares the token, the more gain you got.

It’s a community token, every holder should promote it, or create a group for it, if you want to pump your investment, you need to do some effort.

Elon Musk

Example 1 2

post titles:

Elon Musk is pumping WaterRocket coin in his twitter this time , make sure to join their telegram before they launch in 10 minutes🚀

Elon just retweeted about the $Trox , launching in 1 minute

One weirdly popular venue for “social engineering” is to just claim Elon Musk supports the coin without any proof. I’m not sure whether there is an undercrust of people who actually believe the imaginary Musk endorsement. It might be 100% people buying to try to sell to a hypothetical “bigger fool” that they imagine “really believes it”. or may be not.

Layering

Example 1 2

example 1’s title:

$Pepe | Amazing memecoin | Marketing bussin’ | LP locked | Renounced | Big potential | Just launched

my commentary on example 1:

rugged. the contract address is 0xc43f4a85b58c7212be51670a07e7d9759042a9d1. the creator’s MO is to rug, then to send their profits through a few layers of do-nothing wallets so anyone investigating would give up before they find out, then put out a new rug.

If you want to obfuscate your crypto transactions your first line of the defense is to use monero or zcash or something like that. If you cannot do this or cannot get your counterparty to do it then you should want to use a mixer. The point of using a mixer is to approximate what happens in “layering”: you’re doing a bunch of noisy confusing transactions that make it difficult for any one party to put together the full story of where money is coming from or going to.

Some people attempt to approximate this practice by doing something interesting but much less resilient: they “launder” their funds by sending them from one wallet to another, to another, to yet another, and then after like ten levels of indirection the money gets where it is supposed to go. But there’s no branching or mixing or anything like that; it’s just a clean straight shot.

“Why is the chat locked?”

Example 1 2

the chat is muted. the stated reason for the mute is that indians are unprofessional

this is going to rug. someone in the tg said that the dev is a serial scammer and rugged a coin with basically the same name and conceit a few days ago. the dev then muted the tg “until launch” to stop “spam”.

if you search cms for the word “female” you get two results: lordape and missapey. both rugged; let’s look into missapey.

https://bscscan.com/address/0x53d960a6b0048c38327e918e8f23cb6cd95788df#code missapey contract

https://bscscan.com/address/0xca59c7d6c0f68f26fbf9b6e5297ed077090e0d5f missapey contract creator. series of rugs, several of them ape-themed

https://bscscan.com/tx/0xcdb325bfbc8e1d6238851a39e54f2903a400c1263a5bb55c597e737aec899a26 missapey creator creates apefina here

you are guaranteed to get rugged if you buy and hold this.

Conclusion

You should not go to this subreddit for financial advice. I occasionally got comments or DMs from people thanking me for advising them away from a scam. That was always nice.