Skip to content
philipeachille edited this page Apr 1, 2020 · 3 revisions

Smart Contract Walkthrough

Constructor

Each community deploys their own instance of the contract.

The constructor allows all parameters to be customised for this particular community. The settings can also be changed later.

constructor(
    string memory _name,
    string memory _symbol,
    uint8 _decimals,
    uint _lifetime,
    uint _generationAmount,
    uint _generationPeriod,
    uint _communityTax,
    uint _transactionFee,
    uint _initialBalance,
    address _communityTaxAccount,
    address _controller
)

By default, the account deploying the contract has permission to change settings. This can be changed to a community multisig contract, or handled by Aragon. (see Aragon branch). We refer to the account or contract with this permission as the controller.

Any account can receive tokens. To receive the UBI generation, an account first has to be registered by the controller.

Verifying an account

    /** @notice Create a new account with the specified role
        @dev New accounts can always be created.
            This function can't be disabled. */
    function newAccount(
        address _account
    )
        external
        onlyController
    {
        accountApproved[_account] = true;
        if (initialBalance > 0 && lastTransactionBlock[_account] == 0) {
            _mint(_account, initialBalance);
            emit Mint(_account, initialBalance);
            zeroBlock[_account] = block.number.add(lifetime);
        }
        lastGenerationBlock[_account] = block.number;
        lastTransactionBlock[_account] = block.number;
        emit NewAccount(_account);
    }

The account is marked as approved.

As long as there are no previous transactions, the initial balance is minted. This check ensures accounts can't receive multiple initial balance payments.

The zero block is set to the full lifetime.

The last generation block is set to the current block.

The last transaction block is set to the current block.

Transferring tokens

Most of the logic is in the transfer function.

    /** @notice Transfer the currency from one account to another,
                updating each account to reflect the time passed, and the
                effects of the transfer
        @return Success */
    function transfer(address _to, uint _value) public returns (bool) {

        // Process generation and decay for sender
        triggerOnchainBalanceUpdate(msg.sender);

        // Process generation and decay for recipient
        triggerOnchainBalanceUpdate(_to);

        require(
            _balances[msg.sender] >= _value,
            "Not enough balance to make transfer"
        );

        // Process fees and tax
        uint feesAndTax = processFeesAndTax(
            _value,
            transactionFee,
            communityTax
        );
        uint valueAfterFees = _value.sub(feesAndTax);

        //Extend zero block based on transfer
        zeroBlock[_to] = calcZeroBlock(
            valueAfterFees,
            _balances[_to],
            block.number,
            lifetime,
            zeroBlock[_to]
        );

        /* If they haven't already been updated (during decay or generation),
            then update the lastTransactionBlock for both sender and recipient */
        if (lastTransactionBlock[_to] != block.number) {
            lastTransactionBlock[_to] = block.number;
        }
        if (lastTransactionBlock[msg.sender] != block.number) {
            lastTransactionBlock[msg.sender] = block.number;
        }

        super.transfer(_to, valueAfterFees);

        return true;
    }

a) triggerOnchainBalanceUpdate

We first update the balance of both sender and recipient, to take into account the time passed since the last transaction. This is handled by triggerOnchainBalanceUpdate:

    /// @notice Manually trigger an onchain update of the live balance
    function triggerOnchainBalanceUpdate(address _account) public {

        // 1. Decay the balance
        uint decay = calcDecay(
            lastTransactionBlock[_account],
            _balances[_account],
            block.number,
            zeroBlock[_account]
        );
        uint decayedBalance;
        if (decay > 0) {
            decayedBalance = burnAt(_account, decay);
            emit Decay(_account, decay);
        } else {
            decayedBalance = _balances[_account];
        }

        // 2. Generate tokens
        uint generationAccrued = 0;
        if (accountApproved[_account]) {
            // Calculate the accrued generation, taking into account decay
            generationAccrued = calcGeneration(
                block.number,
                lastGenerationBlock[_account],
                lifetime,
                generationAmount,
                generationPeriod
            );

            if (generationAccrued > 0) {
                // Issue the generated tokens
                _mint(_account, generationAccrued);
                emit IncomeReceived(_account, generationAccrued);

                // Record the last generation block
                lastGenerationBlock[_account] += calcNumCompletedPeriods(
                    block.number,
                    lastGenerationBlock[_account],
                    generationPeriod
                ).mul(generationPeriod);

                // Extend the zero block
                zeroBlock[_account] = calcZeroBlock(
                    generationAccrued,
                    decayedBalance,
                    block.number,
                    lifetime,
                    zeroBlock[_account]
                );

            }
        }
        // Record the last transaction block
        if (decay > 0 || generationAccrued > 0) {
            lastTransactionBlock[_account] = block.number;
        }
    }

