Solidity – Efficiently Accessing Mappings in Solidity

contract-designgas-estimateoptimizationsolidity

I'm working on keeping track of data via a struct stored in a mapping.

mapping(uint256 => Session);

Here is the Session struct:

  struct Session {
      // UID
      uint256 id;
      // Timestamp the sale starts
      uint256 start;
      // Timestamp the sale stops
      uint256 stop;
      // Last rate sync timestamp
      uint256 sync;
      // Current Rate
      uint256 rate;
      // Rate normalization: rate / 10**{decimal}
      uint8 decimal;
      // Current TOKEN|BNB Rate
      uint256 issue;
      // WEI raised
      uint256 raised;
      // Maximum sale quantity
      uint256 max;
      // Total tokens sold
      uint256 sold;
      // Total minutes before bnb rate update
      uint256 threshold;
      // Contract address of chainlink aggregator
      address chainlink;
      // Token Sale Owner
      address owner;
      // Token Contract
      IBEP20 token;
  }

What is the most efficient ways to modify a session?

Method 1:

/**
   * @dev low level token purchase ***DO NOT OVERRIDE***
   * @param _beneficiary Address performing the token purchase
   */
  function buyTokens(uint256 _id, address _beneficiary) public payable open(_id) {

    uint256 weiAmount = msg.value;
    _preValidatePurchase(_beneficiary, weiAmount);

    // calculate token amount to be created
    uint256 tokens = _getTokenAmount(weiAmount);

    // update state
    sessions[_id].raised = sessions[_id].raised.add(weiAmount);

    _processPurchase(_beneficiary, tokens);
    emit TokenPurchase(
      msg.sender,
      _beneficiary,
      weiAmount,
      tokens
    );

    _forwardFunds();
    _postValidatePurchase(_beneficiary, weiAmount);
  }

Method 2:

  /**
   * @dev low level token purchase ***DO NOT OVERRIDE***
   * @param _beneficiary Address performing the token purchase
   */
  function buyTokens(uint256 _id, address _beneficiary) public payable open(_id) {

    uint256 weiAmount = msg.value;
    _preValidatePurchase(_beneficiary, weiAmount);

    // calculate token amount to be created
    uint256 tokens = _getTokenAmount(weiAmount);

    // update state
    Session storage sesh = sessions[_id];
    sesh.raised = sesh.raised.add(weiAmount);

    _processPurchase(_beneficiary, tokens);
    emit TokenPurchase(
      msg.sender,
      _beneficiary,
      weiAmount,
      tokens
    );

    _forwardFunds();
    _postValidatePurchase(_beneficiary, weiAmount);
  }

For some reason in my head Method 2 is better, when I access a mapping does it have to search each time?

And the storage keyword, does that actually modify the object in storage within the mapping or am I just bonkers?

Best Answer

It is a bit hard to know in advance which method is optimal. In your scenario, gasEstimate states that the second method is less expensive.


To test your implementation I stripped everything from the contract except for the assign of the value (also I have a fake structure):

CONTRACT

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;



contract Optim {
    struct Session {
        uint256 raised;
    }

    mapping(uint256 => Session) sessions; 
    
    
    constructor() {        
        sessions[1] = Session(1);
    }

    function direct(uint256 _id) public payable {            
        // update state
        sessions[_id].raised = sessions[_id].raised + 999;        
    }   

    function tmpvar(uint256 _id) public payable {            
        // update state
        Session storage sesh = sessions[_id];
        sesh.raised = sesh.raised + 999;        
    }   
}

creating a simple test with truffle allows us to have some information:

gasEstimate.js

const Optim = artifacts.require("Optim");

contract("Optim", () => {
    it("should have different gas", async() => {
        const O1 = await Optim.new();

        const method1 = await O1.direct.estimateGas(11);
        const method2 = await O1.tmpvar.estimateGas(11);

        console.log("method1 cost: " + method1, "method2 cost: " + method2);
        assert(method1 < method2, "method 1 < method2");
    });
});

the console output is

method1 cost: 42762 method2 cost: 42675

therefore the second method is less expensive