Gas optimization in Solidity, Ethereum

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.

Pending...
Pending...

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 …

Subscribe to Primrose
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.