We calculate whether any tokens have decayed, and then we burn those tokens.

If the account has been approved, we then calculate the generation that is due, and mint the tokens to the account's balance. Tokens will only be issued for each full generation period that has elapsed since the last transfer.

The last generation block is then recorded. Last generation block will be increased in multiples of the generation period, starting with the block that the account was approved. A new account will receive income for the first time after a full generation period has passed following its creation.

We then calculate the effect that this incoming transfer has on the zeroBlock. The generation counts as an incoming transfer and thus extends the lifetime of this account's balance.

As long as there has been some decay or generation, we update the lastTransactionBlock to the current block.

Once both onchain balances reflect the current block, (which represents the current date), we go ahead and process the transfer.

b) Require sufficient sender balance

Next, we check that the sender has enough balance to make the transfer.

require(
    _balances[msg.sender] >= _value,
    "Not enough balance to make transfer"
);

c) processFeesAndTax

Next, we process the fees and tax

// Process fees and tax
uint feesAndTax = processFeesAndTax(
    _value,
    transactionFee,
    communityTax
);
uint valueAfterFees = _value.sub(feesAndTax);
    /** @notice Calculate the fees and tax, send tax to the communtiy account,
            and burn the fees
        @dev Percentage to x dp as defined by taxFeeDecimals e.g.
            when taxFeeDecimals is 2, 1200 is 12.00%
        @return The total amount used for fees and tax */
    function processFeesAndTax(
        uint _value,
        uint _transactionFee,
        uint _communityTax)
        internal
        returns (uint)
    {
        uint feesIncTax = calcFeesIncTax(
            _value,
            _transactionFee
        );
        uint tax = calcTax(
            _value,
            _transactionFee,
            _communityTax
        );
        uint feesToBurn = calcFeesToBurn(
            _value,
            _transactionFee,
            _communityTax
        );
        require(feesIncTax == tax.add(feesToBurn),
            "feesIncTax should equal tax + feesToBurn"
        );

        if (feesToBurn > 0) {
            burn(feesToBurn);
            emit BurnedFees(msg.sender, feesToBurn);
        }

        if (tax > 0) {
            super.transfer(communityTaxAccount, tax);
            emit PaidTax(msg.sender, tax);
        }

        return feesIncTax;
    }

First the fee is calculated. The tax is taken as a percentage of this fee.

taxFeeDecimals defines the number of decimal places in the percentages. So, if taxFeeDecimals is 2, a fee of 3300 and tax of 1000 indicates a fee of 33%, with 10% of this fee taken as tax.

With these values, a transfer of 1000 would incur a fee of 330, with 33 sent to the community account as tax, and 297 burned.

d) calcZeroBlock

Next, we extend the zero block for the recipient.

//Extend zero block based on transfer
zeroBlock[_to] = calcZeroBlock(
    valueAfterFees,
    _balances[_to],
    block.number,
    lifetime,
    zeroBlock[_to]
);
    /** @notice Calculate the block at which the balance for this account will
            be zero
        @return Block at which balance is 0 */
    function calcZeroBlock(
        uint _value,
        uint _balance,
        uint _blockNumber,
        uint _lifetime,
        uint _originalZeroBlock
    )
        public
        pure
        returns (uint)
    {
        if (_balance == 0 || _originalZeroBlock == 0) {
            // No other transaction to consider, so use the full lifetime
            return _blockNumber.add(_lifetime);
        }

        /* transactionWeight is the ratio of the transfer value to the total
            balance after the transfer */
        uint transactionWeight = _value.mul(multiplier).div(
            _balance.add(_value)
        );

        /* multiply the full lifetime by this ratio, and add
            the result to the original zero block */
        uint newZeroBlock = _originalZeroBlock.add(
            _lifetime.mul(transactionWeight).div(multiplier)
        );

        if (newZeroBlock > _blockNumber.add(_lifetime)) {
            newZeroBlock = _blockNumber.add(_lifetime);
        }
        return newZeroBlock;
    }

