Venom In Action. Voting system contracts.

This section will show you how to create your own SMV simple system. The real purpose of this guide - to explore some common mechanics like address calculation, external callings and bounce handling.

No further ado. Let's start with familiar command

npx locklift init --path my-smv

As you previously read, we need to implement two smart contracts. There is no external dependencies for this guide. Start with Vote contract. We have a pretty clean state and constructor without something unusual

Vote.sol
pragma ever-solidity >= 0.61.2;
pragma AbiHeader expire;
pragma AbiHeader pubkey;

import "./Ballot.sol";

contract Vote {
    uint16  static _nonce;
    TvmCell static _ballotCode;

    uint256 _managerPublicKey;
    uint32  _acceptedCount;
    uint32  _rejectedCount;

    constructor(
        uint256 managerPublicKey,
        address sendRemainingGasTo
    ) public {
        tvm.accept();
        tvm.rawReserve(0.1 ever, 0);
        _managerPublicKey = managerPublicKey;
        sendRemainingGasTo.transfer({ value: 0, flag: 128, bounce: false });
    }
}

Next function we need - deployBallot. It realize popular "deploy contract from contract" mechanic well-descripted here. We should just use tvm.buildStateInit function, fill varInit section by future values of our Ballot contract static variables and use keyword new for deploying.

Vote.sol
pragma ever-solidity >= 0.61.2;
...

contract Vote {
   ...
    function deployBallot(address owner, address sendRemainingGasTo) external view {
        tvm.rawReserve(0.1 ever, 0);
        TvmCell ballotStateInit = tvm.buildStateInit({
            contr: Ballot,
            // varInit section has an affect for target contract address calculation
            varInit: {
                _vote: address(this),
                _managerPublicKey: _managerPublicKey,
                _owner: owner
            },
            code: _ballotCode // we store it in state
        });
        new Ballot{
            stateInit: ballotStateInit,
            value: 0,
            flag: 128
        }(
            sendRemainingGasTo
        ); 
    }
    ...
}

Well, the votes will be stored in our Vote contract. That's why we need a special function, that can be called only by Ballot contract. Ballot contract will call this function and pass a vote (accept or reject) into. But how we can define a function, that can be called only by contracts with concrete code (by contracts, that was deployed by Vote contract)?

It can't be any easier. Address of any contract can be definitely calculated, if you know state init variables, public key and contract code:

Vote.sol
pragma ever-solidity >= 0.61.2;
...

contract Vote {
    ...
    // this function will be called by ballots, but how we can know - is calling ballot a fake or not?
    function onBallotUsed(address owner, address sendRemainingGasTo, bool accept) external {
        tvm.rawReserve(0.1 ever, 0);
        // if you know init params of contract you can pretty simple calculate it's address
        TvmCell ballotStateInit = tvm.buildStateInit({
            contr: Ballot,
            varInit: {
                _vote: address(this),
                _managerPublicKey: _managerPublicKey,
                _owner: owner
            },
            code: _ballotCode
        });
        // so address is a hash from state init
        address expectedAddress = address(tvm.hash(ballotStateInit));
        // and now we can just compare msg.sender address with calculated expected address
        // if its equals - calling ballot has the same code, that Vote stores and deploys
        if (msg.sender == expectedAddress) {
            if (accept) {
                _acceptedCount++;
            } else {
                _rejectedCount++;
            }
            sendRemainingGasTo.transfer({value: 0, flag: 128, bounce: false});
        } else {
            msg.sender.transfer({ value: 0, flag: 128, bounce: false });
        }
    }
    ...
}

