Primrose Docs
  • Home
    • ⛓️Blockchain
      • Avalanche
        • What is AVAX?
      • Ethereum
        • Ethereum Cancun Upgrade Explained(draft)
        • go-ethereum: gas estimate
        • Blockchain Transaction Lifecycle
        • Mempool
        • Gas optimization in Solidity, Ethereum
      • Solidity DeepDive
        • Meta transaction
        • solidity: patterns
        • UUPS vs Transparent
        • Solidity Interface
        • Smart contract storage
        • ERC-2981 Contract
        • Solidity modifier
        • Solidity delete keyword
        • How To Make NFTs with On-Chain Metadata - Hardhat and JavaScript
        • How to Build "Buy Me a Coffee" DeFi dapp
        • How to Develop an NFT Smart Contract (ERC 721) with Alchemy
        • Upgradeable Contract
        • Smart Contract Verification
      • Common
        • Eigenlayer
        • MultiSig(draft)
        • Chain-Based Proof-of- Stake, BFT-Style Proof-of-Stake
        • Byzantine Fault Tolerance
        • Zero-knowledge
        • Hierarchical Deterministic Wallet
        • Maker DAO
        • Defi
        • Uniswap
        • IBC
        • Cosmos
        • Gossip Protocol
        • Tendermint
        • UTXO vs Account
        • Blockchain Layer
        • Consensus Algorithm
        • How does mining work?
        • Immutable Ledger
        • SHA256 Hash
        • Filecoin
        • IPFS - InterPlanetary File System
        • IPFS와 파일코인
        • Livepeer
        • Layer 0
      • Bitcoin
        • BIP for HD Wallet
        • P2WPKH
        • Segwit vs Native Segwit
    • 📖Languages
      • Javascript/Typescript
        • Hoisting
        • This value in Javascript
        • Execution Context
        • About Javscript
        • tsconfig.json
        • Nest js Provider
        • 'return await promise' vs 'return promise'
      • Python
        • Pythonic
        • Python: Iterable, Iterator
        • Uvicorn & Gunicorn
        • WSGI, ASGI
        • Python docstring
        • Decorator in Python
        • Namespace in Python
        • Python Method
      • Go
        • GORM+MySQL Connection Pool
        • Context in golang
        • How to sign Ethereum EIP-1559 transactions using AWS KMS
        • Mongo DB in golang(draft)
        • Golang HTTP Package
        • Panic
        • Golang new/make
        • golang container package
        • errgroup in golang
        • Generic Programming in Golang
        • Goroutine(draft)
    • 📝Database
      • MongoDB in golang
      • Nested loop join, Hash join
      • DB Query plan
      • Index
      • Optimistic Lock Pessimistic Lock
    • 💻Computer Science
      • N+1 query in go
      • Web server 를 구성할 때 Thread, Process 개수를 어떻게 정할 것인가?
      • CAP
      • Socket programming
      • DNS, IP
      • URL, URI
      • TLS과 SSL
      • Caching(draft)
      • Building Microservices: Micro Service 5 Deploy Principle
      • Red Black Tree
      • AOP
      • Distributed Lock
      • VPC
      • Docker
      • All about Session and JWT
      • Closure
      • Singleton Pattern
      • TCP 3 way handshake & 4 way handshake
      • Race Condition
      • Process Address Space 
      • Call by value, Call by reference, Call by assignment
      • Zookeeper, ETCD
      • URL Shortening
      • Raft consensus
      • Sharding, Partitioning
    • 📒ETC
      • K8S SIGTERM
      • SQS
      • Git Branch Strategy: Ship / Show / Ask
      • Kafka
      • Redis Data Types
      • CI/CD
      • How does Google design APIs?
      • Minishell (42 cursus)
      • Coroutine & Subroutine
      • Redis
Powered by GitBook
On this page
  • Before optimization
  • After optimization
  • Sources …
  1. Home
  2. Blockchain
  3. Ethereum

Gas optimization in Solidity, Ethereum

PreviousMempoolNextSolidity DeepDive

Last updated 1 year ago

I’m sorry but my English is terrible. I hope you understand that generously.

Recently, I was developing a toy project named Blind Market. It’s a simple P2P trading application using smart contract.

