Executing Monetary Policy

Once the Trustee vote is computed and the winning proposal is determined, the new monetary policy is enacted immediately upon the start of the next generation (which coincides with the end of the reveal window). Random Inflation and Interest Lockups both create respective contracts to handle the process. Linear Supply Change is enacted instantly, directly to the token contract.

Monetary Policy Execution - Walkthrough

When the generation increments, the CurrencyTimer.sol contract and the ECO.sol is notified of the generation increment from the TimedPolicies.sol contract. The TimedPolicies.sol contract calls the notifyGenerationIncrease() in both contracts, which executes the relevant monetary policy functions.

The ECO.sol contract is notified first. Below are comments in the code explaining what each step does.

ECO.sol
function notifyGenerationIncrease() public virtual override {
    uint256 _old = currentGeneration;
    uint256 _new = IGeneration(policyFor(ID_TIMED_POLICIES)).generation();
    require(_new != _old, "Generation has not increased");

    // update currentGeneration
    currentGeneration = _new;
    
    // lookup CurrencyGovernance contract for generation
    CurrencyGovernance bg = CurrencyGovernance(
        policyFor(ID_CURRENCY_GOVERNANCE)
    );
    
    // ensure compute function ran, and if not, run it. 
    if (address(bg) != address(0)) {
        if (
            uint8(bg.currentStage()) <
            uint8(CurrencyGovernance.Stage.Compute)
        ) {
            bg.updateStage();
        }
        if (
            uint8(bg.currentStage()) ==
            uint8(CurrencyGovernance.Stage.Compute)
        ) {
            bg.compute();
        }
        
    // if the default proposal doesn't win, enact the new linear inflation
    // multiplier in the ECO contract
        address winner = bg.winner();
        if (winner != address(0)) {
            uint256 _inflationMultiplier;
            (, , , , _inflationMultiplier, ) = bg.proposals(winner);
            emit NewInflationMultiplier(_inflationMultiplier);

            // updates the inflation value
            uint256 _newInflationMultiplier = (_linearInflationCheckpoints[
                _linearInflationCheckpoints.length - 1
            ].value * _inflationMultiplier) / INITIAL_INFLATION_MULTIPLIER;
            _writeCheckpoint(
                _linearInflationCheckpoints,
                _replace,
                _newInflationMultiplier
            );
        }
    }
}

To recap, the contract updates the generation, executes the compute() function if it hasn't been executed to see which Trustee proposal won, then enacts linear inflation.

The CurrencyTimer.sol contract is notified next. Below are comments in the code explaining what each step does.

CurrencyTimer.sol
function notifyGenerationIncrease() external override {
    uint256 _old = currentGeneration;
    uint256 _new = IGeneration(policyFor(ID_TIMED_POLICIES)).generation();
    require(_new != _old, "Generation has not increased");

    // update currentGeneration
    currentGeneration = _new;
    
    // lookup CurrencyGovernance contract for generation
    CurrencyGovernance bg = CurrencyGovernance(
        policyFor(ID_CURRENCY_GOVERNANCE)
    );

    // sets default values for monetary policy
    uint256 _numberOfRecipients = 0;
    uint256 _randomInflationReward = 0;
    uint256 _lockupDuration = 0;
    uint256 _lockupInterest = 0;

    // ensure compute function ran, and if not, run it. 
    // assign winning proposal values
    if (address(bg) != address(0)) {
        if (uint8(bg.currentStage()) < 3) {
            bg.updateStage();
        }
        if (uint8(bg.currentStage()) == 3) {
            bg.compute();
        }
        address winner = bg.winner();
        if (winner != address(0)) {
            (
                _numberOfRecipients,
                _randomInflationReward,
                _lockupDuration,
                _lockupInterest,
                ,

            ) = bg.proposals(winner);
        }
    }
    
    // clone CurrencyGovernance.sol for next generation
    {
        CurrencyGovernance _clone = CurrencyGovernance(bordaImpl.clone());
        policy.setPolicy(
            ID_CURRENCY_GOVERNANCE,
            address(_clone),
            ID_CURRENCY_TIMER
        );
        emit NewCurrencyGovernance(_clone, _new);
    }

    // clone random inflation contract with correct variables if random
    // inflation passed
    if (_numberOfRecipients > 0 && _randomInflationReward > 0) {
        // new inflation contract
        RandomInflation _clone = RandomInflation(inflationImpl.clone());
        ecoToken.mint(
            address(_clone),
            _numberOfRecipients * _randomInflationReward
        );
        _clone.startInflation(_numberOfRecipients, _randomInflationReward);
        emit NewInflation(_clone, _old);
        randomInflations[_old] = _clone;
    }
    
    // clone lockup contract with correct variables if random
    // lockup passed

    if (_lockupDuration > 0 && _lockupInterest > 0) {
        Lockup _clone = Lockup(
            lockupImpl.clone(_lockupDuration, _lockupInterest)
        );
        emit NewLockup(_clone, _old);
        lockups[_old] = _clone;
        isLockup[address(_clone)] = true;
    }
}