That is the way out! TokenWallets of TIP-3 implementation working the same way to transfer tokens (one wallet calls another wallet's acceptTransfer function).

The last thing we need is a getDetails view function to return results of our vote

function getDetails() external view returns (uint32 accepted, uint32 rejected) {
    return (_acceptedCount, _rejectedCount);
}

Bring it all together

Vote.sol
pragma ever-solidity >= 0.61.2;
pragma AbiHeader expire;
pragma AbiHeader pubkey;

import "./Ballot.sol";

contract Vote {
    uint16  static _nonce;
    TvmCell static _ballotCode;

    uint256 _managerPublicKey;
    uint32  _acceptedCount;
    uint32  _rejectedCount;

    constructor(
        uint256 managerPublicKey,
        address sendRemainingGasTo
    ) public {
        tvm.accept();
        tvm.rawReserve(0.1 ever, 0);
        _managerPublicKey = managerPublicKey;
        sendRemainingGasTo.transfer({ value: 0, flag: 128, bounce: false });
    }

    function deployBallot(address owner, address sendRemainingGasTo) external view {
        tvm.rawReserve(0.1 ever, 0);
        TvmCell ballotStateInit = tvm.buildStateInit({
            contr: Ballot,
            varInit: {
                _vote: address(this),
                _managerPublicKey: _managerPublicKey,
                _owner: owner
            },
            code: _ballotCode
        });
        new Ballot{
            stateInit: ballotStateInit,
            value: 0,
            flag: 128
        }(
            sendRemainingGasTo
        ); 
    }

    // this function will be called by ballots, but how we can know - is calling ballot a fake or not?
    function onBallotUsed(address owner, address sendRemainingGasTo, bool accept) external {
        tvm.rawReserve(0.1 ever, 0);
        // if you know init params of contract you can pretty simple calculate it's address
        TvmCell ballotStateInit = tvm.buildStateInit({
            contr: Ballot,
            varInit: {
                _vote: address(this),
                _managerPublicKey: _managerPublicKey,
                _owner: owner
            },
            code: _ballotCode
        });
        // so address is a hash from state init
        address expectedAddress = address(tvm.hash(ballotStateInit));
        // and now we can just compare msg.sender address with calculated expected address
        // if its equals - calling ballot has the same code, that Vote stores and deploys
        if (msg.sender == expectedAddress) {
            if (accept) {
                _acceptedCount++;
            } else {
                _rejectedCount++;
            }
            sendRemainingGasTo.transfer({value: 0, flag: 128, bounce: false});
        } else {
            msg.sender.transfer({ value: 0, flag: 128, bounce: false });
        }
    }

    function getDetails() external view returns (uint32 accepted, uint32 rejected) {
        return (_acceptedCount, _rejectedCount);
    }
}

Now let's deal with Ballot contract. There is no something special in state and constructor:

Ballot.sol
pragma ever-solidity >= 0.61.2;
pragma AbiHeader expire;
pragma AbiHeader pubkey;

import "./interfaces/IVote.sol";

contract Ballot {
    address static _vote;
    uint256 static _managerPublicKey;
    // we have a static for owner...so our logic would be like "allow this address to vote"
    // we can store a static here for ballot number, and our logic would been "allow that ballot to vote"
    address static _owner;

    bool _activated; // have ballot already been activated
    bool _used;      // have ballot already been used (vote)

    constructor(address sendRemainingGasTo) public {
        // we are reserving another 0.1 here for paying for future external call
        // all another reserves will be on 0.1 only
        tvm.rawReserve(0.1 ever + 0.1 ever, 0);
        if (msg.sender != _vote) {
            selfdestruct(msg.sender);
        }
        _activated = false;
        _used = false;
    }
}    

Let's talk about activation mechanic. In constructor we already reserved little more venoms. We made it with purpose, that fee for external call will be payed from contract balance. That way of gas management allows us to transfer external calls fee paying to user responsibility. But activate method shouldn't be called by somebody unauthorized, so we just use require keyword with comparing msg.pubkey and _managerPublicKey stored in state init. Of course you need to call tvm.accept() function. Simply put, this call allows contract to use it's own balance for execution pay.

Ballot.sol
pragma ever-solidity >= 0.61.2;
...

import "./interfaces/IVote.sol";

contract Ballot {
    ...
    // this function will be called by external message, so contract will pay for this call
    // this mechanic exists for moving commision paying to user responsibility
    // in consctructor we reserver a little more venoms, so here we just will use them (with returning remains)
    // useful mechaninc for your dapp
    function activate() external {
        require(msg.pubkey() == _managerPublicKey, 200);
        tvm.accept(); // allow to use contract balance for paying this function execution
        _activated = true;
        tvm.rawReserve(0.1 ever, 0);
        _owner.transfer({ value: 0, flag: 128, bounce: false });
    }
    ...
}

Let's implement main function of our Ballot - vote.

Pay attention to imports. We have import "./interfaces/IVote.sol"; . It's just an interface for calling our Vote contract (just like for EVM if you know what I mean).

interfaces/IVote.sol
pragma ever-solidity >= 0.61.2;
pragma AbiHeader expire;

interface IVote {
    function onBallotUsed(address owner, address sendRemainingGasTo, bool accept) external;
}

Let us now return for vote function

Ballot.sol
pragma ever-solidity >= 0.61.2;
...

import "./interfaces/IVote.sol";

contract Ballot {
    ...
    function vote(address sendRemainingGasTo, bool accept) external {
        require(msg.sender == _owner, 201); // remember the library for error codes :)
        require(_activated && !_used, 202);
        tvm.rawReserve(0.1 ever, 0);
        // just call our vote contract
        IVote(_vote).onBallotUsed{
            value: 0,
            flag: 128,
            bounce: true
        }(_owner, sendRemainingGasTo, accept);
        _used = true;
    }
    ...
}

That's all. Vote contract will check our Ballot address by calculating it, as you remember, and vote will be accept. But what if Vote calls will fail because of some reason (low gas attached or yet network problem!)? Our Ballot will be marked as used (_used state variable will be set as true, and we can't call vote once again). For solve this problems, TVM has a bounce messages and onBounce function for handling it. Let's deal with it by example

