Vulnerability: An earner can still continue earning even after being removed from the approved list.
Severity: medium

Description:
An earner can still continue earning even after being removed from the approved list.\\nA `M` holder is eligible to earn the `Earner Rate` when they are approved by TTG. The approved `M` holder can call `startEarning()` then begin to earn the `Earner Rate`. They also can `stopEarning()` to quit earning.\\nHowever, when an approved `M` holder is disapproved, only the disapproved holder themselves can choose to stop earning; no one else has the authority to force them to quit earning.\\n`Earner Rate` is calculated in `StableEarnerRateModel#rate()` as below:\\n```\\n    function rate() external view returns (uint256) {\\n        uint256 safeEarnerRate_ = getSafeEarnerRate(\\n            IMinterGateway(minterGateway).totalActiveOwedM(),\\n            IMToken(mToken).totalEarningSupply(),\\n            IMinterGateway(minterGateway).minterRate()\\n        );\\n\\n        return UIntMath.min256(maxRate(), (RATE_MULTIPLIER * safeEarnerRate_) / ONE);\\n    }\\n\\n    function getSafeEarnerRate(\\n        uint240 totalActiveOwedM_,\\n        uint240 totalEarningSupply_,\\n        uint32 minterRate_\\n    ) public pure returns (uint32) {\\n        // solhint-disable max-line-length\\n        // When `totalActiveOwedM_ >= totalEarningSupply_`, it is possible for the earner rate to be higher than the\\n        // minter rate and still ensure cashflow safety over some period of time (`RATE_CONFIDENCE_INTERVAL`). To ensure\\n        // cashflow safety, we start with `cashFlowOfActiveOwedM >= cashFlowOfEarningSupply` over some time `dt`.\\n        // Effectively: p1 * (exp(rate1 * dt) - 1) >= p2 * (exp(rate2 * dt) - 1)\\n        //          So: rate2 <= ln(1 + (p1 * (exp(rate1 * dt) - 1)) / p2) / dt\\n        // 1. totalActive * (delta_minterIndex - 1) >= totalEarning * (delta_earnerIndex - 1)\\n        // 2. totalActive * (delta_minterIndex - 1) / totalEarning >= delta_earnerIndex - 1\\n        // 3. 1 + (totalActive * (delta_minterIndex - 1) / totalEarning) >= delta_earnerIndex\\n        // Substitute `delta_earnerIndex` with `exponent((earnerRate * dt) / SECONDS_PER_YEAR)`:\\n        // 4. 1 + (totalActive * (delta_minterIndex - 1) / totalEarning) >= exponent((earnerRate * dt) / SECONDS_PER_YEAR)\\n        // 5. ln(1 + (totalActive * (delta_minterIndex - 1) / totalEarning)) >= (earnerRate * dt) / SECONDS_PER_YEAR\\n        // 6. ln(1 + (totalActive * (delta_minterIndex - 1) / totalEarning)) * SECONDS_PER_YEAR / dt >= earnerRate\\n\\n        // When `totalActiveOwedM_ < totalEarningSupply_`, the instantaneous earner cash flow must be less than the\\n        // instantaneous minter cash flow. To ensure instantaneous cashflow safety, we we use the derivatives of the\\n        // previous starting inequality, and substitute `dt = 0`.\\n        // Effectively: p1 * rate1 >= p2 * rate2\\n        //          So: rate2 <= p1 * rate1 / p2\\n        // 1. totalActive * minterRate >= totalEarning * earnerRate\\n        // 2. totalActive * minterRate / totalEarning >= earnerRate\\n        // solhint-enable max-line-length\\n\\n        if (totalActiveOwedM_ == 0) return 0;\\n\\n        if (totalEarningSupply_ == 0) return type(uint32).max;\\n\\n        if (totalActiveOwedM_ <= totalEarningSupply_) {//@audit-info rate is slashed\\n            // NOTE: `totalActiveOwedM_ * minterRate_` can revert due to overflow, so in some distant future, a new\\n            //       rate model contract may be needed that handles this differently.\\n            return uint32((uint256(totalActiveOwedM_) * minterRate_) / totalEarningSupply_);\\n        }\\n\\n        uint48 deltaMinterIndex_ = ContinuousIndexingMath.getContinuousIndex(\\n            ContinuousIndexingMath.convertFromBasisPoints(minterRate_),\\n            RATE_CONFIDENCE_INTERVAL\\n        );//@audit-info deltaMinterIndex for 30 days\\n\\n        // NOTE: `totalActiveOwedM_ * deltaMinterIndex_` can revert due to overflow, so in some distant future, a new\\n        //       rate model contract may be needed that handles this differently.\\n        int256 lnArg_ = int256(\\n            _EXP_SCALED_ONE +\\n                ((uint256(totalActiveOwedM_) * (deltaMinterIndex_ - _EXP_SCALED_ONE)) / totalEarningSupply_)\\n        );\\n\\n        int256 lnResult_ = wadLn(lnArg_ * _WAD_TO_EXP_SCALER) / _WAD_TO_EXP_SCALER;\\n\\n        uint256 expRate_ = (uint256(lnResult_) * ContinuousIndexingMath.SECONDS_PER_YEAR) / RATE_CONFIDENCE_INTERVAL;\\n\\n        if (expRate_ > type(uint64).max) return type(uint32).max;\\n\\n        // NOTE: Do not need to do `UIntMath.safe256` because it is known that `lnResult_` will not be negative.\\n        uint40 safeRate_ = ContinuousIndexingMath.convertToBasisPoints(uint64(expRate_));\\n\\n        return (safeRate_ > type(uint32).max) ? type(uint32).max : uint32(safeRate_);\\n    }\\n```\\n\\nAs we can see, the rate may vary due to the changes in `MToken#totalEarningSupply()`, therefore the earning of fixed principal amount could be decreased if `totalEarningSupply()` increases. In some other cases the total earning rewards increases significantly if `totalEarningSupply()` increases, resulting in less `excessOwedM` sending to `ttgVault` when `MinterGateway#updateIndex()` is called.\\nCopy below codes to Integration.t.sol and run `forge test --match-test test_aliceStillEarnAfterDisapproved`\\n```\\n    function test_AliceStillEarnAfterDisapproved() external {\\n\\n        _registrar.updateConfig(MAX_EARNER_RATE, 40000);\\n        _minterGateway.activateMinter(_minters[0]);\\n\\n        uint256 collateral = 1_000_000e6;\\n        _updateCollateral(_minters[0], collateral);\\n\\n        _mintM(_minters[0], 400e6, _bob);\\n        _mintM(_minters[0], 400e6, _alice);\\n        uint aliceInitialBalance = _mToken.balanceOf(_alice);\\n        uint bobInitialBalance = _mToken.balanceOf(_bob);\\n        //@audit-info alice and bob had the same M balance\\n        assertEq(aliceInitialBalance, bobInitialBalance);\\n        //@audit-info alice and bob started earning\\n        vm.prank(_alice);\\n        _mToken.startEarning();\\n        vm.prank(_bob);\\n        _mToken.startEarning();\\n\\n        vm.warp(block.timestamp + 1 days);\\n        uint aliceEarningDay1 = _mToken.balanceOf(_alice) - aliceInitialBalance;\\n        uint bobEarningDay1 = _mToken.balanceOf(_bob) - bobInitialBalance;\\n        //@audit-info Alice and Bob have earned the same M in day 1\\n        assertNotEq(aliceEarningDay1, 0);\\n        assertEq(aliceEarningDay1, bobEarningDay1);\\n        //@audit-info Alice was removed from earner list\\n        _registrar.removeFromList(TTGRegistrarReader.EARNERS_LIST, _alice);\\n        vm.warp(block.timestamp + 1 days);\\n        uint aliceEarningDay2 = _mToken.balanceOf(_alice) - aliceInitialBalance - aliceEarningDay1;\\n        uint bobEarningDay2 = _mToken.balanceOf(_bob) - bobInitialBalance - bobEarningDay1;\\n        //@audit-info Alice still earned M in day 2 even she was removed from earner list, the amount of which is same as Bob's earning\\n        assertNotEq(aliceEarningDay2, 0);\\n        assertEq(aliceEarningDay2, bobEarningDay2);\\n\\n        uint earnerRateBefore = _mToken.earnerRate();\\n        //@audit-info Only Alice can stop herself from earning\\n        vm.prank(_alice);\\n        _mToken.stopEarning();\\n        uint earnerRateAfter = _mToken.earnerRate();\\n        //@audit-info The earning rate was almost doubled after Alice called `stopEarning`\\n        assertApproxEqRel(earnerRateBefore*2, earnerRateAfter, 0.01e18);\\n        vm.warp(block.timestamp + 1 days);\\n        uint aliceEarningDay3 = _mToken.balanceOf(_alice) - aliceInitialBalance - aliceEarningDay1 - aliceEarningDay2;\\n        uint bobEarningDay3 = _mToken.balanceOf(_bob) - bobInitialBalance - bobEarningDay1 - bobEarningDay2;\\n        //@audit-info Alice earned nothing \\n        assertEq(aliceEarningDay3, 0);\\n        //@audit-info Bob's earnings on day 3 were almost twice as much as what he earned on day 2.\\n        assertApproxEqRel(bobEarningDay2*2, bobEarningDay3, 0.01e18);\\n    }\\n```\\n

Impact:
The earnings of eligible users could potentially be diluted.\\nThe `excessOwedM` to ZERO vault holders could be diluted

Recommended mitigation:
Introduce a method that allows anyone to stop the disapproved earner from earning:\\n```\\n    function stopEarning(address account_) external {\\n        if (_isApprovedEarner(account_)) revert IsApprovedEarner();\\n        _stopEarning(account_);\\n    }\\n```\\n