All calculations are pure functions to make testing easier.

If this is the first transaction, or the balance is 0, we give the balance the full lifetime.

Otherwise, we add a portion of the full lifetime, to the same ratio as the weight of the transfer. [the ratio of the transfer value to the total balance after the transfer].

This means that a small transfer (compared to the balance) will extend the total lifetime by a small amount. Whereas, a large transfer will extend the total lifetime by a large amount. blocksToZero will never exceed the value of the current block plus the lifetime setting.

e) lastTransactionBlock

Next, we update the lastTransactionBlock for recipient and sender. This probably already occured during triggerOnchainBalance update, as there was probably some decay. If it's already the current block, we don't waste gas by setting it again.

/* If they haven't already been updated (during decay or generation),
    then update the lastTransactionBlock for both sender and recipient */
if (lastTransactionBlock[_to] != block.number) {
    lastTransactionBlock[_to] = block.number;
}
if (lastTransactionBlock[msg.sender] != block.number) {
    lastTransactionBlock[msg.sender] = block.number;
}

f) transfer

Finally, we trigger the transfer of tokens, minus the fees and tax. ZeppelinOS ERC20 implementation handles the transfer.

super.transfer(_to, valueAfterFees);

Calculating Decay

    /** @notice Calculate the number of tokens decayed since the last transaction
        @return Number of tokens decayed since last transaction */
    function calcDecay(
        uint _lastTransactionBlock,
        uint _balance,
        uint _thisBlock,
        uint _zeroBlock
    )
        public
        pure
        returns (uint)
    {
        require(_thisBlock >= _lastTransactionBlock,
            "Current block must be >= last transaction block"
        );

        // If zero block has not been set, decay = 0
        if (_zeroBlock == 0) {
            return 0;
        }

        // If zero block passed, decay all
        if (_thisBlock >= _zeroBlock) {
            return _balance;
        }

        // If no blocks passed since last transfer, nothing to decay
        uint blocksSinceLast = _thisBlock.sub(_lastTransactionBlock);
        if (blocksSinceLast == 0) {
            return 0;
        }
        /* Otherwise linear burn based on 'distance' moved to zeroblock since
            last transaction */
        uint fullDistance = _zeroBlock.sub(_lastTransactionBlock);
        uint relativeMovementToZero = blocksSinceLast.mul(
            multiplier
        ).div(fullDistance);
        return _balance.mul(relativeMovementToZero).div(multiplier);
    }

Calculating the decay assumes that the zeroBlock is set correctly, and that the balance was accurate as of the last transaction block.

If no zero block has been set, there will be no decay.

If the current block is greater than the zero block, all of the balance has decayed.

If the lastTransactionBlock is the current block, the balance is up to date and nothing further to decay.

Assuming none of these conditions have been met, we perform a linear calculation taking into account the relative movement to the zero block, since the last transaction block. So, if at the last transaction block we were 100 blocks from the zeroBlock, and at this block we are only 50 blocks from the zeroBlock, 50% of the previous balance needs to decay.

Calculating generation

/** @notice Calculate the generation accrued since the last generation
        period
    @dev This function contains a for loop, so in theory may fail for
        accounts that have been inactive for an extremely long time. These
        accounts will have zero balance anyway. */
function calcGeneration(
    uint _blockNumber,
    uint _lastGenerationBlock,
    uint _lifetime,
    uint _generationAmount,
    uint _generationPeriod
)
    public
    pure
    returns (uint)
{
    uint numCompletePeriods = calcNumCompletedPeriods(
        _blockNumber,
        _lastGenerationBlock,
        _generationPeriod
    );

    uint decayPerBlock = multiplier.div(_lifetime);
    uint decayPerGenerationPeriod = decayPerBlock.mul(_generationPeriod);
    uint remainingPerGenerationPeriod = multiplier.sub(
        decayPerGenerationPeriod
    );

    uint generation = 0;
    for(uint i = 0; i < numCompletePeriods; i++) {
        generation = generation.mul(
            remainingPerGenerationPeriod
        ).div(multiplier).add(_generationAmount);
    }
    return generation;
}