Ballot.sol
pragma ever-solidity >= 0.61.2;
...

import "./interfaces/IVote.sol";

contract Ballot {
    ...
    // onBounce function!
    // if our vote contract will reject message, it sends a bounce message to this callback. We should return _used flag to false!
    onBounce(TvmSlice bounce) external {
        uint32 functionId = bounce.decode(uint32);
        // IVote.onBallotUsed send us a bounce message
        if (functionId == tvm.functionId(IVote.onBallotUsed) && msg.sender == _vote) {
            tvm.rawReserve(0.1 ever, 0);
            _used = false; // reset _used flag to false
        }
    }
    ...
}

That's it. Now let's bring it all together.

Ballot.sol
pragma ever-solidity >= 0.61.2;
pragma AbiHeader expire;
pragma AbiHeader pubkey;

import "./interfaces/IVote.sol";

contract Ballot {
    address static _vote;
    uint256 static _managerPublicKey;
    // we have a static for owner...so our logic would be like "allow this address to vote"
    // we can store a static here for ballot number, and our logic would been "allow that ballot to vote"
    address static _owner;

    bool _activated; // have ballot already been activated
    bool _used;      // have ballot already been used (vote)

    constructor(address sendRemainingGasTo) public {
        // we are reserving another 0.1 here for paying for future external call
        // all another reserves will be on 0.1 only
        tvm.rawReserve(0.1 ever + 0.1 ever, 0);
        if (msg.sender != _vote) {
            selfdestruct(msg.sender);
        }
        _activated = false;
        _used = false;
        sendRemainingGasTo.transfer({ value: 0, flag: 128, bounce: false });
    }

    // this function will be called by external message, so contract will pay for this call
    // this mechanic exists for moving commision paying to user responsibility
    // in consctructor we reserver a little more venoms, so here we just will use them (with returning remains)
    // useful mechaninc for your dapp
    function activate() external {
        require(msg.pubkey() == _managerPublicKey, 200);
        tvm.accept(); // allow to use contract balance for paying this function execution
        _activated = true;
        tvm.rawReserve(0.1 ever, 0);
        _owner.transfer({ value: 0, flag: 128, bounce: false });
    }

    function vote(address sendRemainingGasTo, bool accept) external {
        require(msg.sender == _owner, 201);
        require(_activated && !_used, 202);
        tvm.rawReserve(0.1 ever, 0);
        // just call our vote contract
        IVote(_vote).onBallotUsed{
            value: 0,
            flag: 128,
            bounce: true
        }(_owner, sendRemainingGasTo, accept);
        _used = true;
    }

    // onBounce function!
    // if our vote contract will reject message, it sends a bounce message to this callback. We should return _used flag to false!
    onBounce(TvmSlice bounce) external {
        uint32 functionId = bounce.decode(uint32);
        // IVote.onBallotUsed send us a bounce message
        if (functionId == tvm.functionId(IVote.onBallotUsed) && msg.sender == _vote) {
            tvm.rawReserve(0.1 ever, 0);
            _used = false;
        }
    }

}

Do not forget about tests and scripts. We won't show any scripts in this guideline just because there is no something special in. All source ode with deploy script and simple test suite are available in repo. Next section will show you some enhancing for this code.

Last updated