Assignment - Storage Design

OnlyMapping
pragma solidity 0.7.5;
pragma abicoder v2;



contract OnlyMapping {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    mapping(address => Entity) entities;
    
    
    function addEntity(uint _data) public returns(Entity memory){
        Entity memory _entity = Entity(_data, msg.sender);
        
        //push entity to the entities mapping
        entities[msg.sender] = _entity;
    
        return _entity;
    }
    
    function updateEntity(uint _newData) public {
        Entity memory _entity = entities[msg.sender];
        
        //update local memory object reference to msg.sender with new data
        _entity.data = _newData;
        
        //push entity to the entities mapping
        entities[msg.sender] = _entity;
        
    }
    
    function getEntity() public view returns(uint, address){
        return (entities[msg.sender].data, entities[msg.sender]._address);
    }
    
    
}



OnlyArray
pragma solidity 0.7.5;
pragma abicoder v2;



contract OnlyArray {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] entities;
    
    
    function addEntity(uint _data) public returns(Entity memory){
        Entity memory _entity = Entity(_data, msg.sender);
        
        //push entity to the entities mapping
        entities.push(_entity);
    
        return _entity;
    }
    
    function updateEntity(uint _newData) public {
       
        
        for(uint i=0; i<entities.length; i++){
            if(entities[i]._address == msg.sender){
                 Entity storage _entity = entities[i];        
                //update local memory object reference to msg.sender with new data
                _entity.data = _newData;
            }
        }

    }
    
    function getEntity() public view returns(uint, address){
        
          for(uint i=0; i<entities.length; i++){
              if(entities[i]._address == msg.sender){
                  return (entities[i].data, entities[i]._address);
              }
          }
    }
    
    
}



OnlyMapping: Execution cost: 42,051 gas
OnlyArray: Execution cost: 62,887 gas

OnlyArray design consumes more gas than OnlyMapping. The difference of 20,836 I believe is quite substantial.

updateEntity() 5th address result:

OnlyMapping: 9,148 gas
OnlyArray: 20,340 gas

OnlyArray solution consumes more gas because we have to iterate through each item in the array in order to find the item that we would like to update. This takes O(n) time complexity to do, thus slower. This becomes problematic when the array has more items added to it.

On the other hand, with mapping it consumes a little over 2x less gas as it takes O(1) time complexity to update the item considering we pass the address of the function’s caller to the mapping in order to retrieve its data and update it. Thus, removing the process of iterating through items to find the one we want to update which is the main reason why mapping is faster for updating items than arrays.

1 Like

OnlyAnArray.sol

pragma solidity 0.8.0;

contract OnlyAnArray {
    
struct Entity{
    uint data;
    address _address;
}

Entity[] Entities;

function AddEntity(uint data, address) public returns (Entity memory) {
    Entity memory NewEntity = Entity(data, msg.sender);
    Entities.push(NewEntity);
    return Entities[Entities.length - 1];     
}

//or
//function AddEntity(uint data, address) public returns (Entity memory) {
//    Entity memory NewEntity;
//    NewEntity.data = data;
//    NewEntity._address = msg.sender;
//    Entities.push(NewEntity);
//    return Entities[Entities.length - 1];
//}

function updateEntity(uint data, address) public returns (Entity memory){
    Entity memory _NewEntity;
    _NewEntity.data = data;
    Entities.push(_NewEntity);
    return Entities[Entities.length - 1];
}

function getEntitiyCount() public view returns (uint entityCount) {
    return Entities.length;
}
    
}

OnlyAMapping.sol

pragma solidity 0.8.0;

contract OnlyAMapping {
    
struct Entity{
    uint data;
    bool ofOne;
}
    
mapping(address => Entity) public Entities;

function AddEntity(address, uint data) public returns(bool ofTwo){
    if(ofEntity(msg.sender)) revert();
    Entities[msg.sender].data = data;
    Entities[msg.sender].ofOne = true;
    return true;
}

function ofEntity(address) public view returns(bool ofThree){
    return Entities[msg.sender].ofOne;
}    
    
function updateEntity(address, uint data) public returns(bool ofTwo){
    if(!ofEntity(msg.sender)) revert();
    Entities[msg.sender].data = data;
    return true;
} 

}

Executing addEntity function:

  • execution cost is higher in the OnlyAnArray contract: 66762 gas compared to 42916 in the OnlyAMapping contract, is app. 36 % higer. It is a significant difference, because we perform more operations, push the data and the address in the array and return the array. Meanwhile we only set the data for the address in a mapping and return a boolean?

UpdateEntity function, 5.input:

  • OnlyAMapping contract: execution cost 6907 gas,
  • Only AnArray contract: execution cost 32254 gas (app 80% higher), because we are pushing new data in an array, meanwhile in a mapping we are only writing new data for an address?
1 Like

When executing the addEntity function, which design consumes the most gas (execution cost)? Array

Is it a significant difference? Why/why not? The array cost ~5% more gas fee.
Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why? Array consume more gas fee.

1 Like

This is my solution;

pragma solidity 0.8.0;

