Extending Preset ERC1155 exceeds maximum contract size

Hello,

While working with the ERC1155 present contract, I’ve exceeded the maximum size for a contract on Remix (24.5kB). Looking through the inheritance tree, it appears that there are duplications of code. FYI, the rectangles are files that are imported and inherited and the ovals are files that are imported, but not inherited. I haven’t included libraries.

My understanding is that while libraries are not included as part of the contract size, inherited contracts are. So, does the fact that the ERC1155 preset contract inherits ERC1155.sol (and its parent contracts) mulitple times contribute to the contract size. Same goes for context.sol.

Should I reorganize how the contracts inherit to remove duplications to decrease the contract size?

Also, is there any easy way to check the size of a contract or series of contracts?

I would appreciate any advice or comments. Cheers.

Sincerely,
Craig

1 Like

Hi Craig (@cjd9s),

I am sorry you are running into this issue.

The 0x6000 byte code creation limit was introduced in eip-170

The bytecode for inherited contracts is only included once even if they show up multiple times in the inheritance graph.

The OpenZeppelin Contracts libraries that you are using are included in your contract as we use internal library functions:

calls to internal functions use the internal calling convention, which means that all internal types can be passed and types stored in memory will be passed by reference and not copied. To realize this in the EVM, code of internal library functions and all functions called from therein will at compile time be included in the calling contract, and a regular JUMP call will be used instead of a DELEGATECALL .
From: https://solidity.readthedocs.io/en/latest/contracts.html#libraries

Have you tried using the optimizer? I was getting a reduction in code size on the ERC1155 Preset.

If you want the initial contract deployment to be cheaper and the later function executions to be more expensive, set it to --optimize-runs=1 .
From: https://solidity.readthedocs.io/en/latest/using-the-compiler.html

Are you able to share what else you are doing in your contract?

I haven’t got a great way of calculating bytecode size. I have been compiling the contract, copying the bytecode and calculating (length/2) -1

@cjd9s hi, just to add to @abcoathup’s response above, we thought maybe the error messages in the requires were responsible for a sizeable part of the code, so I quickly run some numbers. I compiled ERC1155PresetMinterPauser.sol both as is and replacing all require error messages with empty strings. I did this with and without the optimizer, for 200 runs. I get:

no optimizer:

original: 17413 bytes
no error messages: 16257 bytes
(~7% reduction)

with optimizer:

original: 11981 bytes
no error messages: 10580 bytes
(~11% reduction)

So it does look like there’s a ~7-11% reduction in code size if we drop these messages, but larger gains can be obtained from using the optimizer. Hope this helps!

1 Like

Hi Andrew,

As always, your informative response is very helpful.

That’s good to know about the inherited contracts only being included once.

After reading the link to the Solidity documents, I know understand that internal functions in libraries are included in the calling contract.

I have been using Truffle to compile my contracts. However, I installed solcjs and eventually got it to work.The problem that I had was that it couldn’t locate the imported files. The key was to specify the base-path (e.g. solcjs --bin --base-path … ./ERC1155PresetMinterPauser.sol OR solcjs --bin --optimize-runs=1 --base-path … ./election.sol). I then opened the bin files in notepad and wrote down the number of columns (i.e. hex characters) displayed at the bottom of the window.

I found that the number of characters were the same for all of the files whether I compiled the PresetERC1155MinterPauser.sol by itself or as part of Election.sol. I also found that the number of characters were the same whether I set --optimize-runs=1 or left out that parameter. Perhaps I was doing something wrong.

File Hex Characters Bytes Type
Election 57,345 28,671 Contract
ERC165 1,313 655 Contract
SafeMath 267 132 Library
PresetERC1155MinterPauser 40,359 20,178 Contract
ERC1155 19,689 9,843 Contract
Address 267 132 Library
EnumerableSet 267 132 Library
Pausable 455 226 Contract
Access Control 0 Abstract
Context 0 Abstract
IERC165 0 Interface
ERC1155Burnable 0 Abstract
ERC1155Pausable 0 Abstract
IERC1155 0 Interface
IERC1155Metadata 0 Interface
IERC1155Receiver 0 Interface

