Pool Playground

1. Overview

An interactive educational playground for visualizing and learning Uniswap V2 mechanics by swapping testnet ERC20 tokens.

Learn all about Pool Playground here.

Live on https://pool.eridian.xyz

2. Installation

2.1. Clone repository

git clone https://github.com/EridianAlpha/pool-playground.git

2.2. Install Dependencies

This should happen automatically when first running a command, but the installation can be manually triggered with the following commands:

git submodule init
git submodule update
make install

2.3. Create the .env file

Use the .env.example file as a template to create a .env file.

3. Testing

3.1. Tests

make test
make test-fork

make test-v
make test-v-fork

make test-summary
make test-summary-fork

3.2. Coverage

make coverage
make coverage-fork

make coverage-report
make coverage-report-fork

4. Deployment

4.1. Deploying Uniswap V2 Contracts (optional)

This step is optional and only necessary if you need to deploy the Uniswap V2 core contracts to a new chain. The Uniswap V2 core contracts have already been deployed on Mainnet (used for forks only), Holesky, Sepolia, Base Sepolia, Arbitrum Sepolia, and Optimism Sepolia chains. The addresses of these existing deployments can be found in the .env.example file.

Fork the Uniswap v2-periphery repository and modify the v2-periphery/contracts/libraries/UniswapV2Library.sol contract with the correct init code hash on line 24. Without changing this init code hash, the contract will not be able to calculate the correct pair address. There are a number of reasons why the init code hash changes, but the most likely reason is that the contract was deployed with a different compiler version or with different compiler settings.

You can try to configure your complier to match the settings of the original deployment, but this is not always possible. The best way to get the correct init code hash is to inspect the creation bytecode of the contract that you are using and manually change the init code hash.

# Get the creation bytecode of the UniswapV2Pair contract
CREATION_BYTECODE=$(forge inspect lib/v2-core/contracts/UniswapV2Pair.sol:UniswapV2Pair bytecode)

# Calculate the init code hash of the UniswapV2Pair contract
cast keccak "$CREATION_BYTECODE"
0x0be7e5aaf721ce53efd2148867ffca974ca93687937cd12b66ab2acef87168ed

# Remove the 0x from the start and use that as the "init code hash"
# in `v2-periphery/contracts/libraries/UniswapV2Library.sol` line 24
0be7e5aaf721ce53efd2148867ffca974ca93687937cd12b66ab2acef87168ed

Once you have the correct init code hash updated in your own fork of the Uniswap v2-periphery repository, you can install it to your local environment.

# Example of installing the EridianAlpha/v2-periphery repository with the modified init code hash
forge install EridianAlpha/v2-periphery --no-commit

After installing the modified Uniswap v2-periphery repository, you can deploy the Uniswap V2 Factory and Router02 contracts to the specified chain.

ChainCommand
Anvilmake deploy-uniswapV2-anvil
Holeskymake deploy-uniswapV2-ethereum-holesky
Sepoliamake deploy-uniswapV2-ethereum-sepolia
Base Sepoliamake deploy-uniswapV2-base-sepolia
Arbitrum Sepoliamake deploy-uniswapV2-arbitrum-sepolia
Optimism Sepoliamake deploy-uniswapV2-optimism-sepolia

4.2. Deploying PoolPlayground Contract

Deploys PoolPlayground to the specified chain.

ChainCommand
Anvilmake deploy anvil
Holeskymake deploy ethereum-holesky
Sepoliamake deploy ethereum-sepolia
Base Sepoliamake deploy base-sepolia
Arbitrum Sepoliamake deploy arbitrum-sepolia
Optimism Sepoliamake deploy optimism-sepolia

5. Interactions

Interactions are defined in ./script/Interactions.s.sol

If DEPLOYED_CONTRACT_ADDRESS is set in the .env file, that contract address will be used for interactions. If that variable is not set, the latest deployment on the specified chain will be used.

5.1. Deploy Playground Instance

Call the deploy() function on the PoolPlayground contract. The input parameters are defined in the Interactions.s.sol script.

ChainCommand
Anvilmake deployPlaygroundInstance anvil
Holeskymake deployPlaygroundInstance ethereum-holesky
Sepoliamake deployPlaygroundInstance ethereum-sepolia
Base Sepoliamake deployPlaygroundInstance base-sepolia
Arbitrum Sepoliamake deployPlaygroundInstance arbitrum-sepolia
Optimism Sepoliamake deployPlaygroundInstance optimism-sepolia

6. Bugs and Feature Requests

If you encounter any bugs or have a feature request, please open an issue on GitHub. To help us resolve the issue, please provide the following information:

6.1. Bugs

  • A detailed description of the bug.
  • Steps to reproduce the bug.
  • Expected behavior and actual behavior.
  • Screenshots, if possible, and any additional context or information that may help us resolve the bug.
  • If you have a solution, suggestion, or code change, please submit a pull request.

6.2. Feature Requests

  • For feature requests, questions, or feedback, please open an issue.
  • For security issues or general inquiries, please contact Eridian privately.

7. Authors

8. License

MIT

PoolPlayground

Git Source

Author: EridianAlpha

An interactive educational playground for visualizing and learning Uniswap V2 mechanics by swapping testnet ERC20 tokens.

State Variables

TOKEN_DECIMALS

uint256 public constant TOKEN_DECIMALS = 10 ** 18;

MARKET_PRICE_USD

TokenAmounts public MARKET_PRICE_USD = TokenAmounts({diamond: 100, wood: 20, stone: 2});

