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
Transaction cost: 396740 gas
execution cost: 258700 gas
AddEntry( )
Transaction cost: 27766 gas
execution cost: 6302 gas
UpdateEntry( )
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
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
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
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( )
Transaction cost: 83850 gas
execution cost: 62386 gas
UpdateEntry( )
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
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( )
Transaction cost: 126512 gas
execution cost: 105048 gas
UpdateEntry( )
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