I have a few questions based on this data.

  • Could you briefly explain or point me in the right direction for an explanation of why the interfaces and abstract contracts did not have any bytecode?

  • If I add up the number of hex characters in all of the files except for mine, I get 62,167. My file (Election) by itself is 57,345. Does my file include everything it has inherited and libraries? And, if so, why is the number of hex characters less in my file than in the summation of those files individually?

  • The number of hex characters for PresetERC1155MinterPauser is 40,359 which is about 20,178 bytes. So, if I inherit this contract, I have 24,576 - 20,178 = 4,398 bytes for my contract. Is that correct?

As for sharing my contract, I’m going to post that in a separate reply.

Again, I really appreciate all of your help and advice. Thank you.

Sincerely,
Craig

1 Like

Hi Alejo,

Thanks very much for the additional information. You’ll see from my post just now that I didn’t see any reduction in code size when I set optimize-runs=1. Either there’s something wrong with my command or my method for interpreting bytecode file size is faulty. Any thoughts?

Out of curiosity, what was the number of bytes with ERC1155PresetMinterPauser.sol with the existing error message strings? As you can see from my post just now, I get 40,359 hex characters (or 20,178 bytes).

Again, I appreciate your input and assistance. If you find the time to respond, that would be great. If you have better things to do, no problem. Cheers.

Sincerely,
Craig

1 Like

Hi Andrew,

As promised, I have copied my code below. I’m aware that there are things that I could do to make the bytecode smaller (e.g. I have duplicated inherited functions in my contract, I could get rid of the modifiers and just put require statements in the functions). However, I decided that it’s better to leave the contract as-is and get you to give me the full feedback on all the things I’ve done wrong and how it can be improved. I would rather be flayed on the internet and learn a lot quickly than stumble along by myself at a slow pace. If that’s too much work, just give me some basic guidelines and I’ll take it from there.

I’m working with a friend who is going to be handling the web3.js side, but he has only just started to get involved. Once we start to get a better understanding of how the front end will interact with the contract, I’m sure we’ll end up changing a lot.

FYI, the plan is to implement a contract factory design so that the application creates a new contract for each election.

Once again, thanks for all of your help. It’s all gold.

Your biggest fan,
Craig

pragma solidity ^0.7.0;

import "../node_modules/@openzeppelin/contracts/presets/ERC1155PresetMinterPauser.sol";

