Ethereum DApps: Cross-contract Communication & Token Selling

Share this article

Ethereum DApps: Cross-contract Communication & Token Selling

In part 4 of this tutorial series on building DApps with Ethereum, we started building and testing our DAO contract. Now let’s go one step further and handle adding content and tokens to the story, as per our introduction.

Adding Tokens

For a contract to be able to interact with another contract, it needs to be aware of that other contract’s interface — the functions available to it. Since our TNS token has a fairly straightforward interface, we can include it as such in the contract of our DAO, above the contract StoryDao declaration and under our import statements:

contract LockableToken is Ownable {
    function totalSupply() public view returns (uint256);
    function balanceOf(address who) public view returns (uint256);
    function transfer(address to, uint256 value) public returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    function allowance(address owner, address spender) public view returns (uint256);
    function transferFrom(address from, address to, uint256 value) public returns (bool);
    function approve(address spender, uint256 value) public returns (bool);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    function approveAndCall(address _spender, uint256 _value, bytes _data) public payable returns (bool);
    function transferAndCall(address _to, uint256 _value, bytes _data) public payable returns (bool);
    function transferFromAndCall(address _from, address _to, uint256 _value, bytes _data) public payable returns (bool);

    function increaseLockedAmount(address _owner, uint256 _amount) public returns (uint256);
    function decreaseLockedAmount(address _owner, uint256 _amount) public returns (uint256);
    function getLockedAmount(address _owner) view public returns (uint256);
    function getUnlockedAmount(address _owner) view public returns (uint256);
}

Notice that we don’t need to paste in the “meat” of the functions, but only their signatures (skeletons). This is all that’s needed to interact between contracts.

Now we can use these functions in the DAO contract. The plan is as follows:

  • launch the token (we already did this)
  • launch the DAO from the same address
  • send all tokens from the token-launcher to the DAO, then transfer ownership over the contract to the DAO itself
  • at this point the DAO owns all tokens and can sell them to people using the transfer function, or can reserve them for spending using the approve function (useful during votes), etc.

But how does the DAO know which address the token is deployed on? We tell it.

First, we add a new variable at the top of the DAO contract:

LockableToken public token;

Then, we add some functions:

constructor(address _token) public {
    require(_token != address(0), "Token address cannot be null-address");
    token = LockableToken(_token);
}

The constructor is the function which gets called automatically when a contract is deployed. It’s useful for initializing values like linked contracts, default values, etc. In our case, we’ll use it to consume and save the address of the TNS token. The require check is there to make sure the token’s address is valid.

While we’re at it, let’s add a function that lets users check how many tokens remain for sale in the DAO, and the ability to change to another token should something go wrong and such a change be required. This change deserves an event, too, so let’s add that in as well.

event TokenAddressChange(address token);

function daoTokenBalance() public view returns (uint256) {
    return token.balanceOf(address(this));
}

function changeTokenAddress(address _token) onlyOwner public {
    require(_token != address(0), "Token address cannot be null-address");
    token = LockableToken(_token);
    emit TokenAddressChange(_token);
}

The first function is set to view because it doesn’t change the state of the blockchain; it doesn’t alter any values. This means it’s a free, read-only function call to the blockchain: it doesn’t need a paid transaction. It also returns the balance of tokens as a number, so this needs to be declared on the function’s signature with returns (uint256). The token has a balanceOf function (see the interface we pasted in above) and it accepts one parameter — the address whose balance to check. We’re checking our (this) DAO’s balance, so “this”, and we turn “this” into an address with address().

The token address changing function allows the owner (admin) to change the token contract. It’s identical to the logic of the constructor.

Let’s see how we can let people buy the tokens now.

Buying Tokens

As per the previous part of the series, users can buy tokens by:

  • Using the fallback function if already whitelisted. In other words, just sending ether to the DAO contract.
  • Using the whitelistAddress function by sending more than the fee required for whitelisting.
  • Calling the buyTokens function directly.

There is a caveat, however. When someone calls the buyTokens function from the outside, we want it to fail if there aren’t enough tokens in the DAO to sell. But when someone buys tokens via the whitelist function by sending in too much in the first whitelisting attempt, we don’t want it to fail, because then the whitelisting process will get canceled as everything fails at once. Transactions in Ethereum are atomic: either everything has to succeed, or nothing. So we’ll make two buyTokens functions.

// This goes at the top of the contract with other properties
uint256 public tokenToWeiRatio = 10000;