s_userTokens

mapping(address => TokenAddresses) internal s_userTokens;

s_userInitialTokenBalances

mapping(address => TokenAmounts) internal s_userInitialTokenBalances;

s_contractAddresses

mapping(string => address) internal s_contractAddresses;

Functions

constructor

Constructor to set the Uniswap contract addresses for the network.

constructor(ContractAddress[] memory _contractAddresses);

Parameters

NameTypeDescription
_contractAddressesContractAddress[]An array of ContractAddress structs.

deploy

Deploy a new playground instance.

Overwrites any existing tokens and pools for the user.

function deploy(TokenAmounts calldata _userTokenAmounts, TokenAmounts[] calldata _poolTokenAmounts) public;

Parameters

NameTypeDescription
_userTokenAmountsTokenAmountsThe amount of tokens to mint for the user.
_poolTokenAmountsTokenAmounts[]The amount of tokens to add to the Uniswap pools.

createTokens

Create tokens.

function createTokens(TokenAmounts memory _mintTokenAmounts) internal returns (TokenAddresses memory tokenAddresses);

Parameters

NameTypeDescription
_mintTokenAmountsTokenAmountsThe total amount of tokens to mint.

createUniswapV2Pools

Create UniswapV2 pools.

function createUniswapV2Pools(TokenAddresses memory _tokenAddresses, TokenAmounts[] memory _poolTokenAmounts)
    internal;

Parameters

NameTypeDescription
_tokenAddressesTokenAddressesThe token addresses for the user.
_poolTokenAmountsTokenAmounts[]The amount of tokens to add to the Uniswap pools.

createPairAndAddLiquidity

Create UniSwapV2 pair and add liquidity.

function createPairAndAddLiquidity(
    IUniswapV2Router02 uniswapV2Router,
    address tokenA,
    address tokenB,
    uint256 amountA,
    uint256 amountB
) internal;

Parameters

NameTypeDescription
uniswapV2RouterIUniswapV2Router02
tokenAaddressThe address of the first token.
tokenBaddressThe address of the second token.
amountAuint256The amount of tokenA to add to the pool.
amountBuint256The amount of tokenB to add to the pool.

sendRemainingTokens

Send remaining tokens to the user.

function sendRemainingTokens(TokenAddresses memory _tokenAddresses) internal;

Parameters

NameTypeDescription
_tokenAddressesTokenAddressesThe token addresses for the user.

getContractAddress

Get the contract address for a given identifier.

function getContractAddress(string memory _identifier) public view returns (address);

Parameters

NameTypeDescription
_identifierstringThe identifier of the contract.

getUserTokens

Get the deployed tokens for a user.

function getUserTokens(address _user) public view returns (TokenAddresses memory);

Parameters

NameTypeDescription
_useraddressThe address of the user.

getUserTokenBalances

Get the all the token balances for a user.

function getUserTokenBalances(address _user) public view returns (TokenAmounts memory userTokenBalances);

Parameters

NameTypeDescription
_useraddressThe address of the user.

Returns

NameTypeDescription
userTokenBalancesTokenAmountsThe token balances for the user.

getUserInitialTokenBalances

Get the initial token balances for a user.

function getUserInitialTokenBalances(address _user)
    public
    view
    returns (TokenAmounts memory userInitialTokenBalances);

Parameters

NameTypeDescription
_useraddressThe address of the user.

Returns

NameTypeDescription
userInitialTokenBalancesTokenAmountsThe initial token balances for the user.

Events

TokensAndPoolsCreated

event TokensAndPoolsCreated(address indexed user);

Structs

ContractAddress

struct ContractAddress {
    string identifier;
    address contractAddress;
}

TokenAddresses

struct TokenAddresses {
    address diamond;
    address wood;
    address stone;
}

TokenAmounts

struct TokenAmounts {
    uint256 diamond;
    uint256 wood;
    uint256 stone;
}

Token

Git Source

Inherits: ERC20, ERC20Permit

Author: EridianAlpha

An ERC20 token contract used in the Pool Playground project.

State Variables

preApprovedAddresses

address[] public preApprovedAddresses;

Functions

constructor

Constructor for the Token contract

constructor(uint256 initialMintAmount, string memory name, string memory symbol, address[] memory _preApprovedAddresses)
    ERC20(name, symbol)
    ERC20Permit(name);

Parameters

NameTypeDescription
initialMintAmountuint256The amount of tokens to mint to the deployer
namestringThe name of the token
symbolstringThe symbol of the token
_preApprovedAddressesaddress[]An array of addresses that are pre-approved to spend an unlimited amount of tokens

isPreApproved

Checks if an address is pre-approved to spend an unlimited amount of tokens

function isPreApproved(address spender) public view returns (bool);

Parameters

NameTypeDescription
spenderaddressThe address to check

Returns

NameTypeDescription
<none>booltrue if the address is pre-approved, false otherwise

allowance

Overrides the allowance function to return type(uint256).max for pre-approved addresses

function allowance(address owner, address spender) public view override returns (uint256);

Parameters

NameTypeDescription
owneraddressThe owner of the tokens
spenderaddressThe spender of the tokens

Returns

NameTypeDescription
<none>uint256The allowance of the spender

approve

Overrides the approve function to return true for pre-approved addresses

function approve(address spender, uint256 amount) public override returns (bool);

Parameters

NameTypeDescription
spenderaddressThe spender of the tokens
amountuint256The amount of tokens to approve

Returns

NameTypeDescription
<none>booltrue if the approval was successful, false otherwise