contract Election is ERC1155PresetMinterPauser {

    using SafeMath for uint256;

    

    /// @dev numberOfOffices will ultimately be passed in to the constructor function by the contract factory.

    /// @dev As the name suggests, it details how many offices will be contested in the election.

    uint private numberOfOffices;

    /// @dev We need to track total number of candidates in the election so we can break down the Candidate structs into

    /// @dev individual arrays at the end of the election in the returnVotesAndDetermineWinner function.

    uint private totalCandidates;

    /// @dev electionName will be passed in to the constructor function by the contract factory.

    /// @dev electionName is the name of the particular election

    string public electionName;

    

    /// @dev State are different states for controlling which functions can be used during different phases of the election

    enum State { Populating, Voting, Ended }

    /// @dev Declaring a gobal State variable for use in the contract

    State private state;

    /// @dev uint array for the officeId (e.g. 0 is for president, 1 is for VP, 2 is for secretary, etc.)

    /// @dev for our purposes, we will populate the array with sequential numbers (e.g. if 5 offices in election, array will be [0,1,2,3,4])

    uint[] ids;

    /// @dev uint array for number of tokens (aka votes) per each officeId. 

    /// @dev As every voter will receive 1 vote for each office, this will be 1 for each element (e.g. if 5 offices, array will be [1,1,1,1,1])

    /// @dev The length of the array will be equal to the ids array (so that each officeId has a corresponding number of votes per voter)

    uint[] amounts;

    /// @dev struct to hold data on each candidate

    /// @dev name (obvious), officeId is the office for which they are running, candidateAddress is their Ethereum address,

    /// @dev arrayIndex is their index number in the officeToCandidate mapping, votes is their number of votes(*** 'votes' probably not needed)

    struct Candidate {

        string name;

        uint officeId;

        address candidateAddress;

        uint arrayIndex;

        uint votes;

    }

    /// @dev mapping to store the candidates for each office. E.g. the initial uint is officeId so officeToCandidate[1] will resolve to an array of

    /// @dev Candidate structs associated with officeId 1.

    mapping (uint => Candidate[]) public officeToCandidate;

    /// @dev mapping to indicate if a particular candidate is running for a particular office. Needed to ensure that

    /// @dev voters can only use a particular token for the corresponding officeId. E.g. the initial uint is officeId so officeToCanddiate[1] will 

    /// @dev resolve to a mapping of addresses to bools associated with officeId 1. So, officeToCandidate[1][0x123ABC] will resolve to true or false

    /// @dev which indicates if that address is or isn't running for that particular office

    mapping (uint => mapping (address => bool)) private officeToCandidateToBool;

    

    /// @dev modifier to determine if the contract is in the correct state (Populating, Voting, or Ended).

    modifier inState(State _state)

    {

        require(state==_state, "Invalid State.");

        _;

    }

    /// @dev modifier to determine if the message sender has Minter role

    modifier isMinter()

    {

        require(hasRole(MINTER_ROLE, _msgSender()), "Caller is not a minter");

        _;

    }

    

    /// @dev modifier to determine if the message sender has Pauser role

    modifier isPauser()

    {

        require(hasRole(PAUSER_ROLE, _msgSender()), "Caller is not a pauser");

        _;

    }

    /// @dev modifier to determine if the message sender has Admin role

    modifier isAdmin()

    {

        require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin");

        _;

    }

    /// @dev constructor function that initializes some of the state variables using arguments and sets inital state to 'Populating'

    /// @dev also passing a string to the ERC1155PresetMinterPauser parent contract. As we're not using a uri at the moment, I've set it to 'uri'.

    /// @dev There's a loop to populate the ids and amounts arrays based on the number of offices in the election. If there are five offices in

    /// @dev the election ids will be [0,1,2,3,4] and amounts will be [1,1,1,1,1].

    /// @param _numberOfOffices is required so the contract knows how many offices are in the election

    /// @param _electionName is the name of the election (e.g. election 2021)

    constructor(uint _numberOfOffices, string memory _electionName) ERC1155PresetMinterPauser("uri") 

    {

        state = State.Populating;

        numberOfOffices = _numberOfOffices;

        electionName = _electionName;

        for (uint i; i < _numberOfOffices; i++) {

            ids.push(i);

            amounts.push(1);

        }

    }

    /// @dev function to add candidates to contract which can only be done during while contract is in initial 'Populating' state and only

    /// @dev by an admin.

    /// @dev arguments are placed into a temporary Candidate struct which is then (along with the index position in the array) pushed 

    /// @dev into a Candidate array located in the officeToCandidate mapping. Also, the bool is set to 'true' in the officeToCandidateBool

    /// @dev mapping for the designated _officeId (e.g. 0=Preseident, 1=VP, 2=Secretary) and _canddiateAddress.

    /// @dev Lastly, the totalCandidates variable is incremented by one.

    /// @param _candidateAddress is the Ethereum address for the canddiate to be added

    /// @param _officeId is the number associated with a particular office in the election (e.g. 0=Preseident, 1=VP, 2=Secretary)

    /// @param _name is the name of the candidate

    function addCandidate(address _candidateAddress, uint _officeId, string memory _name) 

    public

    isAdmin

    inState(State.Populating)

    {

        Candidate memory tempCandidate;

        tempCandidate.officeId = _officeId;

        tempCandidate.name = _name;

        tempCandidate.candidateAddress = _candidateAddress;

        tempCandidate.arrayIndex = officeToCandidate[_officeId].length;

        officeToCandidate[_officeId].push(tempCandidate);

        officeToCandidateToBool[_officeId][_candidateAddress] = true;

        totalCandidates = totalCandidates.add(1);

    }

    /// @dev function to remove candidates to contract which can only be done during while contract is in initial 'Populating' state

    /// @dev and only by an admin.

    /// @dev arguments are placed into a temporary Candidate struct which is then (along with the index position in the array) pushed 

    /// @dev into a Candidate array located in the officeToCandidate mapping. Also, the bool is set to 'true' in the officeToCandidateBool

    /// @dev mapping for the designated _officeId (e.g. 0=Preseident, 1=VP, 2=Secretary) and _canddiateAddress.

    /// @dev Lastly, the totalCandidates variable is incremented by one.

    /// @param _officeId is the number associated with a particular office in the election (e.g. 0=Preseident, 1=VP, 2=Secretary)

    /// @param _arrayIndex is the index position of the candidate in the officeToCandidate mapping array marked for removal 

    function removeCandidate(uint _officeId, uint _arrayIndex) 

    public

    isAdmin

    inState(State.Populating)

    {

        require (_arrayIndex < officeToCandidate[_officeId].length);

        // set candidate address to false for this officeId

        officeToCandidateToBool[_officeId][officeToCandidate[_officeId][_arrayIndex].candidateAddress] = false;

        

        // copy last element into array index marked for deletion spot

        officeToCandidate[_officeId][_arrayIndex] = officeToCandidate[_officeId][officeToCandidate[_officeId].length - 1];

        // change Candidate struct to reflect new array position

        officeToCandidate[_officeId][_arrayIndex].arrayIndex = _arrayIndex;

        // pop off last element which was copied into 

        officeToCandidate[_officeId].pop();

        totalCandidates = totalCandidates.sub(1);

    }

    /// @dev Previous to deployment of election contract, we will have created all of the Ethereum accounts for all of the voters and 

    /// @dev candidates. So, we will pass in the voter's Ethereum address and mint tokens in the quantity specified in the amounts array

    /// @dev for each officeId in that array to that address. Only the Minter can use this function and only while in the 'Populating' state. 

    /// @dev ids is the array of officeIds (aka offices in the election).

    /// @dev amounts is the array of number of tokens (aka votes) per officeId

    /// @dev "0x00" is a placeholder because the function requires a data argument that we're not using

    /// @param _voterAddress is the Ethereum address for a particular voter

    function addVoterTokens(address _voterAddress)

    public

    isMinter

    inState(State.Populating)

    {

        _mintBatch(_voterAddress, ids, amounts,"0x00");

    }

    /// @dev To remove tokens for all officeIds and amounts specified in the ids and amounts arrays.

    /// @dev Exact opposite of addVoterTokens

    /// @dev ids is the array of officeIds (aka offices in the election).

    /// @dev amounts is the array of number of tokens (aka votes) per officeId

    /// @param _voterAddress is the Ethereum address for a particular voter

    function removeVoterTokens(address _voterAddress)

    public

    isMinter

    inState(State.Populating)

    {

        _burnBatch(_voterAddress, ids, amounts);

    }

    /// @dev function used to transfer tokens from voters to candidates. Can be called by anyone when contract is in Voting state.

    /// @dev This function can be paused by the Pauser to stop voting in case of an emergency.

    /// @param _officeId is the number associated with a particular office in the election (e.g. 0=Preseident, 1=VP, 2=Secretary)

    /// @param _candidateArrayIndex is the index position of the Candidate struct in the Candidate array in the officeToCandidate mapping

    function vote(uint _officeId, uint _candidateArrayIndex)

    public

    whenNotPaused()

    inState(State.Voting)

    {    

        // check that the candidate being voted for is registered as a candidate for the particular office

        require (officeToCandidateToBool[_officeId][officeToCandidate[_officeId][_candidateArrayIndex].candidateAddress] == true);

        safeTransferFrom(_msgSender(), officeToCandidate[_officeId][_candidateArrayIndex].candidateAddress, ids[_officeId], amounts[_officeId], "0x00");

    }

    /// @dev fuunction that starts the election by changing the state from 'Populating' to 'Voting' which enables the vote function

    /// @dev only admin can call this function and only when election is in the 'Populating' state

    function startElection()

    public

    isAdmin

    inState(State.Populating)

    {

        state = State.Voting;

    }

    /// @dev Function that determines the winner (or draw) for each office and returns the number of votes for each candidate.

    /// @dev Can only be called by the admin and only when the election is in the 'Ended' state. 

    /// @dev Private function so that it can only be called wihtin this contract (by the endElection function)

    /// @return returns six arrays (officeIds, candidateAddresses, arrayIndices, votes, winners, drawOrNots)

    /// @return the values in the officeIds, candidateAddresses, arrayIndices, votes arrays line up

    /// @return while the index values for the winners and drawOrNot arrays are the officeIds 

    function returnVotesAndDetermineWinners() 

    private

    view

    isAdmin

    inState(State.Ended)

    returns (uint[] memory, uint[] memory, uint[] memory, uint[] memory, bool[] memory)

    {

        // These are temporary memory arrays that exist solely to allow me to break down into single arrays to return

        uint[]    memory officeIds = new uint[](totalCandidates);

        uint[]    memory arrayIndices = new uint[](totalCandidates);

        uint[]    memory votes = new uint[](totalCandidates);

        uint[]    memory winners = new uint[](numberOfOffices);

        bool[]    memory drawOrNots = new bool[](numberOfOffices);

        // A temporary index for populating the individual arrays in the nested loop below

        uint tempCandidateIndex;

        // Outer loop is for cycling through all of the offices in the election 

        for (uint office = 0; office < numberOfOffices; office = office.add(1))

        {

            // A temporary index for tracking the array index of the candidate with the highest number of tokens (votes)

            uint currentWinnerIndex = 0;

            // Inner loop is for cycling through all of the candidates running for the various offices

            for (uint candidate = 0; candidate < officeToCandidate[office].length; candidate = candidate.add(1))

            {

                officeIds[tempCandidateIndex] = officeToCandidate[office][candidate].officeId; // Needed to identify candidate in officeToCandidate mapping

                arrayIndices[tempCandidateIndex] = officeToCandidate[office][candidate].arrayIndex; //Needed to identify candidate in officeToCandidate mapping

                votes[tempCandidateIndex] = balanceOf(officeToCandidate[office][candidate].candidateAddress,office); // Not needed? Can query directly using balanceOf?

                // cycling through candidates votes to find the highest number

                if (votes[tempCandidateIndex] > votes[currentWinnerIndex])

                {

                    currentWinnerIndex = officeToCandidate[office][candidate].arrayIndex;

                }

                // if current number isn't higher than biggest number, check to see if they're equal

                else if (votes[tempCandidateIndex] == votes[currentWinnerIndex])

                {

                    drawOrNots[office] = true;

                }

                // Increment tempTotal candidates so the next candidate data will be placed in the right index in the various arrays

                tempCandidateIndex = tempCandidateIndex.add(1);

            }

            // Highest number of votes at the end of a particular office loop must be the winner for that office (unless there's a draw)

            winners[office] = currentWinnerIndex;

        }

        

        // return the various individual arrays

        return (officeIds, arrayIndices, votes, winners, drawOrNots);

    }

    /// @dev function to end the election by changing state from 'Voting' to 'Ended'

    /// @dev Also, calls returnVotesAndDetermineWinner to return number of votes and determine winner

    /// @dev can only be called by the Admin and when the contract state is 'Voting'

    /// @return returns six arrays (officeIds, candidateAddresses, arrayIndices, votes, winners, drawOrNots) passed from private function

    /// @return the values in the officeIds, candidateAddresses, arrayIndices, votes arrays line up

    /// @return while the index values for the winners and drawOrNot arrays are the officeIds 

    function endElection()

    public

    isAdmin

    inState(State.Voting)

    returns(uint[] memory, uint[] memory, uint[] memory, uint[] memory, bool[] memory)

    {

        state = State.Ended;

        return returnVotesAndDetermineWinners();

    }

}
1 Like

