Close Menu

    Subscribe to Updates

    What's Hot

    Best Expense Cards for Freelancers 2025

    July 15, 2025

    Best Mobile Casino & Casino Apps (July 2025)

    July 15, 2025

    Dems attempt to take 3 crypto bills hostage and block Trump

    July 15, 2025
    Facebook X (Twitter) Instagram
    laicryptolaicrypto
    Demo
    • Ethereum
    • Crypto
    • Altcoins
    • Blockchain
    • Bitcoin
    • Lithosphere News Releases
    laicryptolaicrypto
    Home Agreeable Smart Contracts – CoinCodeCap
    Crypto

    Agreeable Smart Contracts – CoinCodeCap

    John SmithBy John SmithJuly 15, 2025No Comments13 Mins Read
    Share
    Facebook Twitter LinkedIn Pinterest Email


    Who decides whether to terminate a smart contract? Who decides whether a smart contract should change state?

    The answer, of course, is that who decides these things is encoded into the smart contract itself, and it will be different for different contracts.

    For example, it may be only the smart contract owner who may terminate the contract. Alternatively, maybe the smart contract gives that ability to the two smart contract users who must agree amongst themselves. Or to 3 of the 4 smart contract users who must agree amongst themselves. There are endless possibilities.

    We can make this easier, by providing our smart contracts with agreement objects, that ensure that the smart contract enforces those mult-signature agreements between users before doing whatever has been agreed, changing state or maybe even changing the smart contract itself. The choice is endless. For instance, here is an article by this author which describes changing a smart contract following agreement between the smart contract owner and the two users.

    A real-world example could be a smart contract that handles last will and testament. In this case, there may be various phases of generating the document, perhaps by some of many solicitors, followed by an approval phase comprising some solicitors and some family members, followed by an execution phase comprising, again, some solicitors and the executors. There may be other phases, each of which comprises various numbers of users in different roles.

    Also Read: Proposing Future Ethereum Access Control

    Example

    For this article, we will transition an example smart contract through each of several states, but at each transition only when an agreement between users has been reached.

    In this example smart contract, there are 4 users.

    The first transition requires the first 2 of the users to agree to something before taking place. 

    The next transition requires the last 3 of our 4 smart contract users to agree before the smart contract can transition to the penultimate state.

    In the penultimate state, we require any 3 out of the 4 users to agree in order to facilitate the transition to the terminated state. 

    Here is a UML State Machine diagram illustrating the states and transitions:

    UML state machine diagram for example smart contractUML state machine diagram for example smart contract
    UML state machine diagram for example smart contract

    See this article by the same author for a discussion of State Machines in Solidity.

    The actions of each of the 4 user must be encoded into the smart contract.

    User    Actions
    user1   transition from Await12 to Await234
    possibly transition from Await3of4 to Terminated
    user2   transition from Await12 to Await234
    transition from Await234 to Await3of4
    possibly transition from Await3of4 to Terminated
    user3   transition from Await234 to Await3of4
    possibly transition from Await3of4 to Terminated
    user4   transition from Await234 to Await3of4
    possibly transition from Await3of4 to Terminated

    Only 3 of the 4 users are required to agree for the ultimate transition.

    Solution Options

    Amongst the many possible solution options, we will consider these options for implementing the agreement objects:

    1) Smart Contract using Accounts
    2) Smart Contract using Roles
    3) Using Library For Struct and Accounts
    4) Using Library For Struct and Roles

    Smart Contracts are not ideal for implementing objects in Solidity, because they carry a large overhead — see below.

    Using Library For Struct is the main feature provided by Solidity to implement objects.

    There is a third option, which is using inheritance as a substitution for composition, but because the example smart contract has multiple agreement objects, that is not applicable here.

    This article by the the same author discusses class features provided by Solidity in more detail.

    The Accounts/Roles options are considered because the implementation could use roles instead of accounts — that decision typically depends on whether your smart contract has more than one user performing the same role. There is plenty of information about role-based access control available. See this helpful OpenZeppelin contribution for instance.

    In practice, we only need to implement 3 of the 4 options and deduce whether it is worth implementing the 4th option. We will not bother with option (2) unless we encounter a compelling reason to implement it.

    Smart Contracts

    Here is the partially completed example smart contract, which fulfils the UML state machine design above, and provides logical class objects for subsequent implementation:

                        MultipleAgreementContract
    contract MultipleAgreementContract {

    // provide agreement objects
    ... agreement12;
    ... agreement234;
    ... agreement3of4;

    constructor(address[4] memory accounts) public {
    // initialise all agreement objects
    }

        enum State { Await12, Await234, Await3of4, Terminated }
    State public state = State.Await12;
        function transition12(address account) public {
    require(state == State.Await12);
    uint object = 123;
    if (agreement12.agree(object, account) > 0)
    state = State.Await234;
    }
        function transition234(address account) public returns (bool) {
    require(state == State.Await234);
    uint object = 456;
    if (agreement234.agree(object, account) > 0)
    state = State.Await3of4;
    }
        function transition3of4(address account) public returns (bool) {
    require(state == State.Await3of4);
    uint object = 789;
    if (agreement3of4.agree(object, account) > 0)
    state = State.Terminated;
    }
    }

    This smart contract contains the state machine. This is simply implemented in the smart contract using enum State, require(state == X) and state = State.X. The state is made public in order to assist with testing (shown later).

    The smart contract contains the transition functions that must be invoked by the users in order to attempt transition to the next state.

    The state changing logic of the transition functions will only be executed if the function is called in the correct state. Each transition function must be called by each of the users who are responsible for the transition, but in any order. For instance, in order to transition from State.Await12 to State.Await234, both user 1 and user 2 must invoke the transition functiontransition12. Following the second such invocation, the machine state will be changed.

    The different solution options are discussed further in the following sections.

    1) Smart Contract using Accounts

    Here is the MultipleAgreementContract constructor for this solution option:

        MultipleAgreementContract for Smart Contract using Accounts
    import "Agreement2.sol";
    import "Agreement3.sol";
    import "Agreement3of4.sol";
    contract MultipleAgreementContract {

    Agreement2 agreement12;
    Agreement3 agreement234;
    Agreement3of4 agreement3of4;

        constructor( address[4] memory accounts ) public {
    agreement12 = new Agreement2([accounts[0], accounts[1]]);
    agreement234 = new Agreement3(
    [accounts[1], accounts[2], accounts[3]]);
    agreement3of4 = new Agreement3of4(accounts);
    }

    Here, each of the agreement objects has its own smart contract and is instantiated with the user accounts that are required to achieve agreement.

    The implementation contracts are along these lines:

                                 Agreement2
    // agreement between 2 accounts
    import "AgreementBase.sol";
    contract Agreement2 is AgreementBase {
    constructor( address[2] memory _accounts ) public {
    accounts.push(_accounts[0]);
    accounts.push(_accounts[1]);
    sigs = 2;
    }
    }

    The implementations of Agreement3 and Agreement3of4 are similar. The AgreementBase contract that these implementation contracts require is:

                                AgreementBase
    // agreement between configured accounts
    import "DiBits.sol";
    contract AgreementBase {

    address[] accounts; // set accounts in derived class
    uint sigs; // // set minimum signatures in derived class

        uint object;
    uint bitMasks;

    function agree(uint _object, address account) public returns
    (int) {

            // returns 
    
    
    
    
            uint bitMask;
    for (uint i = 0; i if (account == accounts[i]) {
    bitMask = 1 break;
    }
    }
    require(bitMask != 0, "Unknown sender");

    if (object != _object) {
    object = _object;
    bitMasks = bitMask;
    return -1;
    }

    bitMasks |= bitMask;

    if (DiBits.count(bitMasks) >= sigs) {
    return 1;
    }
    }
    }

    The accounts array and sigs value must be configured in derived contracts.

    The sigs variable is used for the Agreement3of4 class, and generally for mutli-sig agreements requiring agreement amongst some of the users. This feature of the agreement objects enables the smart contract to determine when agreement has been reached. That could be 1 of 2, 1 of 5, or 3 of 4 as in this case.

    Events may be usefully emitted when a new object is detected, and when agreement is reached. This is not shown for clarity.

    The library function DiBits.count(uint) simply counts the number of set bits.

    3) Using Library For Struct and Accounts

    Here is the MultipleAgreementContract constructor for this solution option:

    MultipleAgreementContract for Using Library For Struct and Accounts
    import "Agreement.sol";
    contract MultipleAgreementContract {
    using AgreementFuns for Agreement;

    Agreement agreement12;
    Agreement agreement234;
    Agreement agreement3of4;

    constructor(address[4] memory accounts) public {
    agreement12.initialise([accounts[0], accounts[1]]);
    agreement234.initialise(
    [accounts[1], accounts[2], accounts[3]]);
    agreement3of4.initialise(accounts);
    agreement3of4.sigs = 3;
    }

    The line using AgreementFuns for Agreement, causes the compiler to use the functions in the AgreementFuns library for Agreement objects. See the Solidity documentation here for more information.

    The constructor initialises the 3 agreement objects with the user accounts required to fulfil the agreements. The last line resets the minimum number of accounts that must agree to 3 (otherwise it would have been all 4 accounts).

    Here is the agreement inclusion file:

                       Agreement using address accounts
    // agreement between given accounts
    import "DiBits.sol";
    struct Agreement {
    address[] accounts; // user accounts are recorded here
    uint sigs; // the min number of accounts required to agree

    uint object; // the object to be agreed e.g. a contract address
    uint bitMasks; // the set of agreed accounts
    }

    // -----------
    library AgreementFuns {

    function initialise(Agreement storage self, address[2] memory
    accounts) public {
    self.accounts.push(accounts[0]);
    self.accounts.push(accounts[1]);
    self.sigs = 2;
    }

    // also initialisers for address[3 and 4] memory accounts

    function agree(Agreement storage self, uint _object, address
    sender) public returns (int) {

            // returns 
    
    
    
    
            uint bitMask;
    uint length = self.accounts.length;
    for (uint i = 0; i if (sender == self.accounts[i]) {
    bitMask = 1 break;
    }
    }
    require(bitMask != 0, "Unknown sender");

    if (self.object != _object) {
    self.object = _object;
    self.bitMasks = bitMask;
    return -1;
    }

    self.bitMasks |= bitMask;

    if (DiBits.count(self.bitMasks) >= self.sigs) {
    return 1;
    }
    }
    }

    This is substantially the same as presented above. The differences are in bold.

    As stated above, events may be usefully emitted when a new object is detected, and when agreement is reached. This is not shown for clarity.

    When using smart contracts, we can derive new contracts from base contracts at no cost to the code.

    However, when using libraries, we are a little more restricted because libraries cannot have base classes or be base classes. So all possible initialisers have to be added to the library to retain data encapsulation. For this reason, it may be preferable to use a dynamic array in the initialiser, and avoid multiple initialisers. That effectively moves the configuration of the Agreement accounts data to the calling routine. Hence, we haven’t bothered with that here.

    4) Using Library For Struct and Roles

    Here is the MultipleAgreementContract constructor for this solution option:

    MultipleAgreementContract for Using Library For Struct and Roles
    import "Agreement.sol";
    contract MultipleAgreementContract {
    using AgreementFuns for Agreement;

    Agreement agreement12;
    Agreement agreement234;
    Agreement agreement3of4;

    mapping(address => uint8) roles;

        constructor(address[4] memory accounts) public {
    roles[accounts[0]] = 1;
    roles[accounts[1]] = 2;
    roles[accounts[2]] = 3;
    roles[accounts[3]] = 4;

    agreement12.initialise([1, 2]);
    agreement234.initialise([2, 3, 4]);
    agreement3of4.initialise([1, 2, 3, 4]);
    agreement3of4.sigs = 3; // multi-sig 3 of 4
    }

    This is substantially the same as the previous solution option, except for the addition of a mapping from the account addresses to the roles identifiers, and initialisation of same in the constructor — we have just used the Role numbers 1 to 4. Lastly, we pass the role numbers, not the account addresses, through to the agreement objects.

    This facility means that, if needed, we can easily add user accounts which map to the current roles without changing anything else in the smart contract, for instance:

                  Example Additional User for existing Role
        constructor(address[5] memory accounts) public {
    roles[accounts[0]] = 1;
    roles[accounts[1]] = 2;
    roles[accounts[2]] = 3;
    roles[accounts[3]] = 4;
            roles[accounts[4]] = 2; // additional account for role 2

    agreement12.initialise([1, 2]);
    agreement234.initialise([2, 3, 4]);
    agreement3of4.initialise([1, 2, 3, 4]);
    agreement3of4.sigs = 3; // multi-sig 3 of 4
    }

    The agreement inclusion file is substantially the same as the previous example. We simply replace all account addresses with role uints:

                       4) Agreement using uint roles
    // agreement between roles
    struct Agreement {
    uint[] roles; // roles are recorded here
        // as before
    }

    That also means that there is an opportunity here to use a variable which requires less than 32bytes to record a role. As well as using an array of uints to contain the roles, we will try using a bytes32 variable, with 1 byte allocated to each role:

                      4a) Agreement using bytes32 roles
    struct Agreement {
    uint len; // number of roles
    bytes32 roles; // roles are recorded here
    uint sigs; // the min number of roles required to agree

    uint bitMasks;
    uint object;
    }

    Obviously, the initialisation and agreement functions will need a little fettling.

    Lastly, we would like to take the opportunity to optimise the gas consumption by minimising the use of Ethereum’s expensive storage slots:

               4b) Agreement optimising use of storage space
    struct Agreement {
    uint160 object; // large enough to hold address
    uint64 roles; // up to 16 roles of 16 values each
    uint8 len; // number of roles
    uint8 sigs; // // the min number of roles required to agree
    uint8 bitMasks; // set of agreements reached so far
    }

    This whole struct fits within one 256bit storage word. Despite the additional machinations required to read and write the data, the gas consumption for storage access will be vastly reduced.

    Testing

    Photo by Science in HD on UnsplashPhoto by Science in HD on Unsplash
    Photo by Science in HD on Unsplash

    The fundamental approach to testing used here is to step the smart contract under test through the machine states originally specified (sunny day testing). Other tests are required to ensure that the smart contract is working properly. The test smart contract is:

                         TestMultipleAgreementContract
    contract TestMultipleAgreementContract {
    address constant account1 = 0x...1111111111;
    address constant account2 = 0x...2222222222;
    address constant account3 = 0x...3333333333;
    address constant account4 = 0x...4444444444;
        MultipleAgreementContract mac;

    constructor() public {
    mac = new MultipleAgreementContract(
    [account1, account2, account3, account4]);
    }

    function test12() public {
    require(mac.state() == ...Await12);
    mac.transit12(account1);
    require(mac.state() == ...Await12);
    mac.transit12(account2);
    require(mac.state() == ...Await234);
    }

    function test234() public {
    require(mac.state() == ...Await234);
    mac.transit234(account2);
    require(mac.state() == ...Await234);
    mac.transit234(account3);
    require(mac.state() == ...Await234);
    mac.transit234(account4);
    require(mac.state() == ...Await3of4);
    }

    function test3of4() public {
    require(mac.state() == ...Await3of4);
    mac.transit3of4(account1);
    require(mac.state() == ...Await3of4);
    mac.transit3of4(account2);
    require(mac.state() == ...Await3of4);
    mac.transit3of4(account2);
    require(mac.state() == ...Await3of4);
    mac.transit3of4(account4);
    require(mac.state() == ...Terminated);
    }
    }

    The test contract is the same, irrespective of the smart contract being tested.

    Gas Consumption

    Having ascertained that all the solution contracts pass these simple tests, we can measure the gas consumed while doing so.

    The gas consumption measuring smart contract to do that is along these lines:

                     GasTestMultipleAgreementContract
    import "DcGas.sol";
    contract GasTestMultipleAgreementContract is DcGas {
    TestMultipleAgreementContract testMac;
        function testConstruct() internal {
    testMac = new TestMultipleAgreementContract();
    }
    function gtestConstruct() public {
    gasCostFun("testConstruct", testConstruct);
    }
        function testTransit12() internal {
    testMac.test12();
    }
    function gtestTransit12() public {
    gasCostFun("testTransit12", testTransit12);
    }
        function testTransit234() internal {
    testMultipleAgreementContract.test234();
    }
    function gtestTransit234() public {
    gasCostFun("testTransit234", testTransit234);
    }
        function testTransit3of4() internal {
    testMultipleAgreementContract.test3of4();
    }
    function gtestTransit3of4() public {
    gasCostFun("testTransit3of4", testTransit3of4);
    }
    }

    We used a method of measuring gas consumption we have used many times before. See this article by the same author for the tehnique.

    Results

    The gas consumed during execution of the tests was collated and entered into spreadsheets. Here is the total gas consumption for the solution options (1), (3) and (4), and the additional solutions introduced above, (4a) and (4b).

    Gas consumption of executing the test smart contractsGas consumption of executing the test smart contracts
    Gas consumption of executing the test smart contracts

    This chart clearly shows the high gas cost of using smart contracts to represent objects in the first column 1) Contract/Accounts.

    Gas consumption is lower when employing using library for struct, as shown in 3) Library / Accounts.

    Changing from using accounts with our libraries to using roles does consume more gas, as indicated by 4) Library / Roles.

    However, we can mitigate that by employing smaller variables to hold our role number as shown in 4a) Library / Roles / bytes32.

    Lastly, it can clearly be seen that optimising the Agreement struct to use bit fields in order to reduce the usage of expensive storage has a marked affect, as indicated by 4b) Library / Roles / Optimisation.

    Assuming that the smart contract is long lived, and could transit from the ultimate state back to the initial state many times, the gas consumed during smart contract construction may be less relevant. Here is a graph of gas consumption ignore the construction costs:

    Gas consumption of transitioning the test smart contractsGas consumption of transitioning the test smart contracts
    Gas consumption of transitioning the test smart contracts

    This shows a lot more clearly that the optimised data structure for roles consumes vastly less gas than the other options.

    Conclusions

    Use Agreement objects whenever you need several users to agree on the same object before changing smart contract state.

    Unless you have a good reason, avoid the use of contracts to represent objects and employ using library for struct instead.

    If it suits the requirements of your smart contract, use agreement roles in preference to agreement accounts.

    As well as additional flexibility in terms of enabling multiple users to occupy the same role, there is an opportunity to optimise the agreement role data and vastly reduce use of Ethereum’s expensive storage slots.

    Bio

    Jules Goddard is Co-founder of Datona Labs, who provide smart contracts to protect your digital information from abuse.

    website — twitter — discord — GitHub





    Source link

    Share. Facebook Twitter Pinterest LinkedIn WhatsApp Reddit Tumblr Email
    John Smith

    Related Posts

    Best Expense Cards for Freelancers 2025

    July 15, 2025

    Best Mobile Casino & Casino Apps (July 2025)

    July 15, 2025

    Dems attempt to take 3 crypto bills hostage and block Trump

    July 15, 2025
    Leave A Reply Cancel Reply

    Demo
    Don't Miss
    Crypto

    Best Expense Cards for Freelancers 2025

    By John SmithJuly 15, 20250

    Meet two friends, Harry and Ron, and start with their life’s journey. Harry had an…

    Best Mobile Casino & Casino Apps (July 2025)

    July 15, 2025

    Dems attempt to take 3 crypto bills hostage and block Trump

    July 15, 2025

    Solana (SOL)-Based Memecoin Platform Pump.fun (PUMP) Gets Bump From New Coinbase Listing

    July 15, 2025

    LAI Crypto is a user-friendly platform that empowers individuals to navigate the world of cryptocurrency trading and investment with ease and confidence.

    Our Posts
    • Altcoins (178)
    • Bitcoin (9)
    • Blockchain (40)
    • Crypto (2,621)
    • Ethereum (385)
    • Lithosphere News Releases (41)
    • Uncategorized (195)

    Subscribe to Updates

    • Twitter
    • Instagram
    • YouTube
    • LinkedIn

    Type above and press Enter to search. Press Esc to cancel.