all files / p1/ Furnace.sol

100% Statements 13/13
100% Branches 14/14
100% Functions 3/3
100% Lines 16/16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100                                                                    44× 44× 44× 44× 44×                                                       7854×     7704×     7704×   7704×   7704× 7704× 7704×             57× 57×   56× 56×                    
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.17;
 
import "../libraries/Fixed.sol";
import "../interfaces/IFurnace.sol";
import "./mixins/Component.sol";
 
/**
 * @title FurnaceP1
 * @notice A helper to melt RTokens continuously and permisionlessly.
 */
contract FurnaceP1 is ComponentP1, IFurnace {
    using FixLib for uint192;
 
    uint192 public constant MAX_RATIO = FIX_ONE; // {1} 100%
    uint48 public constant PERIOD = 12; // {s} 12 seconds; 1 block on PoS Ethereum
 
    IRToken private rToken;
 
    // === Governance params ===
    uint192 public ratio; // {1} What fraction of balance to melt each period
 
    // === Cached ===
    uint48 public lastPayout; // {seconds} The last time we did a payout
    uint256 public lastPayoutBal; // {qRTok} The balance of RToken at the last payout
 
    // ==== Invariants ====
    // ratio <= MAX_RATIO = 1e18
    // lastPayout was the timestamp of the end of the last period we paid out
    //   (or, if no periods have been paid out, the timestamp init() was called)
    // lastPayoutBal was rtoken.balanceOf(this) after the last period we paid out
    //   (or, if no periods have been paid out, that balance when init() was called)
 
    function init(IMain main_, uint192 ratio_) external initializer {
        __Component_init(main_);
        rToken = main_.rToken();
        setRatio(ratio_);
        lastPayout = uint48(block.timestamp);
        lastPayoutBal = rToken.balanceOf(address(this));
    }
 
    // [furnace-payout-formula]:
    //   The process we're modelling is:
    //     N = number of whole periods since lastPayout
    //     bal_0 = rToken.balanceOf(this)
    //     payout_{i+1} = bal_i * ratio
    //     bal_{i+1} = bal_i - payout_{i+1}
    //     payoutAmount = sum{payout_i for i in [1...N]}
    //   thus:
    //     bal_N = bal_0 - payout
    //     bal_{i+1} = bal_i - bal_i * ratio = bal_i * (1-ratio)
    //     bal_N = bal_0 * (1-ratio)**N
    //   and so:
    //     payoutAmount = bal_N - bal_0 = bal_0 * (1 - (1-ratio)**N)
 
    /// Performs any melting that has vested since last call.
    /// @custom:refresher
    // let numPeriods = number of whole periods that have passed since `lastPayout`
    //     payoutAmount = RToken.balanceOf(this) * (1 - (1-ratio)**N) from [furnace-payout-formula]
    // effects:
    //   lastPayout' = lastPayout + numPeriods * PERIOD (end of last pay period)
    //   lastPayoutBal' = rToken.balanceOf'(this) (balance now == at end of pay leriod)
    // actions:
    //   rToken.melt(payoutAmount), paying payoutAmount to RToken holders
 
    function melt() external notPausedOrFrozen {
        if (uint48(block.timestamp) < uint64(lastPayout) + PERIOD) return;
 
        // # of whole periods that have passed since lastPayout
        uint48 numPeriods = uint48((block.timestamp) - lastPayout) / PERIOD;
 
        // Paying out the ratio r, N times, equals paying out the ratio (1 - (1-r)^N) 1 time.
        uint192 payoutRatio = FIX_ONE.minus(FIX_ONE.minus(ratio).powu(numPeriods));
 
        uint256 amount = payoutRatio.mulu_toUint(lastPayoutBal);
 
        lastPayout += numPeriods * PERIOD;
        lastPayoutBal = rToken.balanceOf(address(this)) - amount;
        if (amount > 0) rToken.melt(amount);
    }
 
    /// Ratio setting
    /// @custom:governance
    function setRatio(uint192 ratio_) public governance {
        // solhint-disable-next-line no-empty-blocks
        if (lastPayout > 0) try this.melt() {} catch {}
        require(ratio_ <= MAX_RATIO, "invalid ratio");
        // The ratio can safely be set to 0 to turn off payouts, though it is not recommended
        emit RatioSet(ratio, ratio_);
        ratio = ratio_;
    }
 
    /**
     * @dev This empty reserved space is put in place to allow future versions to add new
     * variables without shifting down storage in the inheritance chain.
     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
     */
    uint256[47] private __gap;
}