function buyTokensThrow(address _buyer, uint256 _wei) external {

    require(whitelist[_buyer], "Candidate must be whitelisted.");
    require(!blacklist[_buyer], "Candidate must not be blacklisted.");

    uint256 tokens = _wei * tokenToWeiRatio;
    require(daoTokenBalance() >= tokens, "DAO must have enough tokens for sale");
    token.transfer(_buyer, tokens);
}

function buyTokensInternal(address _buyer, uint256 _wei) internal {
    require(!blacklist[_buyer], "Candidate must not be blacklisted.");
    uint256 tokens = _wei * tokenToWeiRatio;
    if (daoTokenBalance() < tokens) {
        msg.sender.transfer(_wei);
    } else {
        token.transfer(_buyer, tokens);
    }
}

So, 100 million TNS tokens exist. If we set a price of 10000 tokens per one ether, that comes down to around 4–5 cents per token, which is acceptable.

The functions do some calculations after doing sanity checks against banned users and other factors, and immediately send the tokens out to the buyer, who can start using them as they see fit — either for voting, or for selling on exchanges. If there’s fewer tokens in the DAO than the buyer is trying to buy, the buyer is refunded.

The part token.transfer(_buyer, tokens) is us using the TNS token contract to initiate a transfer from the current location (the DAO) to the destination _buyer for amount tokens.

Now that we know people can get their hands on the tokens, let’s see if we can implement submissions.

Structs and Submissions

As per our intro post, submitting an entry will cost 0.0001 eth times the number of entries in the story already. We only need to count non-deleted submissions (because submissions can be deleted) so let’s add the properties required for this and a method to help us.

uint256 public submissionZeroFee = 0.0001 ether;
uint256 public nonDeletedSubmissions = 0;

function calculateSubmissionFee() view internal returns (uint256) {
    return submissionZeroFee * nonDeletedSubmissions;
}

Note: Solidity has built in time and ether units. Read more about them here.

This fee can only be changed by the owner, but only lowered. For increasing, it needs a vote. Let’s write the decrease function:

function lowerSubmissionFee(uint256 _fee) onlyOwner external {
    require(_fee < submissionZeroFee, "New fee must be lower than old fee.");
    submissionZeroFee = _fee;
    emit SubmissionFeeChanged(_fee);
}

We’re emitting an event to notify all observing clients that the fee has been changed, so let’s declare that event:

event SubmissionFeeChanged(uint256 newFee);

A submission can be text of up to 256 characters, and the same limit applies to images. Only their type changes. This is a great use case for a custom struct. Let’s define a new data type.

struct Submission {
    bytes content;
    bool image;
    uint256 index;
    address submitter;
    bool exists;
}

This is like an “object type” in our smart contract. The object has properties of different types. The content is a bytes type value. The image property is a boolean denoting if it’s an image (true/false). The index is a number equal to the ordinary number of the submission; its index in the list of all submissions (0, 1, 2, 3 …). The submitter is an address of the account which submitted the entry, and the exists flag is there because in mappings all the values of all keys are initialized to default values (false) even if keys don’t exist yet.

In other words, when you have a mapping of address => bool, that mapping is already going to have all the addresses in the world set to “false”. That’s just how Ethereum works. So by checking if the submission exists at a certain hash, we’d get “yes”, whereas the submission might not be there at all. The exists flag helps with that. It lets us check that the submission is there and it exists — that is, was submitted and not just implicitly added by the EVM. Furthermore, it makes “deleting” entries later on much easier.

Note: technically, we could also check to make sure that the submitter’s address isn’t a zero-address.

While we’re here, let’s define two events: one for deleting an entry, one for creating it.

event SubmissionCreated(uint256 index, bytes content, bool image, address submitter);
event SubmissionDeleted(uint256 index, bytes content, bool image, address submitter);

There’s a problem, though. Mappings in Ethereum are not iterable: we can’t loop through them without significant hacking.

To loop through them all, we’ll create an array of identifiers for these submissions wherein the keys of the array will be the index of the submission, while the values will be unique hashes we’ll generate for each submission. Solidity provides us with the keccak256 hashing algorithm for generating hashes from arbitrary values, and we can use that in tandem with the current block number to make sure an entry isn’t duplicated in the same block and get some degree of uniqueness for each entry. We use it like this: keccak256(abi.encodePacked(_content, block.number));. We need to encodePacked the variables passed to the algorithm because it needs a single parameter from us. That’s what this function does.