The generation amount is issued in full after each generation period.

The periods are counted from when the account is first created, so accounts will receive generation on different blocks. Accounts will never receive partial payments of the generation amount.

Generation is only calculated when an account sends or receives a transfer. Depending on the activity of each account, many generation periods may have passed in between transfers. When there are multiple periods, the function iterates through each generation period, applies the decay that would have taken place during the last period, and then adds the next generation amount. This design ensures that the generation income is the same regardless of account activity, while saving gas by not having to update the balance of unused accounts. As soon as a dormant account is used, all generation periods it missed will be processed in one block.

Live Balance Of

/** @notice Return the real balance of the account, as of this block
    @return Latest balance */
function liveBalanceOf(address _account) public view returns (uint) {
    uint decay = calcDecay(
        lastTransactionBlock[_account],
        _balances[_account],
        block.number,
        zeroBlock[_account]
    );
    uint decayedBalance = _balances[_account].sub(decay);
    if (lastGenerationBlock[_account] == 0) {
        return(decayedBalance);
    }
    uint generationAccrued = calcGeneration(
        block.number,
        lastGenerationBlock[_account],
        lifetime,
        generationAmount,
        generationPeriod
    );
    return decayedBalance.add(generationAccrued);
}

liveBalanceOf should be used by GUIs in place of the balanceOf function, which will return an outdated balance. liveBalanceOf will calculate the up to date balance including generation and decay.

balanceOf will show the correct balance as of the lastTransactionBlock

Alternatively, triggerOnchainBalance can be called to force an update. This will incur gas, but after this balanceOf will return the up to date value.

FusedController

pragma solidity ^0.5.0;

/**
    @title A controllable contract that can incrementally transition to an
        immutable contract
    @author The Value Instrument Team
    @notice Provides special permisions to a controller account or contract,
        while allowing those special permissions to be discarded at any time
    @dev This contract was developed for solc 0.5.8 */

contract FusedController {

    address public controller;
    // address with priviledges to adjust settings, add accounts etc

    bool allFusesBlown = false;
    // set this to true to blow all fuses

    bool[7] fuseBlown;
    /* allows a seperate fuse for distinct functions, so that those functions
    can be disabled */

    event BlowFuse(uint8 _fuseID);
    event BlowAllFuses();
    event ChangeController(address _newController);

    constructor(address _controller) public {
        if(_controller == address(0)){
            controller = msg.sender;
        } else {
            controller = _controller;
        }
    }

    /// Modifiers

    modifier fused(uint8 _fuseID) {
        require(
            allFusesBlown == false,
            "Function fuse has been triggered"
        );
        require(
            fuseBlown[_fuseID] == false,
            "Function fuse has been triggered"
        );
        _;
    }
    modifier onlyController() {
      require(msg.sender == controller, "Controller account/contract only");
      _;
    }

    /// Functions

    function blowAllFuses(bool _confirm)
        external
        onlyController
    {
        require(
            _confirm,
            "This will permanently disable function all fused functions, please set _confirm=true to confirm"
        );
        allFusesBlown = true;
        emit BlowAllFuses();
    }

    function blowFuse(uint8 _fuseID, bool _confirm)
        external
        onlyController
    {
        require(
            _confirm == true,
            "This will permanently disable function, please set _confirm=true to confirm"
        );
        fuseBlown[_fuseID] = true;
        emit BlowFuse(_fuseID);
    }

    function changeController(address _newController)
        external
        onlyController
    {
        controller = _newController;
        emit ChangeController(_newController);
    }
}

FusedController allows functions to be fused by adding the modifier fused(id)

If the controller calls blowFuse(id, true) the fuse will be blown, and the function with the corresponding ID will no longer be able to be called.

This can be useful to allow communities freedom to change generation, decay, fees, and tax during an initial trial period, and later to make those settings immutable.

As an example, here is the updateGenerationPeriod function, with fuse:

/// @notice Update the number of blocks between each generation period
function updateGenerationPeriod(uint _generationPeriod)
    external
    onlyController
    fused(4)
{
    generationPeriod = _generationPeriod;
    emit UpdateGenerationPeriod(_generationPeriod);
}

If the controller calls blowFuse(4, true), it will no longer be possible to change the generation period.

Clone this wiki locally