DEXをつくる@Solidity

pythonからスマートコントラクトの関数を実行する際に、Solidityが読めればコードからその実行についての理解を得ることができる。

そこで、UniswapV2のSolidityコードから最低限必要な部分を読み解き、独自DEXをブロックチェーンにデプロイしてみる。
この作業の目的は、Solidityコードを読めるようになり、DEXの動きを理解することである。

※勉学のために書いたコードなので、実際の運用ではマストな機能を省いたりもしていますので、ご承知おきください。
※逐一説明を入れると膨大になるので、備忘録的内容をコード内に書き込みました。
※自分自身の備忘録的意味合いが強い記事です。


もとにしたSolidityコード群

本記事で書くコードの全体像は過去の記事で理解を深めた。

要は、

  • Factoryコントラクト

  • poolコントラクト

  • Routerコントラクト

を書くことになります。

UniswapV2のコードで上記に対応するものは下記。

Factoryコントラクト
Poolコントラクト
Routerコントラクト

また、これらを動作を補助するのが下記のコード(上記のコードから利用されているコード)。

UniswapV2用ERC20コントラクト
Mathライブラリ
UniswapV2用ライブラリ

完成したコード

Factoryコントラクト(gomifactory.sol)

// SPDX-License-Identifier: MIT

//gomifactory.sol

pragma solidity ^0.8.20;

import './gomipair.sol';

contract GomiFactory {
    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;
    
    function createPair(address tokenA, address tokenB) external returns (address pair){
        //tokenAとtokenBが異なるアドレスかどうかを確認
        require(tokenA != tokenB, 'same token address Ponzing');

        //tokenAとtokenBを数値が小さいからtoken0, token1となるように並び替え
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);

        //token0, token1がそれぞれゼロアドレスでないかを確認
        require(token0 != address(0), 'zero address Ponzing');

        //token0とtoken1のペアのプールが既に存在していないかを確認
        //solidityの場合、代入されていないものの値はゼロが入っている
        require(getPair[token0][token1] == address(0), 'existed pair Ponzing');

        //GomiPairコントラクトをデプロイするのに必要な生成コード、というぐらいしか理解できず。
        bytes memory bytecode = type(GomiPair).creationCode;

        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        //if token0=0x01, token1=0x02, abi.encodePackd(token0, token1) = 0x0102
        //実際はtoken0,token1はaddress型なので20バイトなのでabi.encode(token0, token1)は40バイト
        //何度計算しても同じtoken0とtoken1を入力すれば同じsaltの値を得られる

        //solidityの中でassembly言語を呼び出している
        assembly{
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        //create2を使用した場合、生成されるコントラクトアドレスは以下の計算式で決まる
        //keccak256(0xff ++ sender ++ salt ++ keccak256(bytecode))
        //senderはコントラクトをデプロイするアカウントのアドレス(msg.sender)
        //つまり、bytecodeとsaltが同じならばpairも同じ値になる

        //コントラクトアドレスpairにデプロイされたコントラクトのinitialize関数を実行
        GomiPair(pair).initialize(token0, token1);

        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair;
        allPairs.push(pair);

    }

    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }
}

poolコントラクト(gomipair.sol)

// SPDX-License-Identifier: MIT

//gomipair.sol

pragma solidity ^0.8.20;

import './Math.sol';
import './gomiERC20.sol';

