Layout of components in a contract

Hi there, just wondering if the ordering of components like, events, state variables, constructor, modifier, public functions, private functions matters in writing a smart contract.

I’ve seen contracts like PaymentSplitter put event in the first chunk, then state variables, then constructor. I’ve also seen contracts like TimelockController put state variables first, then events, then constructor, modifier, etc.

Wondering if there is a framework of thinking about the layout. Thanks.

It matters only for state variables. Their addresses in storage are based on that order (see Layout of State Variables in Storage).

The way you order function/constructor/modifier/event/error/struct/enum definitions within a contract is just a matter of style and readability.

For modifiers only the order in which they are invoked on a function definition matters. Same as the multiple inheritance order in case of contracts.

2 Likes

I’ve seen contracts like PaymentSplitter put event in the first chunk, then state variables, then constructor. I’ve also seen contracts like TimelockController put state variables first, then events, then constructor, modifier, etc.

Here's the layout recommended by the official docs: Style Guide > Order of Layout. And solhint's order, which is pretty much the same. No one is forced to follow it of course so you'll see various other conventions in the wild.

3 Likes

Thanks @cameel . Great to hear from someone in the Solidity Compiler team.

A side question for you. Why is reading state variables in a smart contract by another smart contract charging gas fees? And the test function below cannot be labeled as a view function, since it is potentially changing state variables. This is a common error for reading from other contracts, and I’m wondering why this is the case. Thanks.

contract Test{
    function test() public returns(uint256) {
        return ISomeStorage(addr).getVal();
    }
}
interface ISomeStorage {
    function getVal() external returns(uint256);
}

Thanks for the link to the Style Guide, probably going to be using that one more often haha.

If you don’t mind me asking, do you think that it (the styling) will change much in the future?

I think for the view function, it means you only read state and will not modify any state, and for pure function, it means not only you will not read any state, but will not modify any state. And if you just directly call view or pure function, it will not cost any gas.
There is a method estimate to calculate how much gas you will cost when you get or set data from contract.

BTW, if a view function is internally called inside a non-view function, it will cost gas, cause this transaction tries to write data to modify the state.

1 Like

Why is reading state variables in a smart contract by another smart contract charging gas fees?

Anything done on-chain costs gas. Otherwise you could spam the network with free transactions that use up resources. This has actually been a big problem in the past and led to repricing of some opcodes. Calling view functions off-chain is free only because it does not involve sending a transaction - it's a local operation performed by your own node so it really does not cost the network anything.

But if you're actually just asking why it's so expensive compared to your own storage variables, note that you can't actually read directly from another contract's storage (well, there's DELEGATECALL but that's an entirely different story). If you declare a public state variable then the compiler actually generates a getter function for you and you can only read that variable from another contract by making an external call to that getter. This is a STATICCALL to make the call + SLOAD to read the variable in the contract that owns it + all the overhead of encoding the parameters (if any) and decoding the result. That's a lot more than the single SLOAD needed to read your own variable.

And the test function below cannot be labeled as a view function, since it is potentially changing state variables. This is a common error for reading from other contracts, and I’m wondering why this is the case.

The function accesses the addr state variable (was it meant to be a state variable? you did not declare it as a parameter). view functions cannot do that.

But even if it were a parameter, the function also performs an external call to a non-view function. That's also considered a state change.

If you fix these two things, you'll be able to make it view:

contract Test {
    function test(address addr) public view returns(uint256) {
        return ISomeStorage(addr).getVal();
    }
}
interface ISomeStorage {
    function getVal() external view returns(uint256);
}
2 Likes

Hard to say. I would not expect radical changes because there isn’t all that much activity around it. My personal opinion is that it should really be a community effort with most input coming from people actually using the language in production on a daily basis. There just isn’t a lot of such feedback currently (PRs are always welcome though). I think that people actually rely more on tools like solhint.

It’s also always possible that someone will not be satisfied with the official style and will create his own guide which suddenly catches on.

2 Likes

Hi @cameel , I guess you must have a good overview of programming languages in the industry for different chains, such as Mokoto for DFinity, PyTeal for Algorand, Rust for Solana, C++ for EOS, etc. What is your general opinion about other languages for smart contract programming? I once asked a developer at Solana about if they support Solidity, and he told me he was working on rolling out a solidity-based Uniswap, not everything though. Is there a compatibility issue between a language and a chain?

@cameel Hi there. I took another look and got to realize two versions can both pass the compiling checks with no warnings or errors. The following snippet is the normal version, and a slightly modified version (due to an error that the view modifier is forgotten) below it also works fine. The normal version, as you mentioned, does not require gas, yet the modified version does require gas.

// SPDX-License-Identifier: MIT

pragma solidity 0.8.4;

contract Template {
    uint256 val;
    
    function setVal(uint256 val_) public {
        val = val_;
    }
    
    function getVal() public view returns(uint256) {
        return val;
    }
}

interface ITemplate {
    function setVal(uint256) external;
    function getVal() external view returns(uint256);
}

contract Test {
    
    function setVal(address addr, uint256 val_) public {
        ITemplate(addr).setVal(val_);
    }
    
    function getVal(address addr) public view returns(uint256) {
        return ITemplate(addr).getVal();
    }
}

The modified version having both views neglected in the interface and calling contract.

interface ITemplate {
    function setVal(uint256) external;
    function getVal() external returns(uint256);
}

contract Test {
    
    function setVal(address addr, uint256 val_) public {
        ITemplate(addr).setVal(val_);
    }
    
    function getVal(address addr) public returns(uint256) {
        return ITemplate(addr).getVal();
    }
}

This version would return an error message saying things like “declared as view, but (potentially) modifies the state, requires non-payable(default) or payable”, which seem to be irrelevant.

interface ITemplate {
    function setVal(uint256) external;
    function getVal() external returns(uint256);
}

contract Test {
    
    function setVal(address addr, uint256 val_) public {
        ITemplate(addr).setVal(val_);
    }
    
    function getVal(address addr) public view returns(uint256) {
        return ITemplate(addr).getVal();
    }
}