I was making a contract using Solidity, and the trade stage proceeded in the order of pending, shipping, and done.

The problem was appeared in done phase. The problem was that when I tried to close the transaction by paying the price raised by the seller in msg.value, the following error occurred.

I was conducting a test in the Remix VM environment, and there was no shortage of ether. But the pending status does not end.

But if there was any doubt, the turnIntoDone force was doing too much.

function turnIntoDone(uint256 tokenId, uint256 index)
        public
        payable
        isApproved
        returns (bool)
    {
        require(Trade[tokenId].phase == Phase.shipping);

        require(
            keccak256(bytes(UserInfo[msg.sender].nickname)) ==
                keccak256(bytes(Trade[tokenId].buyer))
        );

        // Change phase
        Trade[tokenId].phase = Phase.done;

        // Seller nickname
        string memory _seller = Trade[tokenId].seller;

        // estimateFee
        uint256 _fee = estimateFee(tokenId);

        // Get fee
        FeeRevenues += _fee;

        // Transfer fee to smart contract
        payable(address(this)).transfer(_fee);

        // Transfer price to seller
        payable(Trade[tokenId].sellerAddress).transfer(
            Trade[tokenId].price - _fee
        );

        // Update Phase
        TradeLogTable[UserInfo[msg.sender].nickname][index].phase = Phase.done;
        TradeLogTable[_seller][index].phase = Phase.done;

        // Mint Blind Token to buyer and seller
        uint256 AmountOfBuyer = estimateAmountOfBLI(
            Trade[tokenId].buyerAddress,
            tokenId
        );
        uint256 AmountOfSeller = estimateAmountOfBLI(
            Trade[tokenId].sellerAddress,
            tokenId
        );
        mintBlindToken(Trade[tokenId].buyerAddress, AmountOfBuyer);
        mintBlindToken(Trade[tokenId].sellerAddress, AmountOfSeller);

        // Set users grade point
        UserInfo[Trade[tokenId].sellerAddress].gradePoint += 1000;
        UserInfo[Trade[tokenId].buyerAddress].gradePoint += 1000;

        // Update users grade
        updateUserGrade(Trade[tokenId].sellerAddress);
        updateUserGrade(Trade[tokenId].buyerAddress);

        safeTransferFrom(
            Trade[tokenId].sellerAddress,
            Trade[tokenId].buyerAddress,
            tokenId,
            1,
            ""
        );

        emit FinishPurchaseRequest(
            Trade[tokenId].buyerAddress,
            Trade[tokenId].sellerAddress,
            tokenId,
            Trade[tokenId].price,
            Trade[tokenId].usedBLI
        );
        return true;
    }

Looking at it again, it's really long and a lot of work. It became too long because I wrote the code first.

There were parts where data was obtained using the view function in the middle, I thought it was safer to call from inside the function than to call from the outside and get it and inject the data.

The first method I prescribed to solve the problem was to optimize gas. It seemed clear that this function would require a considerable amount of gas, even if this was not the cause.

So I started to rebuild the data structures in contract first.

Before optimization

    // Governance Token
    uint256 public constant BLI = 0;

    // Fee ratio for each user grade
    uint256 public NoobFeeRatio = 10;
    uint256 public RookieFeeRatio = 9;
    uint256 public MemberFeeRatio = 8;
    uint256 public BronzeFeeRatio = 7;
    uint256 public SilverFeeRatio = 6;
    uint256 public GoldFeeRatio = 5;
    uint256 public PlatinumFeeRatio = 4;
    uint256 public DiamondFeeRatio = 3;

    // Sum of fee revenue -> The total fees contract will have
    uint256 private FeeRevenues;

    constructor() ERC1155("http://BlindMarket.xyz/{id}.json") {
        _tokenIdCounter.increment();
        FeeRevenues = 0;
    }

    // Enum for user grade
    enum Grade {
        invalid,
        noob,
        rookie,
        member,
        bronze,
        silver,
        gold,
        platinum,
        diamond
    }

    // Enum for trade phase
    enum Phase {
        invalid,
        pending,
        shipping,
        done,
        canceled
    }

    // Struct for store user data
    struct UserData {
        uint256 gradePoint;
        string nickname;
        Grade grade;
    }

    // Struct for store information about each trade request
    struct Request {
        uint256 tokenId;
        uint256 usedBLI;
        uint256 price;
        address buyerAddress;
        string buyer;
        string seller;
        address sellerAddress;
        Phase phase;
    }

    // Lock status of each NFT Token for product
    mapping(uint256 => bool) MutexLockStatus;

    // Mapping for user address and user information
    mapping(address => UserData) UserInfo;

    // Mapping for tokenID and trade request
    mapping(uint256 => Request) Trade;

    // Mapping for user nickname and total trade history
    mapping(string => Request[]) TradeLogTable;
    
    // Mapping for tokenID and token URI
    mapping(uint256 => string) private tokenURIs;