To recap, the contract updates the generation, executes the compute() function if it hasn't been executed to see which Trustee proposal won, clones a new CurrencyGovernance contract, then creates Random Inflation and/or Lockup Contract clones if required.

Monetary Policy - Cloned Contracts

Linear Inflation is executed immediately in the core policy contracts, but Random Inflation and Lockup Contracts are managed by cloned contracts. This section explains how those contracts work.

Random Inflation Contracts

When Random Inflation is enacted, a RandomInflation.sol, VDFVerifier.sol, and InflationRootHashProposal.sol contract are cloned. The first of which works as a hub for managing the initiation and payout of Random Inflation, and the other two are helpers for the VDF and gamified Merkle Tree process, respectively.

The VDF allows us to determine a fair blind seed for determining who receives inflation. A prime number is submitted by anyone willing to help facilitate the process to RandomInflation.sol. It must be within the 1000 numbers after the block hash (as uint256) of the block before it is submitted. That serves as the starting point for our VDF. Then, anyone may perform a Pietrzak modular exponentiation puzzle which is verified on chain in the VDFVerifier.sol contract. The final value from this is then read by RandomInflation.sol to act as the seed for Random Inflation.

Parallel to this process the InflationRootHashProposal.sol contract gamifies the process of submitting a Merkle hash of all the past ECO voting power for every user (including delegation, snapshotted at the generation increment that enacted Random Inflation). This Merkle tree has a deterministic generation process so anyone can and should do so off-chain and check their value against any submitted value. If the submitted value doesn't match, a challenger may check addresses and, using a binary search, can find the offending difference in minimal attempts. People who propose the Merkle tree root and people who challenge both submit respective amounts of ECO as collateral. If the proposer is correct and defends every challenge, they get their collateral back as well as the ECO from every challenger. If the challengers correctly show that the Merkle hash is false, they split the total value based on how many challenges they submitted.

Once the Merkle hash is accepted and the seed finalized from the VDF, all the recipients of inflation may be calculated. Note that the Merkle tree is deterministic, so cannot be used to rig the outcome, and the VDF takes long enough to compute that you cannot compute the outcome of Random Inflation in the 1 block window of submitting the VDF start point. This means that the results of Random Inflation can be known before the contracts are ready to start claiming, but at that point, the outcome cannot be changed.

Claiming is done via hashing together the random seed and a claim number (starting at 0 and going up to the number of recipients), taking the value of that hash cast as uint256 modulo the total sum of all ECO voting power and then using the Merkle tree as an ordering of summed balances to see where in the list of all ECO does this number land. A claimant must provide a proof of the Merkle tree position that has won as well as the leaf data and the claim number being claimed. If all this data checks out, the number is marked as claimed and the recipient address is paid out the ECO reward as determined in the monetary policy.

Random Inflation is preallocated with the amount of ECO needed to pay out the contract in its entirety, and this pool is affected by Linear Supply Changes.

Given that Random Inflation drips out over 28 days, this means the Random Inflation contract can have a surplus of ECO at the end, or become insolvent and stop random inflation claiming. Surpluses will need to be swept into the community treasury on a regular cadence.

Lockup Contracts

Lockup contracts are managed by Lockup.sol clones, which manage deposits and interest payouts independently of the core smart contract system and must be tracked separately.

Lockup Contracts are open for 48 hours for deposits. Relevant lockup functions:

  • deposit(uint256 _amount) - allows the deposit of ECO funds into the lockup if within the 48-hour window. This function requires creating an allowance for the transfer of the funds before calling.

  • depositFor(uint256 _amount, address _benefactor) - allows the deposit of ECO funds into the lockup for a benefactor if within the 48-hour deposit window. This function requires creating an allowance for the transfer of the funds before calling.

  • withdraw() - allows withdrawal of ECO funds from the contract. A penalty will be paid if this is before the lockup term is over, equal to the interest that would have been paid out.

  • withdrawFor(address _who) - allows withdrawal of ECO funds from the contract on someone else's behalf. This cannot trigger a penalty.

Last updated