Hi Andrew,

There seems to be a minor issue with displaying text. In my earlier post, I wrote "
(e.g. solcjs --bin --base-path … ./ERC1155PresetMinterPauser.sol OR solcjs --bin --optimize-runs=1 --base-path … ./election.sol) ". There should only be two periods after --base-path, not three. When I go to edit the post, there are only two. However, it displays three. FYI. Cheers.

Sincerely,
Craig

1 Like

Hi @cjd9s,

I used Truffle and with the optimizer enabled the contract was under the size limit, without the optimizer Truffle complained about the size.

  compilers: {
    solc: {
      version: "0.7.0",    // Fetch exact version from solc-bin (default: truffle's version)
      // docker: true,        // Use "0.5.1" you've installed locally with docker (default: false)
      settings: {          // See the solidity docs for advice about optimization and evmVersion
        optimizer: {
          enabled: true,
          runs: 1
        },
      //  evmVersion: "byzantium"
      }
    },

I couldn’t find any documentation on why Interfaces and Abstract contracts have 0 bytes, I assume it is because they aren’t fully implemented and we can’t deploy.

I haven’t got an easy way of calculating contract sizes so will have to come back to you on that.

I haven’t played with solc direct (I have run into file path issues in the past).

Hi @cjd9s,

We can use the following to determine the contract size:

$ jq '.deployedBytecode | length / 2 - 1' build/contracts/Election.json

I compiled with optimizer disabled:

$ npx truffle compile

Compiling your contracts...
===========================
> Compiling ./contracts/Election.sol
...
    /home/abcoathup/projects/forum/craig/contracts/Election.sol:6:1: Warning: Contract code size exceeds 24576 bytes (a limit introduced in Spurious Dragon). This contract may not be deployable on mainnet. Consider enabling the optimizer (with a low "runs" value!), turning off revert strings, or using libraries.
contract Election is ERC1155PresetMinterPauser {
^ (Relevant source part starts here and spans across multiple lines).

> Artifacts written to /home/abcoathup/projects/forum/craig/build/contracts
> Compiled successfully using:
   - solc: 0.7.0+commit.9e61f92b.Emscripten.clang

The size is 24771:

$ jq '.deployedBytecode | length / 2 - 1' build/contracts/Election.json
24771

Compiling with the optimizer enabled and runs set to 1

$ npx truffle compile

Compiling your contracts...
===========================
> Compiling ./contracts/Election.sol
...
> Compiled successfully using:
   - solc: 0.7.0+commit.9e61f92b.Emscripten.clang

The contract size is reduced:

$ jq '.deployedBytecode | length / 2 - 1' build/contracts/Election.json
17278

Hi @cjd9s,

I wanted to check how you got on with your ERC1155?

Hi Andrew @abcoathup,

I appreciate the unprompted follow up. Excellent engagement. I’ve been out of action for a few days and have only now had a good chance to digest what you’ve previously written.

I discovered that, as you surmised, because inteface and abstract contracts have unimplemented functions, they can’t be compiled. Hence, no bytecode. (https://medium.com/upstate-interactive/solidity-how-to-know-when-to-use-abstract-contracts-vs-interfaces-874cab860c56)

After some playing around, I got the jq to work although I needed to use jq-win64.

Command Prompt
jq-win64 “.deployedBytecode | length / 2 - 1” build/contracts/Election.json

Visual Studio Code Terminal (powershell)
./jq-win64 “.deployedBytecode | length / 2 - 1” build/contracts/Election.json

Also, and this is probably related to why I couldn’t get different bytecode values when using solcjs and examining the number of bytecode characters in Notepad, I had to ensure that I used ’ truffle compile --all’ to ensure that the election contract recompiled when I changed the optimizer settings for the solc compiler in truffle-config.js.

FYI, I have copied my results below for different optimizer settings:

  • False = 24883
  • True, 2000 = 17983
  • True, 1000 = 17742
  • True, 500 = 17569
  • True, 200 = 17488
  • True, 1 = 17278

We get different results bytecode sizes for the optimizer being set to false, but the same when set to true and runs set to 1. I don’t know why it’s different, but I’m not worried about it.

As the contract is now well within the maximum size, I’m happy for the moment. If you have some extra time, it would be great if you could give me some general advice for the election contract that I posted. If not, no worries. You’ve already done plenty.

If you have even more extra time, could you talk a bit about what to do if the contract is too big to deploy and optimization isn’t an option. Would I deploy multiple contracts and have them call from each other? Delegatecall? Do you have any good sources on where to read up on this topic? Again, if you have enough on your plate, don’t worry about it. I’m so thankful for all the help that you’ve already given.

You’re the man. Cheers.

Sincerely,
Craig

1 Like

Hi @cjd9s,

Some initial thoughts:

You should be able to import as

import "@openzeppelin/contracts/presets/ERC1155PresetMinterPauser.sol";

Do you need the candidate name on-chain? Could you have candidate metadata offchain?

You could split the Election contract into voting tokens, candidate contracts and an election contract.

If there are rules about who you can vote for, then the candidate contracts could implement IERC1155Receiver and reject transfers which didn’t match the conditions.

You should assume that a dapp (and anything interacting with the smart contract directly) will call safeTransferFrom rather than vote. If you need to enforce any logic (such as only voting for a specific office, or not transferring votes to someone else to vote on your behalf), then you can use the _beforeTokenTransfer hook.

I would look to see if you need all the for loops. At some point the number of offices and candidates could result in running out of gas. You could do the equivalent loops off chain and call a transaction to set each winner which overrides the current winner if they have more votes.


I suggest getting opinions from different people on your design. Interested to hear what other people might think.

If you want more feedback, I suggest creating a new topic.

Ideally you won’t get to that situation.

You could look at breaking up your functionality into multiple contracts and/or libraries (a potential breakdown above). You could also consider what could be done off chain. e.g. remove the for loops.

Hi Andrew (@abcoathup),

I trust that you are well.

As always, your help is appreciated and extremley valuable. As I’ve said before, if you ever have something else to do, don’t hesitate to tell me to get lost. I know I’ve already taken a lot of your time. Until then, I’m going to keep coming back with questions.

I have removed the candidate name from the contract. However, after some consideration, I’m concerned that relying on the Ethereum address or position (i.e. officeId and array index) in the officeToCandidate mapping (uint => Candidate) as the unique identifier could be problematic. What if the overlying JavaScript application is somehow compromised such that candidate’s Ethereum addresses are switched (i.e. people think they’re voting for candidate A, but they’re actually voting for candidate B)? Would that be a good argument for having some sort of personally identifying information written into the blockchain?

Splitting into more tightly defined contracts sounds like a good idea. Would the primary reason for that be organizational? Or are there functional reasons (beyond contract size) why splitting would be helpful? Even if it was organziational, I like the idea. How to actually implement multiple contracts and how they interact is something with which I need to gain a better understanding.

I printed out IER165, ERC165, IERC1155Receiver, and ERC1155Receiver to try to understand exactly how they work. As IERC1155Received is an interface contract and ERC1155Receiver is an abstract contract, I would, as you say, have to implement the functions (onERC1155Received and onERC1155BatchReceived). So, again as you say, I would code (into a child contract) whatever the rules are for who a voter can vote for like you can only use a ‘president’ vote to vote for a presidential candidate. I think I understand this. Although, reading ahead, you talk about logic related to voting for a specific office in the _beforeTokenTransfer hook. What would be the difference in logic implemented in IERC1155Receiver and _beforeTokenTransfer?

I understand that I need to use safeTransferFrom instead of vote. In _beforeTokenTransfer, I could check the officeToCandidateToBool mapping (uint => mapping (address => bool)) to ensure that the address to which the voter wants to send the token is actually a candidate for that office. I’d also like to ensure that the contract hasn’t been paused and that the contract is in the correct state. Unfortunately, I don’t define the officeToCandidateToBool mapping
and inState modifier until the most derived election contract. And isPaused, located in the Pausable contract, is not inherited by the ERC1155 contract where _beforeTokenTransfer is found. Should I just move these things up the inheritance hierarchy so that they’re available to ERC1155? Or are there other things to consider?

Going back to the ERC1155Receiver contract, there was something to which I couldn’t find an answer using Google. In that abstract contract, the constructor is defined. It seems to, well, I’m not sure what it’s doing. The _registerInterface function is invoked which changes the boolean to true for a particular bytes value that represents a particular interface. However, the argument for that function uses ‘ERC1155Receiver(0)’. What does that mean?

Also, I understand that the remainder of the line means that it takes the ABI function selectors (first four bytes of the call data for a function call which specifies that function) of the two functions from the IERC1155Receiver interface contract and XORs them together. What is the purpose of XORing the two function selectors together? Why not just assign a bytes value as is done in the ERC165 contract (bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7)?

abstract contract ERC1155Receiver is ERC165, IERC1155Receiver {
constructor() {
_registerInterface(
ERC1155Receiver(0).onERC1155Received.selector ^
ERC1155Receiver(0).onERC1155BatchReceived.selector
);
}
}

Of course, you are correct about the loops. I removed the initial loops in the constructor that calculated the ids and amounts arrays using the numberOfOffices variable passed into the constructor. I replaced them by passing in the ids and amounts arrays as constructor parameters. As for the loops in the returnVotesAndDetermineWinners function, I have no doubt that you are correct. However, I need to build a basic JavaScript interface to get a better understanding of how they’re going to interact. It’s certainly on my mind and I’ll get to that when I can.

Thank you so much for everything so far. Cheers.

Sincerely,
Craig

1 Like

Hi @cjd9s,

Responding to this post is on my todo list. Sorry I haven’t got back to you sooner.

Hi Andrew (@abcoathup),

No worries. Any responses that I receive from you are gravy (i.e. bonus, extra).

Although, now that you’ve prompted me, this is a good opportunity for me to describe what I’ve learned since that last post. Let’s start with IERC1155Receiver contract.

The IERC1155Receiver interface and ERC1155Receiver abstract contracts are used to determine if minting and transferring tokens to contract accounts is permitted. This is done through the _doSafeBatchTransferAcceptanceCheck or _doSafeTransferAcceptanceCheck functions found at the end of the minting and transfer functions in the ERC1155 contract. In _doSafeBatchTransferAcceptanceCheck or _doSafeTransferAcceptanceCheck, the function confirms if the account is a contract and, if it is, invokes the onERC1155BatchReceived function found in the IERC1155Receiver contract and implemented in a child contract. If that function returns the ABI function selector (first four bytes of the call data for a function call which specifies that function) of onERC1155BatchReceived, then the transfer is permitted. Otherwise, the transaction is reverted. The implementation of onERC1155Batchreceived would determine the conditions under which the function would return the correct ABI function selector or not.

It’s tough for me to imagine how to use this because I haven’t yet separated out a candidates or tokens contract. So, I’m still not entirely clear on how everything would interact. However, I think the important point to note for now is that the IERC1155Receiver contract is handling transfers of tokens between contracts and occurs after the state has changed. Of course, if the conditions aren’t satisfied after the state change, the whole transaction will revert. The _beforeTokenTransfer function, although using the same parameters, is slightly different. It is used within a single contract and is invoked in the minting, transferring, and burning functions before the state change.

So, to summarize, the IERC1155Receiver contract is for inter-contract token transfers based on conditions after the state change. On the other hand, the _beforeTokenTransfer function is for intra-contract token transfers (within the token (owner to amount) mapping) before the state change. Does that sound right? Did I miss any salient points?

I recently purchased ‘Building Ethereum Dapps’ by Roberto Infante. It’s been useful in getting my head around interface and abstract contracts. While going through it, he intializes a contract using ‘new ReleaseableSimpleCoin(0);’ So, I undersand that ERC1155Receiver(0) is initializing that particular contract in ERC1155Receiver abstract contract. Is it initializing itself or a future implemented version? It would be weird if it’s initializing itself, right? Does the 0 argument represent the 0 address? I couldn’t find anything on this in Solidity docs. Do you have a resource that explains this functionality?

That’s it for the update to my last post. I’d like to add one more question, if I may. I believe that interface contracts serve two purposes.

  1. To create an object through which you can interact with another deployed contract. In this example, you would import the interface contract, but not inherit it. It would just be available. e.g.

OtherContractName variable1 = OtherContractName(ethereum address);
variable1.someFunction();

OR

In this example, you would also need the full implementation of the interface contract (available, but not inherited) so you could deploy it.

OtherContractName variable1 = new OtherContractName(0);
variable1.someFunction();

  1. To set a template for a standard implementation. In this example, you would inherit the interface contract and implement all of the functions to meet the minimum standard.

Is that right? Am I missing anything?

I must sound like a broken record, but your help is incredibly helpful. Thanks for your time. Cheers.

Sincerely,
Craig

1 Like

After doing any optimizations if your contract will still exceed the max contract size or you are concerned that it might in the future after upgrades then consider using EIP-2535 Diamond Standard which is a contract architecture that works with ERC1155 and solves the max contract limit.

ERC1155 suggests using EIP-2535 Diamond Standard:

To alleviate the need to emit events when changing contract address, consider using the proxy pattern, such as described in EIP-2535. This will also have the added benefit of providing a stable contract address for users.