contract CouponClipper {
    using SafeMath for uint;

    IERC20 constant private ESD = IERC20(0x36F3FD68E7325a35EB768F1AedaAe9EA0689d723);
    IESDS constant private ESDS = IESDS(0x443D2f2755DB5942601fa062Cc248aAA153313D3);
    ICHI  constant private CHI = ICHI(0x0000000000004946c0e9F43F4Dee607b0eF1fA1c);

    // HOUSE_RATE_HALVING_AMNT -- Every time a bot brings in 100000ESD for the house, the house's
    // rate will be cut in half *for that bot and that bot alone*:
    // * 50.0% of offer for 0 -> 100000ESD
    // * 25.0% of offer 100000ESD -> 200000ESD
    // * 12.5% of offer 200000ESD -> 300000ESD
    // ...
    uint constant private HOUSE_RATE_HALVING_AMNT = 100000e18;
    uint constant private HOUSE_RATE = 5000; // 50% -- initial portion of the offer taken by house
    uint constant private MAX_OFFER = 10000; // 100% -- obviously you can't offer more than 100%
    
    address public house = 0x871ee4648d0FBB08F39857F41da256659Eab6334; // collector of house take

    // The basis points offered by coupon holders to have their coupons redeemed -- default is 200 bps (2.0%)
    // E.g., offers[_user] = 500 indicates that _user will pay 500 basis points (5%) to the caller
    mapping(address => uint) private offers;
    // The cumulative revenue (in ESD) earned by the house because of a given bot's hard work. Any time
    // this value crosses a multiple of 100000, the house's take rate will be halved.
    // NOTE: This advantage is non-transferrable. Bots are encouraged to keep their address constant
    mapping(address => uint) private houseTakes;
    
    event SetOffer(address indexed user, uint offer);
    
    // frees CHI from msg.sender to reduce gas costs
    // requires that msg.sender has approved this contract to use their CHI
    modifier useCHI {
        uint gasStart = gasleft();
        _;
        uint gasSpent = 21000 + gasStart - gasleft() + (16 * msg.data.length);
        CHI.freeFromUpTo(msg.sender, (gasSpent + 14154) / 41947);
    }

    // @notice Gets the number of basis points the _user is offering the bots
    // @param _user The account whose offer we're looking up.
    // @return The number of basis points the account is offering to have their coupons redeemed
    function getOffer(address _user) public view returns (uint) {
        return offers[_user];
    }

    // @notice Allows msg.sender to change the number of basis points they are offering.
    // @dev _newOffer must be no more than 10_000 (100%)
    // @dev A user's offer cannot be *decreased* during the 15 minutes before the epoch advance (frontrun protection)
    // @param _offer The number of basis points msg.sender wants to offer to have their coupons redeemed.
    function setOffer(uint _newOffer) external {
        require(_newOffer <= MAX_OFFER, "Clipper: Offer too high");

        if (_newOffer < offers[msg.sender]) {
            uint nextEpochStartTime = getEpochStartTime(ESDS.epoch() + 1);
            uint timeUntilNextEpoch = nextEpochStartTime.sub(block.timestamp);
            require(timeUntilNextEpoch > 15 minutes, "Clipper: Wait until next epoch");
        }
        
        offers[msg.sender] = _newOffer;
        emit SetOffer(msg.sender, _newOffer);
    }
    
    // @notice Internal logic used to redeem coupons on the coupon holder's bahalf
    // @param _user Address of the user holding the coupons (and who has approved this contract)
    // @param _epoch The epoch in which the _user purchased the coupons
    // @param _couponAmount The number of coupons to redeem (18 decimals)
    // @return the fee (in ESD) owned to the bot (msg.sender)
    function _redeem(address _user, uint _epoch, uint _couponAmount) internal returns (uint) {
        // pull user's coupons into this contract (requires that the user has approved this contract)
        try ESDS.transferCoupons(_user, address(this), _epoch, _couponAmount) {
            // redeem the coupons for ESD
            try ESDS.redeemCoupons(_epoch, _couponAmount) {
                // compute fees
                uint fee = _couponAmount.mul(getOffer(_user)).div(10_000);
                // send the ESD to the user
                ESD.transfer(_user, _couponAmount.sub(fee)); // @audit-info : reverts on failure

                // (x >> y) is equivalent to (x / 2**y) for positive integers
                uint houseRate = HOUSE_RATE >> houseTakes[tx.origin].div(HOUSE_RATE_HALVING_AMNT);
                uint houseFee = fee.mul(houseRate).div(10_000);
                houseTakes[tx.origin] = houseTakes[tx.origin].add(houseFee);

                // return the bot fee
                return fee.sub(houseFee);
            } catch {
                // In this block the transfer succeeded but redemption failed, so we need to undo the transfer!!
                ESDS.transferCoupons(address(this), _user, _epoch, _couponAmount);
                return 0;
            }
        } catch {
            return 0;
        }
    }

    // @notice Internal logic used to redeem coupons on the coupon holder's bahalf
    // @param _users Addresses of users holding the coupons (and who has approved this contract)
    // @param _epochs The epochs in which the _users purchased the coupons
    // @param _couponAmounts The numbers of coupons to redeem (18 decimals)
    // @return the total fee (in ESD) owned to the bot (msg.sender)
    function _redeemMany(address[] calldata _users, uint[] calldata _epochs, uint[] calldata _couponAmounts) internal returns (uint) {
        // 0 by default, would cost extra gas to make that explicit
        uint botFee;

        for (uint i = 0; i < _users.length; i++) {
            botFee = botFee.add(_redeem(_users[i], _epochs[i], _couponAmounts[i]));
        }

        return botFee;
    }
    
    // @notice Allows anyone to redeem coupons for ESD on the coupon-holder's bahalf
    // @dev Backwards compatible with CouponClipper V1.
    function redeem(address _user, uint _epoch, uint _couponAmount) external {
        ESD.transfer(msg.sender, _redeem(_user, _epoch, _couponAmount));
    }

    function redeemMany(address[] calldata _users, uint[] calldata _epochs, uint[] calldata _couponAmounts) external {
        ESD.transfer(msg.sender, _redeemMany(_users, _epochs, _couponAmounts));
    }
    
    // @notice Advances the epoch (if needed) and redeems the max amount of coupons possible
    //    Also frees CHI tokens to save on gas (requires that msg.sender has CHI tokens in their
    //    account and has approved this contract to spend their CHI).
    // @param _user The user whose coupons will attempt to be redeemed
    // @param _epoch The epoch in which the coupons were created
    // @param _targetEpoch The epoch that is about to be advanced _to_.
    //    E.g., if the current epoch is 220 and we are about to advance to to epoch 221, then _targetEpoch
    //    would be set to 221. The _targetEpoch is the epoch in which the coupon redemption will be attempted.
    function advanceAndRedeem(address _user, uint _epoch, uint _targetEpoch) external useCHI {
        // End execution early if tx is mined too early
        if (block.timestamp < getEpochStartTime(_targetEpoch)) return;
        
        // advance epoch if it has not already been advanced 
        if (ESDS.epoch() != _targetEpoch) ESDS.advance();
        
        // get max redeemable amount
        uint totalRedeemable = ESDS.totalRedeemable();
        if (totalRedeemable == 0) return; // no coupons to redeem
        uint userBalance = ESDS.balanceOfCoupons(_user, _epoch);
        if (userBalance == 0) return; // no coupons to redeem
        uint maxRedeemableAmount = totalRedeemable < userBalance ? totalRedeemable : userBalance;
        
        // attempt to redeem coupons
        ESD.transfer(msg.sender, _redeem(_user, _epoch, maxRedeemableAmount));
    }

    // @notice Advances the epoch (if needed) and redeems the max amount of coupons possible
    //    Also frees CHI tokens to save on gas (requires that msg.sender has CHI tokens in their
    //    account and has approved this contract to spend their CHI).
    // @param _users The users whose coupons will attempt to be redeemed
    // @param _epochs The epochs in which the coupons were created
    // @param _targetEpoch The epoch that is about to be advanced _to_.
    //    E.g., if the current epoch is 220 and we are about to advance to to epoch 221, then _targetEpoch
    //    would be set to 221. The _targetEpoch is the epoch in which the coupon redemption will be attempted.
    function advanceAndRedeemMany(address[] calldata _users, uint[] calldata _epochs, uint _targetEpoch) external useCHI {
        // End execution early if tx is mined too early
        if (block.timestamp < getEpochStartTime(_targetEpoch)) return;

        // Advance the epoch if necessary
        if (ESDS.epoch() != _targetEpoch) ESDS.advance();
        
        // 0 by default, would cost extra gas to make that explicit
        uint botFee;
        uint amtToRedeem;
        uint totalRedeemable = ESDS.totalRedeemable();

        for (uint i = 0; i < _users.length; i++) {
            if (totalRedeemable == 0) break;

            amtToRedeem = ESDS.balanceOfCoupons(_users[i], _epochs[i]);
            if (totalRedeemable < amtToRedeem) amtToRedeem = totalRedeemable;

            botFee = botFee.add(_redeem(_users[i], _epochs[i], amtToRedeem));
            totalRedeemable = totalRedeemable.sub(amtToRedeem);
        }

        ESD.transfer(msg.sender, botFee);
    }

    
    // @notice Returns the timestamp at which the _targetEpoch starts
    function getEpochStartTime(uint _targetEpoch) public pure returns (uint) {
        return _targetEpoch.sub(106).mul(28800).add(1602201600);
    }
    
    // @notice Allows house address to change the house address
    function changeHouseAddress(address _newAddress) external {
        require(msg.sender == house);
        house = _newAddress;
    }

    // @notice Allows house to withdraw accumulated fees
    function withdraw(address _token, uint _amount) external {
        IERC20(_token).transfer(house, _amount);
    }
}
// @notice Lets anybody trustlessly redeem coupons on anyone else's behalf for a fee.
//    Requires that the coupon holder has previously approved this contract via the ESDS `approveCoupons` function.
// @dev Bots should scan for the `CouponApproval` event emitted by the ESDS `approveCoupons` function to find out which 
//    users have approved this contract to redeem their coupons.
// @dev This contract's API should be backwards compatible with other CouponClippers.
function advanceAndRedeemMany(address[] calldata _users, uint[] calldata _epochs, uint _targetEpoch) external useCHI {
    // End execution early if tx is mined too early
    if (block.timestamp < getEpochStartTime(_targetEpoch)) return;

    // Advance the epoch if necessary
    if (ESDS.epoch() != _targetEpoch) ESDS.advance();
        
    // 0 by default, would cost extra gas to make that explicit
    uint botFee;
    uint amtToRedeem;
    uint totalRedeemable = ESDS.totalRedeemable();

    for (uint i = 0; i < _users.length; i++) {
        if (totalRedeemable == 0) break;

        amtToRedeem = ESDS.balanceOfCoupons(_users[i], _epochs[i]);
        if (totalRedeemable < amtToRedeem) amtToRedeem = totalRedeemable;

        botFee = botFee.add(_redeem(_users[i], _epochs[i], amtToRedeem));
        totalRedeemable = totalRedeemable.sub(amtToRedeem);
    }

    ESD.transfer(msg.sender, botFee);
}

// @notice Advances the epoch (if needed) and redeems the max amount of coupons possible
//    Also frees CHI tokens to save on gas (requires that msg.sender has CHI tokens in their
//    account and has approved this contract to spend their CHI).
// @param _users The users whose coupons will attempt to be redeemed
// @param _epochs The epochs in which the coupons were created
// @param _targetEpoch The epoch that is about to be advanced _to_.
//    E.g., if the current epoch is 220 and we are about to advance to to epoch 221, then _targetEpoch
//    would be set to 221. The _targetEpoch is the epoch in which the coupon redemption will be attempted.