contract MappingStorageDesign {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    mapping(address => Entity) entityStruct;
    
    function addEntity(uint _data) public returns(Entity memory newEntity) {
        
        return entityStruct[msg.sender] = Entity(_data, msg.sender);
    }
    
    function updateEntity(uint _data) public returns(Entity memory updatedData) {
        entityStruct[msg.sender].data = _data;
        return entityStruct[msg.sender] ;
    }
}

contract ArrayStorageDesign {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] entityArray;
    
    function addEntity(uint _data) public  returns(Entity[] memory newEntity) {  
        
        entityArray.push(Entity(_data, msg.sender));
        
        return entityArray;
    }
    
    function updateEntity(uint _data) public returns (Entity[] memory updatedData) {
        entityArray[entityArray.length-1].data = _data; // So as to be able to change the last data in the array.
        return entityArray; 
    }
}
  1. When executing the addEntity function the ArrayStorageDesign consumed 65946 gas while the MappingStorageDesign consumed 43668 gas. This is significant because there is savings on gas using a mapping.
  2. When executing the update function the ArrayStorageDesign consumed 20362 gas while the MappingStorageDesign consumed 7830 gas.
    The Array Solution consumed more gas because it is a complex data type while the Mapping Solution consumed less gas because it is more a simple data type (a one way street) fast and easy look ups to mention but a few of the attributes of a Mapping.
    And most importantly it is not an Enumerable Data Type but an Array is an Enumerable Data Type.
1 Like

The gas price for the array was 410996 gas for the transaction and 272112 gas for the execution.
The gas price for the mapping was 377815 for the transaction and 244287 gas for the execution.
The mapping save a substantial amount of gas.
The array gas price post added entities was 62154 gas for execution and 8481 gas for transaction.
The mapping gas price was 555590 gas for transaction and 666000 for execution.

