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.
functionturnIntoDone(uint256 tokenId,uint256 index)publicpayableisApprovedreturns (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 nicknamestringmemory _seller = Trade[tokenId].seller;// estimateFeeuint256 _fee =estimateFee(tokenId);// Get fee FeeRevenues += _fee;// Transfer fee to smart contractpayable(address(this)).transfer(_fee);// Transfer price to sellerpayable(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 selleruint256 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 gradeupdateUserGrade(Trade[tokenId].sellerAddress);updateUserGrade(Trade[tokenId].buyerAddress);safeTransferFrom( Trade[tokenId].sellerAddress, Trade[tokenId].buyerAddress, tokenId,1,"" );emitFinishPurchaseRequest( Trade[tokenId].buyerAddress, Trade[tokenId].sellerAddress, tokenId, Trade[tokenId].price, Trade[tokenId].usedBLI );returntrue; }
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 Tokenuint256publicconstant BLI =0;// Fee ratio for each user gradeuint256public NoobFeeRatio =10;uint256public RookieFeeRatio =9;uint256public MemberFeeRatio =8;uint256public BronzeFeeRatio =7;uint256public SilverFeeRatio =6;uint256public GoldFeeRatio =5;uint256public PlatinumFeeRatio =4;uint256public DiamondFeeRatio =3;// Sum of fee revenue -> The total fees contract will haveuint256private FeeRevenues;constructor() ERC1155("http://BlindMarket.xyz/{id}.json") { _tokenIdCounter.increment(); FeeRevenues =0; }// Enum for user gradeenumGrade { invalid, noob, rookie, member, bronze, silver, gold, platinum, diamond }// Enum for trade phaseenumPhase { invalid, pending, shipping, done, canceled }// Struct for store user datastructUserData {uint256 gradePoint;string nickname; Grade grade; }// Struct for store information about each trade requeststructRequest {uint256 tokenId;uint256 usedBLI;uint256 price;address buyerAddress;string buyer;string seller;address sellerAddress; Phase phase; }// Lock status of each NFT Token for productmapping(uint256=>bool) MutexLockStatus;// Mapping for user address and user informationmapping(address=> UserData) UserInfo;// Mapping for tokenID and trade requestmapping(uint256=> Request) Trade;// Mapping for user nickname and total trade historymapping(string=> Request[]) TradeLogTable;// Mapping for tokenID and token URImapping(uint256=>string) private tokenURIs;
After optimization
// Governance Tokenuint256publicconstant BLI =0;// Fee ratio for each user gradeuint256publicconstant NOOB_FEE_RATIO =10;uint256publicconstant ROOKIE_FEE_RATIO =9;uint256publicconstant MEMBER_FEE_RATIO =8;uint256publicconstant BRONZE_FEE_RATIO =7;uint256publicconstant SILVER_FEE_RATIO =6;uint256publicconstant GOLD_FEE_RATIO =5;uint256publicconstant PLATINUM_FEE_RATIO =4;uint256publicconstant DIAMOND_FEE_RATIO =3;// Total fee revenueuint256private FeeRevenues;constructor() ERC1155("http://BlindMarket.xyz/{id}.json") { _tokenIdCounter.increment(); FeeRevenues =0; }// Enum for user gradeenumGrade { invalid, noob, rookie, member, bronze, silver, gold, platinum, diamond }// Enum for trade phaseenumPhase { invalid, pending, shipping, done, canceled }// Struct for store user datastructUserData {uint256 gradePoint;string nickname; Grade grade; }// Struct for store user datastructRequest {uint256 tokenId;bytes32hash;string buyer;string seller;addresspayable buyerAddress;addresspayable sellerAddress; Phase phase; }// Lock status of each NFT Token for productmapping(uint256=>bool) MutexLockStatus;// Mapping for user address and user informationmapping(address=> UserData) UserInfo;// Mapping for tokenID and trade requestmapping(uint256=> Request) Trade;// Mapping for user nickname and total trade historymapping(string=> Request[]) TradeLogTable;// Mapping for tokenID and token URImapping(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".functionencode(uint128_usedBLI,uint128_price)internalpurereturns (bytes32 x) {assembly {mstore(0x16, _price)mstore(0x0, _usedBLI) x :=mload(0x16) } }// Decodes "bytes32 hash" to "usedBLI" and "price".functiondecode(bytes32 x)internalpurereturns (uint128 usedBLI,uint128 price) {assembly { price := xmstore(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.