After optimization

    // Governance Token
    uint256 public constant BLI = 0;

    // Fee ratio for each user grade
    uint256 public constant NOOB_FEE_RATIO = 10;
    uint256 public constant ROOKIE_FEE_RATIO = 9;
    uint256 public constant MEMBER_FEE_RATIO = 8;
    uint256 public constant BRONZE_FEE_RATIO = 7;
    uint256 public constant SILVER_FEE_RATIO = 6;
    uint256 public constant GOLD_FEE_RATIO = 5;
    uint256 public constant PLATINUM_FEE_RATIO = 4;
    uint256 public constant DIAMOND_FEE_RATIO = 3;

    // Total fee revenue
    uint256 private FeeRevenues;

    constructor() ERC1155("http://BlindMarket.xyz/{id}.json") {
        _tokenIdCounter.increment();
        FeeRevenues = 0;
    }

    // Enum for user grade
    enum Grade {
        invalid,
        noob,
        rookie,
        member,
        bronze,
        silver,
        gold,
        platinum,
        diamond
    }

    // Enum for trade phase
    enum Phase {
        invalid,
        pending,
        shipping,
        done,
        canceled
    }

    // Struct for store user data
    struct UserData {
        uint256 gradePoint;
        string nickname;
        Grade grade;
    }

    // Struct for store user data
    struct Request {
        uint256 tokenId;
        bytes32 hash;
        string buyer;
        string seller;
        address payable buyerAddress;
        address payable sellerAddress;
        Phase phase;
    }

    // Lock status of each NFT Token for product
    mapping(uint256 => bool) MutexLockStatus;

    // Mapping for user address and user information
    mapping(address => UserData) UserInfo;

    // Mapping for tokenID and trade request
    mapping(uint256 => Request) Trade;

    // Mapping for user nickname and total trade history
    mapping(string => Request[]) TradeLogTable;

    // Mapping for tokenID and token URI
    mapping(uint256 => string) private tokenURIs; 

The first thing I tried was to reconstruct the variables of the structure.

I change the value type of usedBLI and price to uint128 from uint256.

Because the SSTORE command consumes a lot of gas, it was determined that it was necessary to pack the variables according to uint256.

Second is declare every fee ratio variable as constant variable.

Third is add encode and decode function for encoding two variable (usedBLI, price) and for decoding when we use them.

I judged that it would be more efficient to keep the encoded bytes32 variable instead of keeping the two variables in the structure. The encoding function and the decoding function were written as follows.

    // Encodes "usedBLI" and "price" as "bytes32 hash".
    function encode(uint128 _usedBLI, uint128 _price)
        internal
        pure
        returns (bytes32 x)
    {
        assembly {
            mstore(0x16, _price)
            mstore(0x0, _usedBLI)
            x := mload(0x16)
        }
    }

    // Decodes "bytes32 hash" to "usedBLI" and "price".
    function decode(bytes32 x)
        internal
        pure
        returns (uint128 usedBLI, uint128 price)
    {
        assembly {
            price := x
            mstore(0x16, x)
            usedBLI := mload(0)
        }
    }

Finally, local variable was actively used to minimize the number of times the storage data is read.

As I made the correction, the reason was actually payable.

There was a part of transferring to the contract using payable, and when I annotated that part, everything worked smoothly.

However, through this process, my knowledge of gas optimization seems to have improved than before, so I share it in writing.

Below are the links that helped solve the problem. It would be good to refer to.

Sources …

⛓️
Pending...