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
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
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).
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.