We also need to store the submissions somewhere, so let’s define two more contract variables.

mapping (bytes32 => Submission) public submissions;
bytes32[] public submissionIndex;

Okay, let’s try and build the createSubmission function now.

function createSubmission(bytes _content, bool _image) external payable {

    uint256 fee = calculateSubmissionFee();
    require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");

    bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
    require(!submissions[hash].exists, "Submission must not already exist in same block!");

    submissions[hash] = Submission(
        _content,
        _image,
        submissionIndex.push(hash),
        msg.sender,
        true
    );

    emit SubmissionCreated(
        submissions[hash].index,
        submissions[hash].content,
        submissions[hash].image,
        submissions[hash].submitter
    );

    nonDeletedSubmissions += 1;
}

Let’s go through this line by line:

function createSubmission(bytes _content, bool _image) external payable {

The function accepts bytes of content (bytes is a dynamically sized array of bytes, useful for storing arbitrary amounts of data) and a boolean flag for whether or not this input is an image. The function is only callable from the outside world and is payable which means it accepts Ether alongside the transaction call.

uint256 fee = calculateSubmissionFee();
require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");

Next, we calculate how much it costs to submit a new entry and then check if the value passed along with the transaction is equal to or greater than the fee.

bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
require(!submissions[hash].exists, "Submission must not already exist in same block!");

We then calculate the hash of this entry (bytes32 is a fixed-size array of 32 bytes, so 32 characters which is also the length of the output of keccak256). We use this hash to find out if a submission with that hash already exists and cancel everything if it does.

submissions[hash] = Submission(
    _content,
    _image,
    submissionIndex.push(hash),
    msg.sender,
    true
);

This part creates a new submission at the hash location in the submissions mapping. It simply passes in the values via a new struct as defined above in the contract. Note that while you might be used to the new keyword from other languages, it isn’t necessary (or allowed) here. We then emit the event (self-explanatory) and finally, there’s nonDeletedSubmissions += 1;: this is what increases the fee for the next submission (see calculateSubmissionFee).

But there’s a lot of logic missing here. We still need to:

  • account for images, and
  • check for whitelist/blacklist presence and 1 TNS token ownership on submitting accounts.

Let’s do images first. Our original plan said that an image can only be submitted every 50 texts. We’ll need two more contract properties:

uint256 public imageGapMin = 50;
uint256 public imageGap = 0;

Surely you can already assume how we’re going to handle this? Let’s add the following into our createSubmission method, immediately before creating the new submission with submissions[hash] = ....

if (_image) {
    require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it.");
    imageGap = 0;
} else {
    imageGap += 1;
}

Extremely simple: if the entry is supposed to be an image, then first check that the gap between images is more than 49 and reset it to 0 if it is. Otherwise, increase the gap by one. Just like that, every 50th (or more) submission can now be an image.

Finally, let’s do the access check. We can put this code before the fee calculation and immediately after the entry point to the function, because access checking should happen first.

require(token.balanceOf(msg.sender) >= 10**token.decimals());
require(whitelist[msg.sender], "Must be whitelisted");
require(!blacklist[msg.sender], "Must not be blacklisted");

The first line checks if the message sender has more tokens than 10 to the power of the number of decimals in a token’s contract (because we can change the token address, so it’s possible another token will take our token’s place later on and that one might not have 18 decimals!). In other words, 10**token.decimals is, in our case, 10**18, which is 1 000 000 000 000 000 000, or 1 followed by 18 zeroes. If our token has 18 decimals, that’s 1.000000000000000000, or one (1) TNS token. Note that your compiler or linter might give you some warnings when analyzing this code. That’s because the decimals property of the token is public, so its getter function is auto-generated (decimals()), but it’s not explicitly listed in the interface of the token which we listed at the top of the contract. To get around this, we can change the interface by adding this line:

function decimals() public view returns (uint256);

One more thing: since there’s an owner fee for using the contract currently set as 1%, let’s put aside the amount that the owner can withdraw and keep the rest in the DAO. The easiest way to do this is to track how much the owner can withdraw and increase that number after every submission’s creation. Let’s add a new property to the contract:

uint256 public withdrawableByOwner = 0;

And then add this to the end of our createSubmission function:

withdrawableByOwner += fee.div(daofee);

We can let the owner withdraw with a function like this:

function withdrawToOwner() public {
    owner.transfer(withdrawableByOwner);
    withdrawableByOwner = 0;
}

This sends the allowed amount to the owner and resets the counter to 0. In case the owner doesn’t want to withdraw the whole amount, we can add another function for that edge case:

function withdrawAmountToOwner(uint256 _amount) public {
    uint256 withdraw = _amount;
    if (withdraw > withdrawableByOwner) {
        withdraw = withdrawableByOwner;
    }
    owner.transfer(withdraw);
    withdrawableByOwner = withdrawableByOwner.sub(withdraw);
}

Since we’ll be referencing submissions by their hashes often, let’s write a function which checks if a submission exists so we can replace our submissions[hash].exists checks with it:

function submissionExists(bytes32 hash) public view returns (bool) {
    return submissions[hash].exists;
}

Some other helper functions for reading submissions will also be necessary:

function getSubmission(bytes32 hash) public view returns (bytes content, bool image, address submitter) {
    return (submissions[hash].content, submissions[hash].image, submissions[hash].submitter);
}

function getAllSubmissionHashes() public view returns (bytes32[]) {
    return submissionIndex;
}

function getSubmissionCount() public view returns (uint256) {
    return submissionIndex.length;
}

This is self explanatory. getSubmission fetches the submission data, getAllSubmissionHashes fetches all the unique hashes in the system, and getSubmissionCount lists how many submissions there are in total (including deleted ones). We use a combination of these functions on the client side (in the UI) to fetch the content.

The full createSubmission function now looks like this:

function createSubmission(bytes _content, bool _image) storyActive external payable {

    require(token.balanceOf(msg.sender) >= 10**token.decimals());
    require(whitelist[msg.sender], "Must be whitelisted");
    require(!blacklist[msg.sender], "Must not be blacklisted");

    uint256 fee = calculateSubmissionFee();
    require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");

    bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
    require(!submissionExists(hash), "Submission must not already exist in same block!");

    if (_image) {
        require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it.");
        imageGap = 0;
    } else {
        imageGap += 1;
    }

    submissions[hash] = Submission(
        _content,
        _image,
        submissionIndex.push(hash),
        msg.sender,
        true
    );

    emit SubmissionCreated(
        submissions[hash].index,
        submissions[hash].content,
        submissions[hash].image,
        submissions[hash].submitter
    );

    nonDeletedSubmissions += 1;
    withdrawableByOwner += fee.div(daofee);
}

Deleting

So what about deleting submissions? That’s easy enough: we just switch the exists flag to false!

function deleteSubmission(bytes32 hash) internal {
    require(submissionExists(hash), "Submission must exist to be deletable.");
    Submission storage sub = submissions[hash];

    sub.exists = false;
    deletions[submissions[hash].submitter] += 1;

    emit SubmissionDeleted(
        sub.index,
        sub.content,
        sub.image,
        sub.submitter
    );

    nonDeletedSubmissions -= 1;
}

First, we make sure the submission exists and isn’t already deleted; then we retrieve it from the storage. Next we set its exists flag to false, increase the number of deletions in the DAO for that address by 1 (useful when tracking how many entries a user has had deleted for them later on; this can lead to a blacklist!), and we emit a deletion event.

Finally, we reduce the new submission creation fee by reducing the number of non-deleted submissions in the system. Let’s not forget to add a new property to our contract as well — one to track these deletions:

mapping (address => uint256) public deletions;

Deployments Get More Complicated

Now that we’re using tokens in another contract, we need to update the deployment script (3_deploy_storydao) to pass the token’s address into the constructor of the StoryDao, like so:

var Migrations = artifacts.require("./Migrations.sol");
var StoryDao = artifacts.require("./StoryDao.sol");
var TNSToken = artifacts.require("./TNSToken.sol");

module.exports = function(deployer, network, accounts) {
  if (network == "development") {
    deployer.deploy(StoryDao, TNSToken.address, {from: accounts[0]});
  } else {
    deployer.deploy(StoryDao, TNSToken.address);
  }
};

Read more about configuring deployments here.

Conclusion

In this part, we added the ability for participants to buy tokens from our DAO and to add submissions into the story. Another part of the DAO contract remains: voting and democratization. That’s what we’ll handle in the next post.

Bruno SkvorcBruno Skvorc
View Author

Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.

DAOethereumethereum-hubethereum-tutorialsThe Neverending StoryTNS token
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form