//GomiERC20にはコンストラクタ引数が必要なので指定する
contract GomiPair is GomiERC20("GomiPoolToken", "GPT", 18) {

    uint public constant MINIMUM_LIQUIDITY = 10**3;

    address public factory;
    address public token0;
    address public token1;

    uint112 private reserve0;
    uint112 private reserve1;

    //solidity 0.7.0以降ではconstructorの可視性がデフォルトで内部限定
    //(internal相当)となるので明示しない
    constructor() {
        factory = msg.sender;
    }
    
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'You are Ponzing Scheme');
        token0 = _token0;
        token1 = _token1;
    }

    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1){
        _reserve0 = reserve0;
        _reserve1 = reserve1;
    }

    function mint(address to) external returns (uint liquidity) {
        //現在コントラクトの変数として記録されているトークン保有量を取得
        (uint112 _reserve0, uint112 _reserve1) = getReserves();

        //実際にこのコントラクトが保有しているトークン量をtoken0,token1について
        //それぞれ取得
        uint balance0 = GomiERC20(token0).getbalanceOf(address(this));
        uint balance1 = GomiERC20(token1).getbalanceOf(address(this));

        //先に流動性追加されているはずなので現在このプールが持っている
        //token0,token1からこのプールのトークン保有量変数として保持している
        //reserve0,reserve1を引いた値が預け入れたトークン量
        uint amount0 = balance0 - reserve0;
        uint amount1 = balance1 - reserve1;

        uint _totalSupply = totalSupply;
        if(_totalSupply == 0){
            //当該流動性プールがまだ存在しない場合
            liquidity = Math.sqrt(amount0*amount1) - MINIMUM_LIQUIDITY;
            _mint(address(0), MINIMUM_LIQUIDITY);
        }else{
            //当該流動性プールが既に存在している場合
            liquidity = Math.min(amount0*_totalSupply/_reserve0, amount1*_totalSupply/_reserve1);
        }
        require(liquidity > 0, 'Insufficient liquidity, you are ponzing');

        //算出した流動性トークン量を流動性提供者に送る
        _mint(to, liquidity);

        //reserveの値を最新にする
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
    }

    function burn(address to) external returns (uint amount0, uint amount1){
        //(uint112 _reserve0, uint112 _reserve1) = getReserves();
        address _token0 = token0;
        address _token1 = token1;
        uint balance0 = GomiERC20(_token0).getbalanceOf(address(this));
        uint balance1 = GomiERC20(_token1).getbalanceOf(address(this));

        //liquidity=burnされるliquidityトークンの量
        //通常時、GomiPairコントラクトはliquidityトークンを持っていない
        //(流動性提供者が持っている)
        //burn関数起動時、流動性提供者からburnするliquidityトークンが
        //GomiPairコントラクトに送られてきているのでコントラクトがこれを保有している
        //つまり、GomiPairコントラクトが持っているliquidityトークンをburnすれば良い
        uint liquidity = balanceOf[address(this)];

        uint _totalSupply = totalSupply;
        amount0 = liquidity*balance0/_totalSupply;
        amount1 = liquidity*balance1/_totalSupply;
        require(amount0 > 0 && amount1 > 0, 'Insufficient Liquidity Ponzing');
        _burn(address(this), liquidity);
        bool check0 = GomiERC20(_token0).transfer(to, amount0);
        bool check1 = GomiERC20(_token1).transfer(to, amount1);
        require(check0 && check1, 'TRANSFER_FAILED_PONZING');

        //reserveを更新
        reserve0 = uint112(GomiERC20(_token0).getbalanceOf(address(this)));
        reserve1 = uint112(GomiERC20(_token1).getbalanceOf(address(this)));

    }

    function swap(uint amount0Out, uint amount1Out, address to) external {
        require(amount0Out > 0 || amount1Out > 0, 'Insufficient Output Ponzing');
        (uint112 _reserve0, uint112 _reserve1) = getReserves();
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'Insufficient liquidity Ponzing');

        uint balance0;
        uint balance1;
        {
        address _token0 = token0;
        address _token1 = token1;
        require(to != _token0 && to != _token1, 'Invalid To');

        if(amount0Out > 0){
            bool check0 = GomiERC20(_token0).transfer(to, amount0Out);
            require(check0, 'Transfer token0 failed');
        }
        if(amount1Out > 0){
            bool check1 = GomiERC20(_token1).transfer(to, amount1Out);
            require(check1, 'Transfer token1 failed');
        }
        balance0 = GomiERC20(_token0).getbalanceOf(address(this));
        balance1 = GomiERC20(_token1).getbalanceOf(address(this));
        }

        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'Insufficient Input Amount');

        {
        //手数料0.3%前提
        uint balance0Adjusted = balance0*1000 - amount0In*3;
        uint balance1Adjusted = balance1*1000 - amount1In*3;
        require(balance0Adjusted*balance1Adjusted >= uint(_reserve0)*uint(_reserve1)*(1000**2), 'K Ponzing');
        }

        //reserveを更新
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
    }




}

Routerコントラクト(gomirouter.sol)

// SPDX-License-Identifier: MIT

//gomirouter.sol

pragma solidity ^0.8.20;

import './gomiERC20.sol';
import './gomipair.sol';
import './gomifactory.sol';
import './V2Library.sol';