// Mapping
pragma solidity 0.8.0;
contract simpleMapping {
    struct Entity {
        uint data;
        address _address;
        bool isEntity;
    }
    mapping(address => Entity) public entityStructs;
    function addEntity(address entityAddress, uint data) public payable returns (bool success) {
     require(isEntity(entityAddress));
     entityStructs[entityAddress].data=data;
     entityStructs[entityAddress]._address= msg.sender;
        return true;
    }
    function isEntity(address entityAddress) public view returns(bool isIndeed) {
    return entityStructs[entityAddress].isEntity;
  }
    function updateEntity(address entityAddress, uint data) public returns (bool success) {
         require(isEntity(entityAddress));
        entityStructs[entityAddress].data=data;
        return true;
    } 
}
`//Array
pragma solidity 0.8.2;
contract Zebra {
struct Entity {
uint data;
address _address;
}
Entity[] public Entities;
function addEntity(uint data, address _address) public returns(Entity memory) {
Entity memory addEntity;
addEntity.data =data;
addEntity._address=msg.sender;
Entities.push(addEntity);
return Entities[Entities.length - 1];
}
function updateEntity() public view returns(uint entityVolume) {
return Entities.length;
}
1 Like

This is the solution of the assignment based on an array. I added a getIndex() function to return the index in the array of a given address. I use this function to make sure every address only gets one position in the array, so one address can not be added more than one time. This mechanism causes the execution cost to increase every time a new entity is added since getIndex() is O(n) complexity.

Adding a new entity costs 51000 gas and goes up for every further addition.
Updating the 5th entity costs 20000 gas.

pragma solidity 0.8.0;

contract simpleStructArray { 

    struct EntityStruct {
        address entityAddress;
        uint entityData;
    }

    EntityStruct[] public entityList;
    
    function getIndex(address _address) public view returns(int){ //if address not in list: returns -1; else, returns index
        
        for(uint i = 0; i<entityList.length; i++){
            if(entityList[i].entityAddress == _address) return int(i);
        }
        return -1;
    }

    function addEntity(uint _entityData) public { //execution cost 51000 to 63000 gas
        address entityAddress = msg.sender;
        require(getIndex(entityAddress) == -1, "An entity for this address already exists");
        EntityStruct memory newEntity;
        newEntity.entityAddress = entityAddress;
        newEntity.entityData    = _entityData;
        entityList.push(newEntity);
    }
  
    function updateEntity(uint _entityData) public { //updating the 5th added entity: 20000 gas (compared to 9000 for updating the 1st added entity)
        address entityAddress = msg.sender;
        int index = getIndex(entityAddress);
        require(index != -1, "No entity for this address exists");
        entityList[uint(index)].entityData = _entityData;
    }
  
}


This is the contract based on the mapping.
Adding an entity has a constant cost of 42000 gas.
Updating any entity has a constant cost of 6500 gas (including the 5th entity)
This cost is lower than the array-based solution and does not depend on the size of the mapping because accessing an element in the mapping is O(constant) time complexity.


contract simpleStructMapping { 

    struct EntityStruct {
        address entityAddress;
        uint entityData;
    }

    mapping (address => EntityStruct) public entityMapping;
    
    function addEntity(uint entityData) public { //42000 for all
        address entityAddress = msg.sender;
        if(isEntity(entityAddress)) revert(); 
        entityMapping[entityAddress].entityData = entityData;
        entityMapping[entityAddress].entityAddress = entityAddress;
    }
      
    function isEntity(address entityAddress) public view returns(bool isIndeed) {
        return entityMapping[entityAddress].entityAddress != address(0x0);
    }
    
    
    function updateEntity(uint entityData) public { //6500 gas independent of which entity is updated
        address entityAddress = msg.sender;
        if(!isEntity(entityAddress)) revert();
        entityMapping[entityAddress].entityData = entityData;
    }
}
1 Like

When executing the addEntity function, which design consumes the most gas (execution cost)?
Array design costs more.

Is it a significant difference? Why/why not?
In Array solution adding entity costs ~2 times more and searching the update will cost ~4 times more. Update function will probably cost even more, if there are more entities.

Add 5 Entities into storage using the addEntity function and 5 different addresses.
Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

Array solution costs more gas. In array solution the entities needed to be loop trough to find the matching sender address.


EntityMapping.sol

pragma solidity 0.7.5;

contract EntityMapping {
    
    // not needed for this one. If it is needed to add fields to the struct then the mapping should be updated.
    struct Entity {
        uint _data;
        address _address;
    }
    
    mapping(address => uint) entities;
    
    // addEntity() creates a new entity for msg.sender and adds it to the mapping / array
    function addEntity(uint data) public {
        entities[msg.sender] = data;
    }
    
    // updateEntity() updates the data in a saved entity for msg.sender.
    function updateEntity(uint data) public {
        entities[msg.sender] = data;
    }
}

EntityArray.sol

pragma solidity 0.7.5;

contract EntityArray {
    
    struct Entity {
        uint _data;
        address _address;
    }
    
    Entity[] entities;
    
    // addEntity() creates a new entity for msg.sender and adds it to the mapping / array
    function addEntity(uint data) public {
        entities.push(Entity(data, msg.sender));
    }
    
    // updateEntity() updates the data in a saved entity for msg.sender.
    function updateEntity(uint data) public {
        for (uint i = 0; i < entities.length; i++)
        {
            if (entities[i]._address == msg.sender )
            {
                entities[i]._data = data;
            }
        }
    }
}

image

1 Like

Here are the 2 contracts that I have developed. I made the following assumptions (for both contracts):
addentity() - must only add an entity for this address if one doesn’t already exist.
updateEntity() - must only update an entity if one already exists for this address.

My first storage design for the struct just using a mapping:

pragma solidity 0.8.4;
pragma abicoder v2;

contract StorageInMapping {
    
   struct Entity{
      uint data;
      address _address;
    }
    
    mapping(address => Entity) internal _entities;
    
    
    function addEntity(uint entityData) public {
        require(_entities[msg.sender]._address == address(0), "Entry already exists!");
        _entities[msg.sender] = Entity(entityData, msg.sender);
    }
    

    function updateEntity(uint entityData) public {
        require(_entities[msg.sender]._address == msg.sender, "No current entry to update!");
        _entities[msg.sender].data = entityData;
    }
    
    
    function getEntity() public view returns(Entity memory theRecord) {
        return _entities[msg.sender];
    }
    
}

My second storage design just using an array:

pragma solidity 0.8.4;
pragma abicoder v2;

contract StorageInArray {
    
    struct Entity{
      uint data;
      address _address;
    }
    
    Entity[] internal _entities;
    
    
    function addEntity(uint entityData) public {
        require(!_hasAnEntity(msg.sender), "An entry already exists!");
        _entities.push(Entity(entityData, msg.sender));
    }
    

    function updateEntity(uint entityData) public {
        require(_hasAnEntity(msg.sender), "No current entry to update!");
        uint index = _getEntryIndex(msg.sender);
        _entities[index].data = entityData;
    }
    
    
    function getEntity(address target) public view returns(Entity memory theRecord) {
        return _entities[_getEntryIndex(target)];
    }
    
    
    // Internal functions
    
    function _hasAnEntity(address candidate) internal view returns(bool) {
        for (uint i=0; i<_entities.length; i++) {
            if (_entities[i]._address == candidate) return true;
        }
        return false;
    }
    
    
    function _getEntryIndex(address target) internal view returns(uint) {
        for (uint i=0; i<_entities.length; i++) {
            if (_entities[i]._address == target) return i;
        }
        revert ("No entry for this address");
    }
    
}

Results of code execution: addEntity() function used to add an entry for only one address:

  1. StorageInMapping
    transaction cost: 63880 gas
    execution cost: 42416 gas

  2. StorageInArray
    transaction cost: 84661 gas
    execution cost: 63197 gas

There’s already a significant difference in gas execution cost. The StorageInArray design consumes about 50% more gas in execution cost than the StoreInMapping design. This is because array storage is more costly than storage in a mapping. Whilst the array is iterated to check that an entry doesn’t already exist; there is only one storage entry (and so just one array element to check). This shows that a single array read and write costs significantly more than a single mapping read and write operation.

For each contract : Adding 5 entities (to 5 different addresses) using addEntity(), then updating 5th entry with updateEntity():

  1. StorageInMapping (when updating 5th entity)
    transaction cost: 27944 gas
    execution cost: 6480 gas
    TOTAL: 34424

  2. StorageInArray (when updating 5th entity)
    transaction cost: 55217 gas
    execution cost: 33753 gas
    TOTAL: 88960

The storage array design consumes significantly more gas (over 5x the execution cost) as the effect of having to iterate through the five array entries (to find the index into the array for the address in question) is taking its toll. The gas cost will continue to increase as the number of entities stored increases (ie. size of the array increases).

1 Like
  1. The Array method consumes more gas than the Mapping method for addEntity(). The Mapping method is about 50% cheaper in execution gas cost. This is because the Array method uses more computing resources.
  2. The Array solution consumes more gas. This is because it uses more computing resources than the Mapping method.
1 Like
pragma solidity 0.8.1;

contract mappingStorage {
    
    struct Entity{
        uint data;
        address _address;
    }
    
    mapping(address => Entity) internal _entities;
    
    function addEntity(uint entitydata) public {
        require(_entities[msg.sender]._address == address(0), "Entry already exists!");
        _entities[msg.sender] = Entity(entitydata, msg.sender);
    }
    
    function updateEntity(uint entitydata) public {
        require(_entities[msg.sender]._address == msg.sender, "No current entry to update!!");
        _entities[msg.sender].data = entitydata;
    }
    
    function getEntity() public view returns(Entity memory theRecord) {
        return _entities[msg.sender];
    }
    
}
pragma solidity 0.8.1;

contract arrayStorage {
    
        struct Entity{
            uint data;
            address _address;
        }
    
    Entity[] internal _entities;
    
    function addEntry(uint entitydata) public {
        require(!_hasAnEntity(msg.sender), "An entry is in exsistance!");
        _entities.push(Entity(entitydata, msg.sender));
    }
    
    
    function updateEntity(uint entitydata) public {
        require(_hasAnEntity(msg.sender), "No entry to update!");
        uint index = _getEntryIndex(msg.sender);
        _entities[index].data = entitydata;
    }
    
    
    function _hasAnEntity(address newEntry) internal view returns(bool) {
        for (uint m=0; m<_entities.length; m++) {
            if (_entities[m]._address == newEntry) return true;
        }
        return false;
    }
    
    
    function _getEntryIndex(address location) internal view returns(uint) {
        for (uint m=0; m<_entities.length; m++) {
            if (_entities[m]._address == location) return m;
        }
        revert("No entry for this address");
    } 
    
}

1: Using the Array method consumes far more gas than the Mapping method for addEntry functions. This is due to the computing functions that need to take place for the array functions.

2: More computing power in the Array method causes more gas to be consumed. this is a flaw in the storage design.

1 Like

Array Contract

pragma solidity 0.7.5;
pragma abicoder v2;

contract StorageDesignAssignmentARRAY {
    
    struct Entity{
        uint data;
        address _address;
        string name;
    }
    
    Entity[] public entityArray;
    
    function addEntity(string memory name, uint data, address entityaddress) public returns (bool success) {
        Entity memory addEntity;
        addEntity._address = entityaddress;
        addEntity.data = data;
        addEntity.name = name;
        entityArray.push(addEntity);
        return true;
    }
    
    function updateEntity(uint _index, uint data, string memory name) public returns (bool success) {
        entityArray[_index].data = data;
        entityArray[_index].name = name;
        return true;
        
    }
    
    
    function getEntity2() public view returns (Entity[] memory) {
        return entityArray;
    }
}
    
    

Mapping contract

pragma solidity 0.7.5;
pragma abicoder v2;

contract StorageDesignAssignmentMAPPING {
    
    struct Entity{
    uint data;
    string name;
    address _address; 
    }
    
    mapping (address => Entity) entityMapping;
    
    //CREATE new entity for msg.sender and add to the mapping 
    function addEntity(uint data, string memory name, address _address) public {
    entityMapping[_address].data = data;
    entityMapping[_address].name = name;
    entityMapping[_address]._address = _address;
    }
    
    //UPDATE data in an already created entity
    function updateEntity(address _address, string memory name, uint data) public returns (bool success){
        entityMapping[_address].data = data;
        entityMapping[_address].name = name;
        return true;
    }
    
}
  1. Mapping design gas consumption: 63,430 gas
    Array design gas consumption: 84,491 gas
    It is 25% cheaper to add an entity to a mapping than it is an array

  2. Execution cost for update to Mapping: 13,498 gas
    Execution cost for update to Array: 15,117 gas
    The solution that consumes more gas in updating information is still the array, by an increase in almost 12%

1 Like

MY ASSIGNMENT SOLUTION

So i took my time with this and explored a few things. If anyone reads this and notices something i assume or say is incorrect just comment a reply and i will fix it. Thanks in advance. Now obviously the requirements of the assignment limit us to using two functions being the addEntry( ) and updateEntry( ) funcs. I tried to write a decent program that can still handle some edge cases while only using the two functions So ive written 4 contracts. They are:

1. MappingContract

2. BetterMappingContract

3. ArrayContract

4. BetterArrayContract

I want to begin with the mapping contract because i noticed something quite interesting when coding up my SimpleMapping solution that i would like some clarity on for those who read this. I have given my code below

contract MappingStorage {
    
    
     struct Entity {
        uint data;
        address _address;
    }
    
    mapping (address => Entity) public newEntity;
   
    function addEntity(address _address, uint _data) public {
        
        require(_address == msg.sender);
        newEntity[_address].data = _data;
        newEntity[_address]._address = msg.sender;
     
    }
    
    function updateEntit(address address_, uint _data) public{
        
        require(newEntity[address_]._address == msg.sender);
        //uint id = 0;
        
        newEntity[address_].data = _data;
        
        
    }
}

So the nice thing about this simple program is that its very simple and uses no complex logic like for loops or anything which would change the time complexity of the program therefore upping the cost of gas both for deployment and function executions. Below are the screenshots of the gas cost for deploying the initial contract and also for calling the two functions:

DEPLOYMENT
MappingDeployment

Transaction cost: 396740 gas
execution cost: 258700 gas

AddEntry( )
MappingAddEntry

Transaction cost: 27766 gas
execution cost: 6302 gas

UpdateEntry( )
MappingUpdateEntry

Transaction cost: 29554 gas
execution cost: 6682 gas

These costs seem relatively cheap since were not storing large amounts of data anywhere like in arrays or anything. And the fact that we are using amppings here rather than ID index and array look ups makes things a little cheaper as to execute these functions by index we would need to store each instance in some array and keep track of them all.

However i stumbled upon something with this simple solution. By making the mapping public i can use it when i deploy the contract and get the details of a specific mapping by passing in the address. I noticed that if i was to add a new entry with the same address of a previously added entry then the attributes of that entry simply get overwritten. This has the exact same effect as the updateEntry( ) function (only for this particular contract setup) Since were not storing our entity instances anywhere then i thought perhaps there is no need for the updateEntry( ) function since it is doing the same thing. So i removed it and this is the changes in gas

DEPLOYMENT
m2

Transaction cost: 3259790 gas
execution cost: 205451 gas

Obviously there is no need to show the updateEntry ( ) again as it has not chagnged there for the cost will be pretty much the same. However by removing the updateEntry( ) function we saved roughly 70000 gas for the transacion cost and 50000 gas for the execution gas. Which is not the best but its still slightly better

One thing im unsure of with this simple contract however is the correctness of my decision to remove the updateEntry( ) function. Because perhaps duplicates are getting stored somewhere in memory that im not aware of and when i go to querey the mapping of msg.sender that solidity just returns the most recent one because it cant decide which entry to return if there are duplicates of the same address. This is something i curious about but not sure of. If anyone has any insight into this let me know.

BETTER MAPPING CONTRATC
Now obviously the bad thing about this contract is its so simple ad is not very secure or robust (simply just the assignment requirements) (no deletion or duplication prevention). So i decided to write a better version of this contract while adhering to the assignment requirements. This only difference is that this contract prevents duplicate entries.

contract MappingStorageBetter {
    
    
     struct Entity {
        uint data;
        address _address;
        bool isKnown;
    }
    
    mapping (address => Entity) public newEntity;
    
    
    function addEntity(address _address, uint _data) public {
        require(!newEntity[_address].isKnown);
        require(_address == msg.sender);
        newEntity[_address].data = _data;
        newEntity[_address]._address = msg.sender;
        newEntity[_address].isKnown = true;
    
        
    }
    
    function updateEntit( uint _data) public{
        
        
        require(newEntity[msg.sender]._address == msg.sender);
        
        //uint id = 0;
        
        newEntity[msg.sender].data = _data;
        
        
    }
}

One of the nice things about using mappings over arrays is that its much easier to get by without having to use for loops for lookups or data manipulation. This contract is very much the same but it adds one more level of security by preventing duplicates. Since this is so the updateEntry( ) function obviously has to be included as we cant create two entries for the same address.

DEPLOYMENT
BeterMappingDeployment

Transaction cost: 421006 gas
execution cost: 27618 gas

As we can see compared to the deployment of the simple mapping solution the transaction cost has gone up by about 25000 gas and the execution cost has gone up by roughly the same. This is a very minimal sacrifice for the added level of security in my opinion. The nice thing is that our solution still runs in constant time thus there is no loops or anything to really drive up the gas price. Again this is a nice characteristic of mappings. However this upgraded solution is by no means prefect its pretty terrible still as we arent keeping a log of our instances and we still cant support deletion. But the point here is to show the demonstration of the gas cost. Next lets look at the array contracts

ARRAY STORAGE SOLUTIONS
Again i have written two contracts for the array version of the solution. Perhaps We should expect the deployment and execution cost to rise in comparison to the mapping solutions because we are storing data for lookups which going to be more costly in hindsight this is more so true the more our storage array grows and grows with mor eentries. The code below for my simple array solution is given

contract ArrayStorageSimple {
    
    
     struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] public newEntity;
  
    function addEntity(uint _data) public {
        
        Entity memory newentity; 
        newentity.data = _data;
        newentity._address = msg.sender;
        newEntity.push(newentity);
    }
    
    function updateEntity(uint _id, uint data) public{
    
        newEntity[_id].data = data;
    }
}

Now i tried to keep it as simple as possible for this solution and as a result it is pretty horrible. The addEntry( ) function is ok and does its job fine. However the sloppiness is in the update entry function. I am passing in an ID to look up. This might not seem so bad but remember that if we are logged in as some address then were going to want to modify the data of our address or msg.sender. However there is no way to know what index our data is logged in in our Entity array. This is a big problem.But just for simplicity i looked beyond this fact for my simple array solution. Now lets look at the gas costs

DEPLOYMENT
ad

Transaction cost: 297618 gas
execution cost: 185426 gas

As we can see in comparison with the simple mapping contract both the transaction and execution costs of deploying the contract are seem to be actually slightly cheaper by around 50000 gas for each which is quite a bit.

AddEntry( )
aad

Transaction cost: 83850 gas
execution cost: 62386 gas

UpdateEntry( )
aud

Transaction cost: 23904 gas
execution cost: 2312 gas

Now here is where we notice a much larger descrepancy between the costs of adding and updating entries for the array and mapping solutions. The transaction cost for the addEntry( ) function has nearly gone up 2 fold (40000) and the execution cost has also nearly doubled. This is a huge increase and again it comes down to the fact that at the end of our addEntry( ) function were are pushing this struct instance into an array. This is more costly. However the importtant thing to recognise is what are your needs? The mapping solution provides a much cheaper alternative but we can never look up specific instamces of our entities whereas the array solution allows us this privelage. So it all comes down to what are the requirments of your specific problem at hand.

We can see that the updateEntry( ) function actually costs considerably less. The update functions are very similar between the simple array and simply mapping solutions and the reason it does not cost as much here is to do with the fact that we only have one entry in our array therefore the array lookup to find that entry and modifiy its data is not too cumbersome. Thus handling and modifying data at the first index is not too costly. However if i were to have 100 entries in this array and try to modify the last one the rise in exectution and transaction cost would be much more signicicant.

BETTER MAPPING SOLUTION
Lastly we will look at a better version of my mapping solution which again, is not incredible but it does add an extra layer of security, namely again, the prevention of adding duplicate entries. Lets have a look at the code below and see if adding in duplicate prevention only rises the total gas cost marginally like was the case in my upgraded mapping solution.

contract ArrayStorageBetter {
    
    
     struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] public newEntity;
    address[] public addressArray;
    
    
    function addEntity(uint _data) public {
        
        for (uint i = 0; i < addressArray.length; i++) {
            require(addressArray[i] != msg.sender);
        }
        addressArray.push(msg.sender);
        
        Entity memory newentity; 
        newentity.data = _data;
        newentity._address = msg.sender;
    
        newEntity.push(newentity);
    }
    
    function updateEntity(address _address, uint data) public{
        
        if (_address != msg.sender) revert();
        uint id = 0;
        
        while (newEntity[id]._address != msg.sender) {
            id++;
            
        }
        newEntity[id].data = data;
    }
}

Now I know the assignment brief said only to use arrays on one contract technically i have done that although its not what the assignment wanted lol. One thing to notice here is now that our solution handles duplicate entries and also our contract requires that msg.sender is the entity instance being created for. The way ive handled duplication here is by creating an address array that keeps track of the addresses of previously created entity instances. Then at the beginning on the addEntry( ) function we run through a fir loop that checks if msg.sender is already in the address array if so then we know we have duplicates so we revert. Then the function continues the same as the simple array

Likewise in the updateEntry( ) function we want to require that the address were modifying is msg.sender so i have included a revert statement to handle this. Then i iterate through a while loop which keeps going until weve found msg.sender when it has we update the data. You might think what happens if msg.sender is not yet an instance, the while loop will never stop. Well the addition of the require statement handles that as the function cannot be called unless msg.sender is already an instance

Now comes the interesting part. This solution is obviously better than the last but the added level of security forces us to include loops which as you can imagine are going to drastically increase the gas costs because our solutions now run in linear time due to the loops

DEPLOYMENT
ArrayDeployment

Transaction cost: 126512 gas
execution cost: 105045 gas

As we can see in comparison with the simple array contract both the transaction and execution costs of deploying the contract have increased by around 5 fold gas costs which is a hige increase. Again this is to do with the addition of for loops which causes our solution to no longer run in constant time we now run in linear time which is slower. Thus the gas costs are more expensive Below are the costs for the two function addEntry( ) and UpdateEntry( )

AddEntry( )
ArrayAddEntry

Transaction cost: 126512 gas
execution cost: 105048 gas

UpdateEntry( )
ArrayUpdateEntry

Transaction cost: 31196 gas
execution cost: 8324 gas

As we can see the addition of the loops is making the cost and execution of these functions wayyy more expensive than that of both the simple array solution and also the mapping solution. So this begs the question, why use arrays to store data at all? Well it all comes down to the specific needs and requirements at the problem at hand. Each problem is different and thus requires different approaches to solve it. The nice thing about arrays is that we have a method whereby we can do look ups and get information on any entry. As we saw earlier this was not the case with mappings. One last thing to comment on is the drastic increase in gas costage that the addition of duplicate handling made in the better array solution in comparsion with that of the better mapping solution. Obviously there is ways to optimise this and here we are restricted to only two functions but nonetheless its something to consider when deciding what data structures to use

1 Like

It is much more expensive to work with the array in both the addEntity function and the updateEntity function. As more addresses are added, the difference between the two becomes even more pronounced, with every person added, another factor of complexity is added to to parsing the array.

pragma solidity 0.8.0;

contract SimpleMapping {
    
    struct Entity{
        uint data;
        address entityAddress;
    }
    
    mapping(address => Entity) public entityMap;
    
    function addEntity(uint _data) public {
        entityMap[msg.sender].data = _data;
    }
    
    function updateEntity(uint _data) public {
        entityMap[msg.sender].data = _data;
    }
    
    
}

pragma solidity 0.8.0;

contract SimpleArray {
    
    
    struct Entity{
        uint data;
        address entityAddress;
    }
    
    
    Entity[] public entityArray;
        
    function addEntity(uint _data) public {
        Entity memory newEntity = Entity(_data, msg.sender);
        entityArray.push(newEntity);
    }
    
    function updateEntity(uint _data) public {
        if (entityArray.length == 0) revert();
        for (uint i = 0; i < entityArray.length; i++) {
            if (entityArray[i].entityAddress == msg.sender) {
                entityArray[i].data = _data;
                break;
            }  
        }
    }
        
    }

    
    

1 Like
IStorageDesignPattern.sol
pragma solidity ^0.8.1;

interface IStorageDesignPattern{
    
    function addEntity(uint data) external;
    
    function updateEntity(uint newData) external;
}
Entity.sol
pragma solidity ^0.8.1;

struct Entity{
    uint data;
    address connectedAddress;
}
MappingStorageDesignPattern.sol
pragma solidity ^0.8.1;

import "./IStorageDesignPattern.sol";
import "./Entity.sol";

contract MappingStorageDesignPattern is IStorageDesignPattern{
    
    mapping(address => Entity) entities;
    
    function addEntity(uint data) external override{

        entities[msg.sender].data = data;
        entities[msg.sender].connectedAddress = msg.sender;
    
    }
    
    function updateEntity(uint newData) external override{
        entities[msg.sender].data = newData;
    
    }

}
ArrayStorageDesignPattern.sol
pragma solidity ^0.8.1;

import "./IStorageDesignPattern.sol";
import "./Entity.sol";

contract ArrayStorageDesignPattern is IStorageDesignPattern{
    
    Entity[] entities;
    
    function addEntity(uint data) external override{
        
        entities.push(Entity(data, msg.sender));

    }

    function updateEntity(uint newData) external override{
        
        for (uint i=0; i<entities.length; i++){
        
            if (entities[i].connectedAddress == msg.sender){
                
                entities[i].data = newData;
            }
        
        }
    
    }

}
addEntity() and updateEntity() gas costs

Mapping addEntity() had 62982 in transaction cost and 41454 in execution cost.
Mapping updateEntity() had 26979 in transaction cost and 5515 in execution cost.

Array addEntity() had 83814 in transaction cost and 62286 in execution cost.
Array updateEntity() had 31378 in transaction cost and 9914 in execution cost.

The array is a lot slower compared to mapping.

updateEntity() gas costs with 5 elements

updating with 5 elements in mapping had a transaction cost of 26979 and an execution cost of 5515. The same as with only one element.

updating with 5 elements in array had a transaction cost of 61850 and 40386 in execution cost. A lot more than with only one element.

1 Like
pragma solidity 0.8.0;

contract OnlyMapping{
    
    struct Entity{
        uint data;
        address _address;
    }
    
    mapping(address => Entity) public Entities;
    
    function addEntity(uint _data) public {
        Entities[msg.sender].data = _data;
        Entities[msg.sender]._address = msg.sender;
    }
    
    function updateEntity(uint _data) public {
        Entities[msg.sender].data = _data;
        
    }
}

pragma solidity 0.8.0;

contract OnlyArray{
    
    struct Entity{
        uint data;
        address _address;
    }
    
    Entity[] public Entities;
    
    function addEntity(uint _data) public returns (bool success) {
        Entity memory newEntity;
        newEntity._address = msg.sender;
        newEntity.data = _data;
        Entities.push(newEntity);
        return true;
    }
    
    function updateEntity(uint rowNumber, uint _data) public {
        Entities[rowNumber].data = _data;
    }
    
}

When executing the addEntity function, which design consumes the most gas (execution cost)? Is it a significant difference? Why/why not?

Only Mapping execution cost 41454 gas
Only Array execution cost 62585 gas

Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

Only mapping used total of 208,585 gas
Only Array used total of 254,346 gas

From what I understand array is more expensive because it need to add to a list in chronological order where mapping is instant ref without any list.

1 Like

The array contract had a gas fee of 62286 for adding an entity, while the mapping contract had a gas fee of 41450. The array obviously has a higher cost for adding entities by about 20%.

For the update entity function the execution cost for the Array implementation was about 4 time more than the mapping implementation. This is due to having to loop through the array to get the 5th indexed value while the map just returns the value of the key which is one operation compared to having to do it 5 times.

1 Like

Below is the Array Contract:

pragma solidity 0.8.0;

contract contrArray{
    
    struct Entity
    {
    address _address;
    uint data;
    }
    
Entity[] public EntityStructArray;
address[] public addressArray;

//Adding Addresses with numeric data to the array

function addEntity(address _address, uint entityData) public returns(bool status)
    {
        for(uint i=0; i < addressArray.length; i++)
        {
            require(addressArray[i]!=_address);
        }
        addressArray.push(_address);
        Entity memory newEntity;
        newEntity._address = _address;
        newEntity.data = entityData;
        EntityStructArray.push(newEntity);
        return true;
    }
    
  //Updating numeric data of the array linked to the specific index
  
    function updateEntity(uint _index, uint _data) public returns(bool status)
    {
        EntityStructArray[_index].data = _data;
        return true;
    }
    
   
    
}

Below is the Mapping Contract:

pragma solidity 0.8.0;

contract contrMapping{
    
    struct Entity
    {
        address _address;
        uint data;
        bool isKnown;
    }
    
    mapping(address => Entity) public entities;
   
    //Adding Addresses with numeric data to the mapping
    
    function addEntitiy(address _address,uint entityData) public returns(bool status)
    {
        require(!entities[_address].isKnown);
        entities[_address]._address = _address;
        entities[_address].data = entityData;
        entities[_address].isKnown = true;
        return true;
    }
    
    //Updating numeric data of the mapping linked to the specific address
    
    function updateEntity(address _address, uint entityData) public returns(bool status)
    {
        entities[_address].data = entityData;
        return true;
    }
    
}

Below is the comparison table for the analysis of the gas cost:

Screenshot 2021-05-12 at 7.05.48 PM

which design consumes the most gas (execution cost)? why/ why not?
Ans: Array function consumes more gas as array needs to be iterated through it’s elements.
Basic things like maintaining unique addresses comes at very high cost. It also needs to maintain 2 different arrays to fulfil the functionality.
Mapping is much simpler in nature when it comes to control the redundancy.

Which solution consumes more gas and why?
Ans: As per observation from above table we can easily conclude that Gas consumed by Array is much higher.
For the similar set of transactions on mapping we can save avg. 26% of the gas.
During update transaction it is identified that in array gas cost dropped by 92.28% & for mapping it’s dropped by 90.73%
Update function in Mapping consumes 11.86% lesser gas than array update function.

1 Like

My contracts:

mappingStorage.sol

Using the mapping as storage solution:

pragma solidity 0.8.0;

contract mappingStorage {

    struct Entity {
        uint data;
        address _address;
    }

    mapping(address => Entity) entitiyStruct;

    function addEntity(uint _data) public {
        entitiyStruct[msg.sender].data = _data;
        entitiyStruct[msg.sender]._address = msg.sender;
    }

    function updateEntity(uint _data) public {
        entitiyStruct[msg.sender].data = _data;
    }
}

arrayStorage.sol

Using the array as storage solution:

pragma solidity 0.8.0;

contract arrayStorage {
    
    struct Entity {
        uint data;
        address _address;
    }
    
    Entity[] entityStructs;
    
    function addEntity(uint _data) public {
        Entity memory newEntity;
        newEntity.data = _data;
        newEntity._address = msg.sender;
        entityStructs.push(newEntity);
    }
    
    function updateEntity(uint _data) public {
        for(uint i = 0; i < entityStructs.length; i++) {
            if(entityStructs[i]._address == msg.sender) {
                entityStructs[i].data = _data;
            }
        }
    }
}

1. When executing the addEntity function, which design consumes the most gas (execution cost)?

Mapping storage execution cost: 41454 gas
Array storage execution cost: 62386 gas

2. Is it a significant difference? Why/why not?

Yes, the mapping storage solution saves approximately 1/3 of the array storage solution costs.

3. Add 5 Entities into storage using the addEntity function and 5 different addresses. Then update the data of the fifth address you used. Do this for both contracts and take note of the gas consumption (execution cost). Which solution consumes more gas and why?

Mapping storage execution cost: 5515 gas
Array storage design: 20942 gas

The cost in this case is much higher in the array storage solution than in the mapping storage solution (almost 4x higher). This is most probably due to the looping necessary in to find the struct corresponding to the sender address in the array.

1 Like

Here are our results.

Mapping is more efficient. We think it is because this is a special Solidity data type and highly optimized.

Mapping

64433 - Addentity

30141 - Updateentity

Array

86065 - Addentity

31208 - Updateentity

1 Like

It is clear that the mapping solution is cheaper than the array solution.

The execution costs for the mapping stays uniform throughout (in my case 42422 gas), whereas the very first initialisation of the array (its zero index) is significantly higher than the remaining four (the first element cost 62299 gas and the rest each cost 47299 gas). Either way it’s more expensive to use the array. I’m not really sure why creating mappings are cheaper than adding elements to an array.

Changing data is also significantly more expensive with arrays. Mapping = 6480 gas and Dynamic_array = 22052. This is because with a mapping you just change call the data and change it (little computing), whereas with an array you first have to iterate through the entire array until you find the element you need - the further back your element lies (the larger the index), the more expensive each update will be - and then change it.

1 Like