contract GomiRouter {
    address public immutable factory;

    constructor(address _factory){
        factory = _factory;
    }

    modifier ensure(uint deadline){
        require(deadline >= block.timestamp, 'expired');
        _;
    }

    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin
    ) private returns (uint amountA, uint amountB){
        //getPairについて
        //Solidityではpublicとして定義された状態変数に対して、自動的にゲッター関数が
        //作成される。
        //ゲッター関数は状態変数を読み取るための関数でSolidityコンパイラが自動生成する
        //関数名は状態変数と同じ
        //引数が必要な場合は状態変数の型に応じて適切に設定される
        //書き込み操作はサポートせず読み取り専用
        if(GomiFactory(factory).getPair(tokenA, tokenB) == address(0)){
            GomiFactory(factory).createPair(tokenA, tokenB);
        }
        address pair = GomiFactory(factory).getPair(tokenA, tokenB);
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        (uint reserve0, uint reserve1) = GomiPair(pair).getReserves();
        //ここまでで(token0, reserve0)(token1, reserve1)の対応にすりかえ
        uint reserveA;
        uint reserveB;
        if(token0 == tokenA){
            require(token0==tokenA && token1==tokenB, 'Invalid Token Address');
            reserveA = reserve0;
            reserveB = reserve1;
        }else{
            require(token0==tokenB && token1==tokenA, 'Invalid Token Address');
            reserveA = reserve1;
            reserveB = reserve0;
        }
        //これで(tokenA, reserveA)(tokenB, reserveB)の対応に戻った
        if(reserveA == 0 && reserveB == 0){
            //reserveが0の場合は提供したいamountをそのまま提供
            (amountA, amountB) = (amountADesired, amountBDesired);
        }else{
            //まずはAを希望量提供する前提でBの対応量を計算
            uint amountBOptimal = V2Library.quote(amountADesired, reserveA, reserveB);
            if(amountBOptimal <= amountBDesired){
                //Bの対応量がBの提供希望量よりも小さいことを確認し、
                //さらにBの対応量が最低量よりも大きいことを確認
                require(amountBOptimal >= amountBMin, 'Insufficient B Amount');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            }else{
                //上記で決まらないということはBの提供希望量を基準にAの対応量を決めることになる
                uint amountAOptimal = V2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'Insufficient A Amount');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }

    //参照コードにはvirtualとoverrideが指定されているが省いた
    //virtual:継承先のコントラクトで関数の上書きを可能とする
    //override:継承元のコントラクトにある同じ関数名の関数を上書きすることを示す
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external ensure(deadline) returns(uint amountA, uint amountB, uint liquidity){
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = GomiFactory(factory).getPair(tokenA, tokenB);
        bool checkA = GomiERC20(tokenA).transferFrom(msg.sender, pair, amountA);
        require(checkA, 'transfer failed tokenA');
        bool checkB = GomiERC20(tokenB).transferFrom(msg.sender, pair, amountB);
        require(checkB, 'transfer failed tokenB');
        liquidity = GomiPair(pair).mint(to);
        
    }

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) public ensure(deadline) returns (uint amountA, uint amountB){
        address pair = GomiFactory(factory).getPair(tokenA, tokenB);
        GomiERC20(pair).transferFrom(msg.sender, pair, liquidity);
        (uint amount0, uint amount1) = GomiPair(pair).burn(to);
        (amountA, amountB) = tokenA < tokenB ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, 'Insufficient A Amount');
        require(amountB >= amountBMin, 'Insufficient B Amount');
    }

    function getAmountOut(
        uint amountIn,
        uint reserveIn,
        uint reserveOut
    ) public pure returns (uint amountOut){
        return V2Library.getAmountOut(amountIn, reserveIn, reserveOut);
    }

    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        //address[] calldata path
        //pathは、path[0]=tokenA, path[1]=tokenB, path[2]=tokenCというswap経路
        //を表すのでここでは2トークンの交換を想定してtokenA, tokenBに置き換えた
        address tokenA, //入金するトークン
        address tokenB, //出金するトークン
        address to
        //stack too deepを避けるためにdeadlineによるensureを削除した(実際の稼働よりも学習優先)
        //uint deadline
    ) external returns (uint amountOut){
        address pair = GomiFactory(factory).getPair(tokenA, tokenB);
        (uint reserve0, uint reserve1) = GomiPair(pair).getReserves();
        (uint reserveA, uint reserveB) = tokenA < tokenB ? (reserve0, reserve1) : (reserve1, reserve0);
        amountOut = V2Library.getAmountOut(amountIn, reserveA, reserveB);
        require(amountOut >= amountOutMin, 'Insufficient Amount Out');
        bool check = GomiERC20(tokenA).transferFrom(msg.sender, pair, amountIn);
        require(check, 'TransferFrom Failed');
        if(tokenA < tokenB){
            GomiPair(pair).swap(0, amountOut, to);
        }else{
            GomiPair(pair).swap(amountOut, 0, to);
        }
    }
}

Mathライブラリ(Math.sol)

//Math.sol

pragma solidity ^0.8.20;

// a library for performing various math operations

library Math {
    function min(uint x, uint y) internal pure returns (uint z) {
        z = x < y ? x : y;
    }

    // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
    function sqrt(uint y) internal pure returns (uint z) {
        if (y > 3) {
            z = y;
            uint x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
}

疑似ERC20コントラクト(gomiERC20.sol)

//SPDX-License-Identifier: MIT

//gomiERC20.sol

pragma solidity ^0.8.20;

contract GomiERC20 {
    string private name;
    string private symbol;
    uint8 private decimals;
    uint internal totalSupply;  //継承先のコントラクトから呼び出すので
    mapping(address account => uint256) internal balanceOf;  //継承先のコントラクトから呼び出すので
    //mapping(address account => mapping(address spender => uint)) private allowances;
    address private owner;

    constructor(string memory _name, string memory _symbol, uint8 _decimals){
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        owner = msg.sender;
    }

    //関数名を変数名と同じにするとコンパイラがエラーを吐くのでgetを頭に付けた
    function getname() public view returns(string memory){
        return name;
    }

    function getsymbol() public view returns(string memory){
        return symbol;
    }

    function getdecimals() public view returns(uint8){
        return decimals;
    }

    function gettotalSupply() public view returns(uint256){
        return totalSupply;
    }

    function getbalanceOf(address account) public view returns(uint256){
        return balanceOf[account];
    }

    function getowner() public view returns(address){
        return owner;
    }

    function transfer(address to, uint256 value) public returns(bool){
        address from = msg.sender;
        require(from != address(0), "zero address is unavailable(from)");
        require(to != address(0), "zero address is unavailable(to)");
        require(balanceOf[from] >= value, "insufficient balance");
        balanceOf[to] += value;
        balanceOf[from] -= value;
        return true;
    }
    
    //本来はinternal設定がマストだが、簡便のためにpublicに設定
    function _mint(address to, uint value) public{
        totalSupply = totalSupply + value;
        balanceOf[to] = balanceOf[to] + value;
    }

    function _burn(address from, uint value) internal{
        balanceOf[from] = balanceOf[from] - value;
        totalSupply = totalSupply - value;
    }

    function hello() external pure returns(string memory){
        return "You are Ponzing Scaming Ossan";
    }

    function transferFrom(address from, address to, uint value) external returns(bool){
        require(from != address(0), 'zero address is unavailable(from)');
        require(to != address(0), 'zero address is unavailable(to)');
        //本来はここでmsg.senderがfromのトークンについてallowancesがあるかの確認が必要
        //今回は価値の無いトークンなので許可なしで他人のトークンを送れる
        require(balanceOf[from] >= value, 'insufficient balance');
        balanceOf[from] = balanceOf[from] - value;
        balanceOf[to] = balanceOf[to] + value;
        return true;
    }
}

このコードについては、下記も参考にしています。

疑似V2用ライブラリ(V2Library.sol)

//SPDX-License-Identifier: MIT

//gomiERC20.sol

pragma solidity ^0.8.20;

contract GomiERC20 {
    string private name;
    string private symbol;
    uint8 private decimals;
    uint internal totalSupply;  //継承先のコントラクトから呼び出すので
    mapping(address account => uint256) internal balanceOf;  //継承先のコントラクトから呼び出すので
    //mapping(address account => mapping(address spender => uint)) private allowances;
    address private owner;

    constructor(string memory _name, string memory _symbol, uint8 _decimals){
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        owner = msg.sender;
    }

    //関数名を変数名と同じにするとコンパイラがエラーを吐くのでgetを頭に付けた
    function getname() public view returns(string memory){
        return name;
    }

    function getsymbol() public view returns(string memory){
        return symbol;
    }

    function getdecimals() public view returns(uint8){
        return decimals;
    }

    function gettotalSupply() public view returns(uint256){
        return totalSupply;
    }

    function getbalanceOf(address account) public view returns(uint256){
        return balanceOf[account];
    }

    function getowner() public view returns(address){
        return owner;
    }

    function transfer(address to, uint256 value) public returns(bool){
        address from = msg.sender;
        require(from != address(0), "zero address is unavailable(from)");
        require(to != address(0), "zero address is unavailable(to)");
        require(balanceOf[from] >= value, "insufficient balance");
        balanceOf[to] += value;
        balanceOf[from] -= value;
        return true;
    }
    
    //本来はinternal設定がマストだが、簡便のためにpublicに設定
    function _mint(address to, uint value) public{
        totalSupply = totalSupply + value;
        balanceOf[to] = balanceOf[to] + value;
    }

    function _burn(address from, uint value) internal{
        balanceOf[from] = balanceOf[from] - value;
        totalSupply = totalSupply - value;
    }

    function hello() external pure returns(string memory){
        return "You are Ponzing Scaming Ossan";
    }

    function transferFrom(address from, address to, uint value) external returns(bool){
        require(from != address(0), 'zero address is unavailable(from)');
        require(to != address(0), 'zero address is unavailable(to)');
        //本来はここでmsg.senderがfromのトークンについてallowancesがあるかの確認が必要
        //今回は価値の無いトークンなので許可なしで他人のトークンを送れる
        require(balanceOf[from] >= value, 'insufficient balance');
        balanceOf[from] = balanceOf[from] - value;
        balanceOf[to] = balanceOf[to] + value;
        return true;
    }
}

デプロイ(@Unichainテストネット)

Factoryコントラクトのデプロイ

forge create --broadcast --rpc-url https://sepolia.unichain.org --private-key <my_private_key> src/gomifactory.sol:GomiFactory

デプロイトランザクション

デプロイされたコントラクト

Routerコントラクトのデプロイ

forge create --broadcast --rpc-url https://sepolia.unichain.org --private-key <my_private_key> src/gomirouter.sol:
GomiRouter --constructor-args 0x782DB0e5114A0D1606e4cC2590537baEf3c162a0

--constructor-argsで引数指定しているのはFactoryコントラクトのアドレス

デプロイトランザクション

デプロイされたコントラクト

poolコントラクトは?

poolコントラクトはFactoryコントラクト経由でデプロイされる(Factoryコントラクトがデプロイする)のでここではデプロイしません。

実験(python)

テストトークンT1とT2を作成し、このT1とT2のpoolコントラクトを作成。
T1とT2の流動性提供、T1とT2のスワップを試してみる。

まずは、gomiERC20.solを使ってT1トークンとT2トークンをデプロイする。

T1トークンデプロイ

forge create --broadcast --rpc-url https://sepolia.unichain.org --private-key <my_private_key> src/gomiERC20.sol:GomiERC20 --constructor-args "Test1 Token" "T1" 18

デプロイトランザクション

デプロイされたコントラクト(T1トークンコントラクト)

T2トークンデプロイ

forge create --broadcast --rpc-url https://sepolia.unichain.org --private-key <my_private_key> src/gomiERC20.sol:GomiERC20 --constructor-args "Test2 Token" "T2" 18

デプロイトランザクション

デプロイされたコントラクト(T2トークンコントラクト)

T1トークンとT2トークンをmint(発行)

#tokenABmint.py

from web3 import Web3
from eth_abi import encode
from datetime import datetime
import json
import time

my_wallet_address = 'my_wallet_address'
my_private_key = 'my_private_key'
tokenA = '0xA84068D42Ac42B6Bd3A4aD0eD68F9624AdA91D0D'
tokenB = '0x2B87b2E2b273E46e2115480adaCDcbd23dFC2F9e'

#Web3のセットアップ(Unichain Sepolia)
w3 = Web3(Web3.HTTPProvider("https://sepolia.unichain.org"))

# コントラクトインスタンスの作成
with open('token_ABI.json') as f:
   contractABI = json.load(f)
tokenA_contract = w3.eth.contract(address=tokenA, abi=contractABI)
tokenB_contract = w3.eth.contract(address=tokenB, abi=contractABI)

#トランザクションの作成(mint)
tx1 = tokenA_contract.functions._mint(my_wallet_address, w3.to_wei(1000, 'ether')).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

#トランザクションの表示
#print(tx1)

# トランザクションの署名と送信
signed_tx1 = w3.eth.account.sign_transaction(tx1, private_key=my_private_key)
tx1_hash = w3.eth.send_raw_transaction(signed_tx1.raw_transaction)

print('tokenA mint transaction')
print(w3.to_hex(tx1_hash))
result = w3.eth.wait_for_transaction_receipt(tx1_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

time.sleep(60)

#トランザクションの作成(mint)
tx2 = tokenB_contract.functions._mint(my_wallet_address, w3.to_wei(1000, 'ether')).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

#トランザクションの表示
#print(tx2)

# トランザクションの署名と送信
signed_tx2 = w3.eth.account.sign_transaction(tx2, private_key=my_private_key)
tx2_hash = w3.eth.send_raw_transaction(signed_tx2.raw_transaction)

print('tokenB mint transaction')
print(w3.to_hex(tx2_hash))
result = w3.eth.wait_for_transaction_receipt(tx2_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')


time.sleep(60)

#gettotalSupply()関数の呼び出し
totalSupplyA = tokenA_contract.functions.gettotalSupply().call()
print("totalSupplyA = " + str(totalSupplyA))
totalSupplyB = tokenB_contract.functions.gettotalSupply().call()
print("totalSupplyB = " + str(totalSupplyB))

#getbalanceOf()関数の呼び出し
balanceA = tokenA_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceA = " + str(balanceA))
balanceB = tokenB_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceB = " + str(balanceB))

何度か実行してT1トークンとT2トークンの保有量を増やす。

流動性提供:addLiquidity(T1とT2)してみる

T1とT2のpoolを作成します。
既にこのpoolが存在すれば流動性が追加されます。

T1:T2 = 2:1の割合でpool作成しています。

#addLiquidity.py

from web3 import Web3
from eth_abi import encode
from datetime import datetime
import json
import time

my_wallet_address = 'my_wallet_address'
my_private_key = 'my_private_key'
router = '0xc624418F6e21d18B4AEC0d79B0e77F260826d27b'
tokenA = '0xA84068D42Ac42B6Bd3A4aD0eD68F9624AdA91D0D'
tokenB = '0x2B87b2E2b273E46e2115480adaCDcbd23dFC2F9e'

#Web3のセットアップ(Unichain Sepolia)
w3 = Web3(Web3.HTTPProvider("https://sepolia.unichain.org"))

# コントラクトインスタンスの作成
with open('router_ABI.json') as f:
   routerABI = json.load(f)
router_contract = w3.eth.contract(address=router, abi=routerABI)

#factoryコントラクトのアドレス確認
factory_address = router_contract.functions.factory().call()
print('factory conatract address: ' + str(factory_address))

now = int(datetime.now().timestamp())
deadline = now + 200
#トランザクションの作成(addLiquidity 2000/1000)
tx1 = router_contract.functions.addLiquidity(tokenA, tokenB, w3.to_wei(2000, 'ether'), w3.to_wei(1000, 'ether'), 0, 0, my_wallet_address, deadline).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

#トランザクションの表示
#print(tx1)

# トランザクションの署名と送信
signed_tx1 = w3.eth.account.sign_transaction(tx1, private_key=my_private_key)
tx1_hash = w3.eth.send_raw_transaction(signed_tx1.raw_transaction)

print('addLiquidity 2000/1000 transaction')
print(w3.to_hex(tx1_hash))
result = w3.eth.wait_for_transaction_receipt(tx1_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')


time.sleep(60)


#factory contractからpool contractを求める
with open('factory_ABI.json') as f:
   factoryABI = json.load(f)
factory_contract = w3.eth.contract(address=factory_address, abi=factoryABI)

pair_address = factory_contract.functions.getPair(tokenA, tokenB).call()
print("pool address: " + str(pair_address))

#pool contractからreserveを求める
with open('pool_ABI.json') as f:
   poolABI = json.load(f)
pool_contract = w3.eth.contract(address=pair_address, abi=poolABI)

(reserve0, reserve1) = pool_contract.functions.getReserves().call()
print("reserve0: " + str(reserve0))
print("reserve1: " + str(reserve1))
if(tokenA < tokenB):
    reserveA = reserve0
    reserveB = reserve1
else:
    reserveA = reserve1
    reserveB = reserve0
print("reserveA: " + str(reserveA))
print("reserveB: " + str(reserveB))

#追加のaddLiquidity
now = int(datetime.now().timestamp())
deadline = now + 200
#トランザクションの作成(addLiquidity 200/110)
tx2 = router_contract.functions.addLiquidity(tokenA, tokenB, w3.to_wei(200, 'ether'), w3.to_wei(110, 'ether'), 0, 0, my_wallet_address, deadline).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

signed_tx2 = w3.eth.account.sign_transaction(tx2, private_key=my_private_key)
tx2_hash = w3.eth.send_raw_transaction(signed_tx2.raw_transaction)

print('addLiquidity 200/110 transaction')
print(w3.to_hex(tx2_hash))
result = w3.eth.wait_for_transaction_receipt(tx2_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

time.sleep(60)

#再度pool内のreserveを確認
(reserve0, reserve1) = pool_contract.functions.getReserves().call()
print("reserve0: " + str(reserve0))
print("reserve1: " + str(reserve1))
if(tokenA < tokenB):
    reserveA = reserve0
    reserveB = reserve1
else:
    reserveA = reserve1
    reserveB = reserve0
print("reserveA: " + str(reserveA))
print("reserveB: " + str(reserveB))

実行したログが残っていませんでした・・・

スワップしてみる(T1→T2、T2→T1)

#swap.py

from web3 import Web3
from eth_abi import encode
from datetime import datetime
import json
import time

my_wallet_address = 'my_wallet_address'
my_private_key = 'my_private_key'
router = '0xc624418F6e21d18B4AEC0d79B0e77F260826d27b'
tokenA = '0xA84068D42Ac42B6Bd3A4aD0eD68F9624AdA91D0D'
tokenB = '0x2B87b2E2b273E46e2115480adaCDcbd23dFC2F9e'

#Web3のセットアップ(Unichain Sepolia)
w3 = Web3(Web3.HTTPProvider("https://sepolia.unichain.org"))

# コントラクトインスタンスの作成
with open('token_ABI.json') as f:
   contractABI = json.load(f)
tokenA_contract = w3.eth.contract(address=tokenA, abi=contractABI)
tokenB_contract = w3.eth.contract(address=tokenB, abi=contractABI)


# コントラクトインスタンスの作成
with open('router_ABI.json') as f:
   routerABI = json.load(f)
router_contract = w3.eth.contract(address=router, abi=routerABI)

#factoryコントラクトのアドレス確認
factory_address = router_contract.functions.factory().call()
print('factory conatract address: ' + str(factory_address))

#factory contractからpool contractを求める
with open('factory_ABI.json') as f:
   factoryABI = json.load(f)
factory_contract = w3.eth.contract(address=factory_address, abi=factoryABI)

pair_address = factory_contract.functions.getPair(tokenA, tokenB).call()
print("pool address: " + str(pair_address))

#pool contractからreserveを求める
with open('pool_ABI.json') as f:
   poolABI = json.load(f)
pool_contract = w3.eth.contract(address=pair_address, abi=poolABI)

#getbalanceOf()関数の呼び出し
balanceA = tokenA_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceA = " + str(balanceA))
balanceB = tokenB_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceB = " + str(balanceB))

(reserve0, reserve1) = pool_contract.functions.getReserves().call()
#print("reserve0: " + str(reserve0))
#print("reserve1: " + str(reserve1))
if(tokenA < tokenB):
    reserveA = reserve0
    reserveB = reserve1
else:
    reserveA = reserve1
    reserveB = reserve0
print("reserveA: " + str(reserveA))
print("reserveB: " + str(reserveB))

amountBOut_ = router_contract.functions.getAmountOut(w3.to_wei(200, 'ether'), reserveA, reserveB).call()
amountBOut = amountBOut_/(10**18)
print("200 $A → " + str(amountBOut) + " $B")
amountAOut_ = router_contract.functions.getAmountOut(w3.to_wei(100, 'ether'), reserveB, reserveA).call()
amountAOut = amountAOut_/(10**18)
print("100 $B → " + str(amountAOut) + "$A")

amountBOut_ = router_contract.functions.getAmountOut(w3.to_wei(20, 'ether'), reserveA, reserveB).call()
amountBOut = amountBOut_/(10**18)
print("20 $A → " + str(amountBOut) + " $B")
amountAOut_ = router_contract.functions.getAmountOut(w3.to_wei(10, 'ether'), reserveB, reserveA).call()
amountAOut = amountAOut_/(10**18)
print("10 $B → " + str(amountAOut) + "$A")

amountBOut_ = router_contract.functions.getAmountOut(w3.to_wei(2, 'ether'), reserveA, reserveB).call()
amountBOut = amountBOut_/(10**18)
print("2 $A → " + str(amountBOut) + " $B")
amountAOut_ = router_contract.functions.getAmountOut(w3.to_wei(1, 'ether'), reserveB, reserveA).call()
amountAOut = amountAOut_/(10**18)
print("1 $B → " + str(amountAOut) + "$A")

now = int(datetime.now().timestamp())
deadline = now + 200
#トランザクションの作成(swap 200 $A)
tx1 = router_contract.functions.swapExactTokensForTokens(
    w3.to_wei(200, 'ether'),
    0,
    tokenA,
    tokenB,
    my_wallet_address
    ).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

signed_tx1 = w3.eth.account.sign_transaction(tx1, private_key=my_private_key)
tx1_hash = w3.eth.send_raw_transaction(signed_tx1.raw_transaction)

print('swap 200 $A transaction')
print(w3.to_hex(tx1_hash))
result = w3.eth.wait_for_transaction_receipt(tx1_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

time.sleep(60)

#getbalanceOf()関数の呼び出し
balanceA = tokenA_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceA = " + str(balanceA))
balanceB = tokenB_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceB = " + str(balanceB))

(reserve0, reserve1) = pool_contract.functions.getReserves().call()
#print("reserve0: " + str(reserve0))
#print("reserve1: " + str(reserve1))
if(tokenA < tokenB):
    reserveA = reserve0
    reserveB = reserve1
else:
    reserveA = reserve1
    reserveB = reserve0
print("reserveA: " + str(reserveA))
print("reserveB: " + str(reserveB))

amountBOut_ = router_contract.functions.getAmountOut(w3.to_wei(200, 'ether'), reserveA, reserveB).call()
amountBOut = amountBOut_/(10**18)
print("200 $A → " + str(amountBOut) + " $B")
amountAOut_ = router_contract.functions.getAmountOut(w3.to_wei(100, 'ether'), reserveB, reserveA).call()
amountAOut = amountAOut_/(10**18)
print("100 $B → " + str(amountAOut) + "$A")

amountBOut_ = router_contract.functions.getAmountOut(w3.to_wei(20, 'ether'), reserveA, reserveB).call()
amountBOut = amountBOut_/(10**18)
print("20 $A → " + str(amountBOut) + " $B")
amountAOut_ = router_contract.functions.getAmountOut(w3.to_wei(10, 'ether'), reserveB, reserveA).call()
amountAOut = amountAOut_/(10**18)
print("10 $B → " + str(amountAOut) + "$A")

amountBOut_ = router_contract.functions.getAmountOut(w3.to_wei(2, 'ether'), reserveA, reserveB).call()
amountBOut = amountBOut_/(10**18)
print("2 $A → " + str(amountBOut) + " $B")
amountAOut_ = router_contract.functions.getAmountOut(w3.to_wei(1, 'ether'), reserveB, reserveA).call()
amountAOut = amountAOut_/(10**18)
print("1 $B → " + str(amountAOut) + "$A")


now = int(datetime.now().timestamp())
deadline = now + 200
#トランザクションの作成(swap 200 $A)
tx2 = router_contract.functions.swapExactTokensForTokens(
    w3.to_wei(10, 'ether'),
    0,
    tokenB,
    tokenA,
    my_wallet_address
    ).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

signed_tx2 = w3.eth.account.sign_transaction(tx2, private_key=my_private_key)
tx2_hash = w3.eth.send_raw_transaction(signed_tx2.raw_transaction)

print('swap 10 $B transaction')
print(w3.to_hex(tx2_hash))
result = w3.eth.wait_for_transaction_receipt(tx2_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')


time.sleep(60)

#getbalanceOf()関数の呼び出し
balanceA = tokenA_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceA = " + str(balanceA))
balanceB = tokenB_contract.functions.getbalanceOf(my_wallet_address).call()
print("balanceB = " + str(balanceB))

(reserve0, reserve1) = pool_contract.functions.getReserves().call()
#print("reserve0: " + str(reserve0))
#print("reserve1: " + str(reserve1))
if(tokenA < tokenB):
    reserveA = reserve0
    reserveB = reserve1
else:
    reserveA = reserve1
    reserveB = reserve0
print("reserveA: " + str(reserveA))
print("reserveB: " + str(reserveB))

実行してみた。
下記の画像は何度も実行したうちの1つを取り出しているので綺麗な数字にはなっていません。

入金額が大きくなると交換レートが悪くなることも確認できます。

トランザクション1

トランザクション2

※ABI

上記のpythonコードを実行するには、

  • factory_ABI.json

  • pool_ABI.json

  • router_ABI.json

  • token_ABI.json

が必要ですが、これはSolidityコードをコンパイルしたときに生成されたjsonファイルからコピペしました。

以上です。


いいなと思ったら応援しよう!