contract xBNT is ERC20UpgradeSafe, OwnableUpgradeSafe, PausableUpgradeSafe {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    IERC20 private bnt;
    IERC20 private vbnt;
    IContractRegistry private contractRegistry;
    IBancorGovernance internal bancorGovernance;
    IMinimalProxyFactory private proxyFactory;

    bytes32 private constant bancorNetworkName = 'BancorNetwork';
    bytes32 private constant stakingRewardsName = 'StakingRewards';
    bytes32 private constant liquidityProtectionName = 'LiquidityProtection';

    uint32 private constant PPM = 1000000;
    uint256 private constant DEC_18 = 1e18;
    uint256 private constant BUFFER_TARGET = 20; // 5%
    uint256 private constant MAX_UINT = 2**256 - 1;
    uint256 private constant WAITING_PERIOD = 2 days;
    uint256 private constant INITIAL_SUPPLY_MULTIPLIER = 10;
    uint256 private constant LIQUIDATION_TIME_PERIOD = 4 weeks;

    uint256 private lpImplementationChangedTimestamp;
    uint256 private governanceAddressChangedTimestamp;

    uint256 public adminActiveTimestamp;

    uint256 public lowestActiveProxyIndex;
    uint256 public nextProxyIndex;

    uint256 public totalAllocatedNav;
    uint256 public pendingRewardsContributionToNav;

    uint256 public withdrawableBntFees;

    address private manager;
    address private manager2;
    address internal liquidityProviderImplementation;

    address private queuedLiquidityProviderImplementation;
    address private queuedGovernanceAddress;

    address private constant ZERO_ADDRESS = address(0);

    struct FeeDivisors {
        uint256 mintFee;
        uint256 burnFee;
        uint256 claimFee;
    }

    FeeDivisors public feeDivisors;

    struct Deposit {
        address proxyAddress;
        uint256 depositId;
        uint256 initialContribution;
        uint256 latestContributionToNav;
    }

    mapping(uint256 => Deposit) public depositIdToDeposit;

    struct ProxyData {
        uint256[] depositIds;
        uint256 pendingRewardsContributionToNav;
        uint256 deployedBnt;
        bool balanceClaimed;
    }

    mapping(address => ProxyData) private proxyAddressToData;
    mapping(uint256 => address) public proxyIndexToAddress;

    event AddLiquidity(
        address poolToken,
        uint256 amount,
        uint256 depositId,
        uint256 proxyIndex,
        uint256 timestamp
    );

    event ClaimRestake(
        uint256 proxyIndex,
        uint256 amount,
        uint256 depositId,
        uint256 timestamp
    );

    event ClaimRemove(uint256 proxyIndex, uint256 rewardsClaimed);

    event ClaimRewards(uint256 proxyIndex, uint256 rewardsClaimed);

    event ClaimBalance(uint256 proxyIndex);

    event RewardsNavUpdated(
        uint256 previousRewardsNav,
        uint256 newRewardsNav,
        uint256 timestamp
    );

    event KeyAddressChange();

    function initialize(
        IERC20 _bnt,
        IERC20 _vbnt,
        IContractRegistry _contractRegistry,
        IBancorGovernance _bancorGovernance,
        IMinimalProxyFactory _proxyFactory,
        address _liquidityProviderImplementation,
        uint256 _mintFeeDivisor,
        uint256 _burnFeeDivisor,
        uint256 _claimFeeDivisor,
        string memory _symbol
    ) public initializer {
        __ERC20_init('xBNT', _symbol);
        __Ownable_init();
        __Pausable_init();

        bnt = _bnt;
        vbnt = _vbnt;
        contractRegistry = _contractRegistry;
        bancorGovernance = _bancorGovernance;
        proxyFactory = _proxyFactory;
        liquidityProviderImplementation = _liquidityProviderImplementation;

        _setFeeDivisors(_mintFeeDivisor, _burnFeeDivisor, _claimFeeDivisor);
        _updateAdminActiveTimestamp();
    }

    /* ========================================================================================= */
    /*                                          User-Facing                                      */
    /* ========================================================================================= */

    /*
     * @notice Mint xBNT using ETH
     * @param path: BancorNetwork trade path
     * @param minReturn: BancorNetwork trade minReturn
     */
    function mint(address[] calldata path, uint256 minReturn)
        external
        payable
        whenNotPaused
    {
        require(msg.value > 0, 'Must send ETH');

        uint256 incrementalBnt =
            IBancorNetwork(contractRegistry.addressOf(bancorNetworkName))
                .convertByPath{value: msg.value}(
                path,
                msg.value,
                minReturn,
                ZERO_ADDRESS,
                ZERO_ADDRESS,
                0
            );

        _mintInternal(incrementalBnt);
    }

    /*
     * @notice Mint xBNT using BNT
     * @notice Must run approval first
     * @param bntAmount: BNT amount
     */
    function mintWithToken(uint256 bntAmount) external whenNotPaused {
        require(bntAmount > 0, 'Must send BNT');

        bnt.transferFrom(msg.sender, address(this), bntAmount);

        _mintInternal(bntAmount);
    }

    function _mintInternal(uint256 _incrementalBnt) private {
        uint256 fee =
            _calculateAndIncrementFee(_incrementalBnt, feeDivisors.mintFee);

        uint256 mintAmount =
            calculateMintAmount(_incrementalBnt.sub(fee), totalSupply());

        super._mint(msg.sender, mintAmount);
    }

    function calculateMintAmount(uint256 incrementalBnt, uint256 totalSupply)
        public
        view
        returns (uint256 mintAmount)
    {
        if (totalSupply == 0)
            return incrementalBnt.mul(INITIAL_SUPPLY_MULTIPLIER);

        mintAmount = (incrementalBnt).mul(totalSupply).div(
            getNav().sub(incrementalBnt)
        );
    }

    /*
     * @notice Burn xBNT
     * @notice Will fail if pro rata BNT is more than buffer balance
     * @param redeemAmount: xBNT to burn
     * @param redeemForEth: Redeem for ETH or BNT
     * @param path: If redeem for ETH, BancorNetwork path
     * @param minReturn: If redeem for ETH, BancorNetwork minReturn
     */
    function burn(
        uint256 redeemAmount,
        bool redeemForEth,
        address[] memory path,
        uint256 minReturn
    ) public {
        require(redeemAmount > 0, 'Must send xBNT');

        uint256 bufferBalance = getBufferBalance();
        uint256 proRataBnt = getNav().mul(redeemAmount).div(totalSupply());
        require(
            proRataBnt <= bufferBalance,
            'Burn exceeds available liquidity'
        );

        super._burn(msg.sender, redeemAmount);
        uint256 fee =
            _calculateAndIncrementFee(proRataBnt, feeDivisors.burnFee);

        if (redeemForEth) {
            address bancorNetworkAddress =
                contractRegistry.addressOf(bancorNetworkName);
            _approveIfNecessary(bnt, bancorNetworkAddress); // in case registry addr has changed

            uint256 ethRedemption =
                IBancorNetwork(bancorNetworkAddress).convertByPath(
                    path,
                    proRataBnt.sub(fee),
                    minReturn,
                    ZERO_ADDRESS,
                    ZERO_ADDRESS,
                    0
                );
            (bool success, ) = msg.sender.call.value(ethRedemption)('');
            require(success, 'Transfer failed');
        } else {
            bnt.transfer(msg.sender, proRataBnt.sub(fee));
        }
    }

    /* ========================================================================================= */
    /*                                      Liquidity Provision                                  */
    /* ========================================================================================= */

    /*
     * @notice Makes BNT deposit on Bancor
     * @notice Deploys new proxy
     * @notice Allocates buffer BNT to allocated NAV
     * @param _poolAnchor: Address of liquidity pool
     * @param _amount: BNT amount
     */
    function addLiquidity(IConverterAnchor _poolAnchor, uint256 _amount)
        external
        onlyOwnerOrManager
    {
        uint256 salt =
            uint256(keccak256(abi.encodePacked(nextProxyIndex, _amount)));
        address liquidityProviderProxy =
            proxyFactory.deploy(salt, liquidityProviderImplementation);

        bnt.transfer(liquidityProviderProxy, _amount);

        uint256 depositId =
            ILiquidityProvider(liquidityProviderProxy)
                .initializeAndAddLiquidity(
                contractRegistry,
                address(this),
                bnt,
                vbnt,
                address(_poolAnchor),
                _amount
            );

        Deposit memory newDeposit =
            Deposit({
                proxyAddress: liquidityProviderProxy,
                depositId: depositId,
                initialContribution: _amount,
                latestContributionToNav: _amount
            });

        emit AddLiquidity(
            address(_poolAnchor),
            _amount,
            depositId,
            nextProxyIndex,
            block.timestamp
        );

        depositIdToDeposit[depositId] = newDeposit;

        ProxyData storage proxyData =
            proxyAddressToData[liquidityProviderProxy];
        proxyData.depositIds.push(depositId);
        proxyData.deployedBnt = _amount;

        proxyIndexToAddress[nextProxyIndex] = liquidityProviderProxy;
        nextProxyIndex++;

        totalAllocatedNav = totalAllocatedNav.add(_amount);

        _stake(_amount);
        _updateAdminActiveTimestamp();
    }

    /*
     * @notice Restakes rewards from current deposit into new deposit
     * @notice Deploys capital to same proxy as current deposit
     * @notice Allocates from rewards NAV to allocated NAV
     * @param proxyIndex: Proxy index
     * @param poolToken: Pool to restake rewards to
     */
    function claimAndRestake(uint256 proxyIndex, address poolToken)
        external
        onlyOwnerOrManager
    {
        address proxyAddress = proxyIndexToAddress[proxyIndex];
        ProxyData storage proxyData = proxyAddressToData[proxyAddress];

        ILiquidityProvider lpProxy = ILiquidityProvider(proxyAddress);

        (uint256 newDepositId, uint256 restakedBal) =
            lpProxy.claimAndRestake(poolToken);

        // fee effectively deducted from buffer balance
        // because full rewards are restaked without cycling through xBNT
        _calculateAndIncrementFee(restakedBal, feeDivisors.claimFee);

        proxyData.depositIds.push(newDepositId);
        proxyData.deployedBnt = proxyData.deployedBnt.add(restakedBal);

        // zero out restaked rewards
        pendingRewardsContributionToNav = pendingRewardsContributionToNav.sub(
            proxyData.pendingRewardsContributionToNav
        );
        proxyData.pendingRewardsContributionToNav = 0;

        // add restaked rewards back to nav
        totalAllocatedNav = totalAllocatedNav.add(restakedBal);

        depositIdToDeposit[newDepositId] = Deposit({
            proxyAddress: proxyAddress,
            depositId: newDepositId,
            initialContribution: restakedBal,
            latestContributionToNav: restakedBal
        });

        emit ClaimRestake(
            proxyIndex,
            restakedBal,
            newDepositId,
            block.timestamp
        );

        _stake(restakedBal);
        _updateAdminActiveTimestamp();
    }

    /*
     * @notice Iterates through proxies to calculate current available rewards
     * @notice Must be called daily or more to stay current with NAV
     * @notice We specify begin/end indices in case num proxies approaches gas limit
     * @param beginProxyIndexIterator: proxyIndex to begin iteration
     * @param endProxyIndexIterator: proxyIndex to end iteration
     */
    function updatePendingRewardsContributionToNav(
        uint256 beginProxyIndexIterator,
        uint256 endProxyIndexIterator
    ) external {
        require(
            beginProxyIndexIterator >= lowestActiveProxyIndex,
            'Invalid index'
        );
        require(endProxyIndexIterator <= nextProxyIndex, 'Invalid index');
        require(
            endProxyIndexIterator > beginProxyIndexIterator,
            'Invalid order'
        );

        IStakingRewards stakingRewards = getStakingRewardsContract();

        uint256 replacedPendingRewardsContributionToNav;
        uint256 updatedPendingRewardsContributionToNav;

        for (uint256 i = lowestActiveProxyIndex; i < nextProxyIndex; i++) {
            address proxyAddress = proxyIndexToAddress[i];
            replacedPendingRewardsContributionToNav = replacedPendingRewardsContributionToNav
                .add(
                proxyAddressToData[proxyAddress].pendingRewardsContributionToNav
            );

            uint256 newContributionToRewardsNav =
                stakingRewards.pendingRewards(proxyAddress);

            proxyAddressToData[proxyAddress]
                .pendingRewardsContributionToNav = newContributionToRewardsNav;
            updatedPendingRewardsContributionToNav = updatedPendingRewardsContributionToNav
                .add(newContributionToRewardsNav);
        }

        emit RewardsNavUpdated(
            pendingRewardsContributionToNav,
            updatedPendingRewardsContributionToNav,
            block.timestamp
        );

        pendingRewardsContributionToNav = pendingRewardsContributionToNav
            .add(updatedPendingRewardsContributionToNav)
            .sub(replacedPendingRewardsContributionToNav);
    }

    /*
     * @notice Updates NAV for value of deposits
     * @notice Needs to be called weekly at least
     * @notice Due to IL protection, allocated NAV is assumed to be the greater of value
     * of initial deposit or  removeLiquidityReturn
     * @notice We specify begin/end indices in case num deposits approaches gas limit
     * @param beginProxyIndexIterator: proxyIndex to begin iteration
     * @param endProxyIndexIterator: proxyIndex to end iteration
     */
    function updateTotalAllocatedNav(
        uint256 beginProxyIndexIterator,
        uint256 endProxyIndexIterator
    ) external {
        require(
            beginProxyIndexIterator >= lowestActiveProxyIndex,
            'Invalid index'
        );
        require(endProxyIndexIterator <= nextProxyIndex, 'Invalid index');

        ILiquidityProtection lp = getLiquidityProtectionContract();

        uint256[] memory depositIds;
        uint256 newContributionToAllocatedNav;

        for (
            uint256 i = beginProxyIndexIterator;
            i < endProxyIndexIterator;
            i++
        ) {
            depositIds = proxyAddressToData[proxyIndexToAddress[i]].depositIds;

            for (uint256 j = 0; j < depositIds.length; j++) {
                (newContributionToAllocatedNav, , ) = lp.removeLiquidityReturn(
                    depositIds[j],
                    PPM,
                    block.timestamp.add(100 days)
                );

                Deposit storage deposit = depositIdToDeposit[depositIds[j]];

                // we do this check here only modifying state if contribution is higher
                // than previous, implicitly capturing Bancor's IL protection framework
                if (
                    newContributionToAllocatedNav >
                    deposit.latestContributionToNav
                ) {
                    totalAllocatedNav = totalAllocatedNav
                        .sub(deposit.latestContributionToNav)
                        .add(newContributionToAllocatedNav);
                    deposit
                        .latestContributionToNav = newContributionToAllocatedNav;
                }
            }
        }
    }

    /*
     * @notice Removes all deposits from proxy at lowestActiveProxyIndex
     */
    function claimRewardsAndRemoveLiquidity() external onlyOwnerOrManager {
        _claimRewardsAndRemoveLiquidity();
        _updateAdminActiveTimestamp();
    }

    function emergencyClaimAndRemove() external liquidationTimeElapsed {
        _claimRewardsAndRemoveLiquidity();
    }

    function _claimRewardsAndRemoveLiquidity() private {
        address proxyAddress = proxyIndexToAddress[lowestActiveProxyIndex];
        ILiquidityProvider lpProxy = ILiquidityProvider(proxyAddress);
        ProxyData storage proxyData = proxyAddressToData[proxyAddress];

        // rewards nav reallocated implicitly to buffer balance
        pendingRewardsContributionToNav = pendingRewardsContributionToNav.sub(
            proxyData.pendingRewardsContributionToNav
        );
        proxyData.pendingRewardsContributionToNav = 0;

        _unstake(proxyData.deployedBnt);
        vbnt.transfer(proxyAddress, proxyData.deployedBnt);

        uint256 rewardsClaimed = lpProxy.claimRewardsAndRemoveLiquidity();
        _calculateAndIncrementFee(rewardsClaimed, feeDivisors.claimFee);

        emit ClaimRemove(lowestActiveProxyIndex, rewardsClaimed);

        // we don't deduct totalAllocatedNav yet because we need to wait
        // 24 hours to `claimBalance`. Only rewards are immediately retrieved

        lowestActiveProxyIndex++;
        _updateAdminActiveTimestamp();
    }

    /*
     * @notice Second step in removal process
     * @notice Claims deposits balance 24 hrs after `claimRewardsAndRemoveLiquidity` called
     * @param proxyIndex: proxyIndex
     */
    function claimBalance(uint256 proxyIndex) external onlyOwnerOrManager {
        _claimBalance(proxyIndex);
        _updateAdminActiveTimestamp();
    }

    function emergencyClaimBalance(uint256 proxyIndex)
        external
        liquidationTimeElapsed
    {
        _claimBalance(proxyIndex);
    }

    function _claimBalance(uint256 _proxyIndex) private {
        address proxyAddress = proxyIndexToAddress[_proxyIndex];
        ProxyData memory proxyData = proxyAddressToData[proxyAddress];

        require(!proxyData.balanceClaimed, 'Already claimed');
        proxyAddressToData[proxyAddress].balanceClaimed = true;

        ILiquidityProvider lpProxy = ILiquidityProvider(proxyAddress);
        lpProxy.claimBalance();

        uint256 contributionToTotalAllocatedNav;

        uint256[] memory depositIds = proxyData.depositIds;
        for (uint256 i = 0; i < depositIds.length; i++) {
            contributionToTotalAllocatedNav = contributionToTotalAllocatedNav
                .add(depositIdToDeposit[depositIds[i]].latestContributionToNav);
        }

        emit ClaimBalance(_proxyIndex);

        // allocatedNav now becomes bnt buffer balance
        totalAllocatedNav = totalAllocatedNav.sub(
            contributionToTotalAllocatedNav
        );
    }

    /*
     * @notice Claims rewards from a proxy without restaking
     * @notice Will reset rewards multiplier - use sparingly when buffer balance needed
     */
    function claimRewards(uint256 proxyIndex) external onlyOwnerOrManager {
        address proxyAddress = proxyIndexToAddress[lowestActiveProxyIndex];
        ILiquidityProvider lpProxy = ILiquidityProvider(proxyAddress);

        uint256 proxyContributionToRewardsNav =
            getProxyAddressRewardsContributionToNav(proxyAddress);
        pendingRewardsContributionToNav = pendingRewardsContributionToNav.sub(
            proxyContributionToRewardsNav
        );
        proxyAddressToData[proxyAddress].pendingRewardsContributionToNav = 0;

        uint256 rewards = lpProxy.claimRewards();
        _calculateAndIncrementFee(rewards, feeDivisors.claimFee);
        _updateAdminActiveTimestamp();

        emit ClaimRewards(proxyIndex, rewards);
    }

    function getLiquidityProtectionContract()
        public
        view
        returns (ILiquidityProtection)
    {
        return
            ILiquidityProtection(
                contractRegistry.addressOf(liquidityProtectionName)
            );
    }

    function getStakingRewardsContract() public view returns (IStakingRewards) {
        return IStakingRewards(contractRegistry.addressOf(stakingRewardsName));
    }

    /* ========================================================================================= */
    /*                                             Utils                                         */
    /* ========================================================================================= */

    function getProxyAddressDepositIds(address proxyAddress)
        public
        view
        returns (uint256[] memory)
    {
        return proxyAddressToData[proxyAddress].depositIds;
    }

    function getProxyAddressRewardsContributionToNav(address proxyAddress)
        public
        view
        returns (uint256)
    {
        return proxyAddressToData[proxyAddress].pendingRewardsContributionToNav;
    }

    function changeLiquidityProviderImplementation(address newImplementation)
        external
        onlyOwner
    {
        queuedLiquidityProviderImplementation = newImplementation;
        lpImplementationChangedTimestamp = block.timestamp;
        emit KeyAddressChange();
    }

    function confirmLiquidityProviderImplementationChange() external onlyOwner {
        require(
            block.timestamp >
                lpImplementationChangedTimestamp.add(WAITING_PERIOD),
            'Too soon'
        );
        liquidityProviderImplementation = queuedLiquidityProviderImplementation;
    }

    function changeGovernanceAddress(address newAddress) external onlyOwner {
        queuedGovernanceAddress = newAddress;
        governanceAddressChangedTimestamp = block.timestamp;
        emit KeyAddressChange();
    }

    function confirmGovernanceAddressChange() external onlyOwner {
        require(
            block.timestamp >
                governanceAddressChangedTimestamp.add(WAITING_PERIOD),
            'Too soon'
        );
        bancorGovernance = IBancorGovernance(queuedGovernanceAddress);
    }

    /* ========================================================================================= */
    /*                                           Governance                                      */
    /* ========================================================================================= */

    // we should probably have a setter in case bancor gov address changes
    function _stake(uint256 _amount) private {
        bancorGovernance.stake(_amount);
    }

    function _unstake(uint256 _amount) private {
        bancorGovernance.unstake(_amount);
    }

    /* ========================================================================================= */
    /*                                               NAV                                         */
    /* ========================================================================================= */

    function getTargetBufferBalance() public view returns (uint256) {
        return getNav().div(BUFFER_TARGET);
    }

    function getNav() public view returns (uint256) {
        return
            totalAllocatedNav.add(getRewardsContributionToNav()).add(
                getBufferBalance()
            );
    }

    function getRewardsContributionToNav() public view returns (uint256) {
        uint256 unassessedFees =
            pendingRewardsContributionToNav.div(feeDivisors.claimFee);
        return pendingRewardsContributionToNav.sub(unassessedFees);
    }

    function getBufferBalance() public view returns (uint256) {
        uint256 bntBal = bnt.balanceOf(address(this));
        if (bntBal < withdrawableBntFees) return 0;
        return bntBal.sub(withdrawableBntFees);
    }

    function _calculateFee(uint256 _value, uint256 _feeDivisor)
        internal
        pure
        returns (uint256 fee)
    {
        if (_feeDivisor > 0 && _value > 0) {
            fee = _value.div(_feeDivisor);
        }
    }

    function _incrementWithdrawableBntFees(uint256 _feeAmount) private {
        withdrawableBntFees = withdrawableBntFees.add(_feeAmount);
    }

    function _calculateAndIncrementFee(uint256 _value, uint256 _feeDivisor)
        private
        returns (uint256 fee)
    {
        fee = _calculateFee(_value, _feeDivisor);
        _incrementWithdrawableBntFees(fee);
    }

    function setDelegate(
        address delegateRegistry,
        bytes32 id,
        address delegate
    ) external onlyOwnerOrManager {
        IDelegateRegistry(delegateRegistry).setDelegate(id, delegate);
    }

    /* ========================================================================================= */
    /*                                              Utils                                        */
    /* ========================================================================================= */

    function _approveIfNecessary(IERC20 _token, address _toApprove) private {
        if (_token.allowance(address(this), _toApprove) == 0) {
            _token.safeApprove(_toApprove, MAX_UINT);
        }
    }

    function getBancorNetworkContract() public view returns (IBancorNetwork) {
        return IBancorNetwork(contractRegistry.addressOf(bancorNetworkName));
    }

    function approveVbnt(address _toApprove) external onlyOwnerOrManager {
        vbnt.approve(_toApprove, MAX_UINT);
    }

    function pauseContract() external onlyOwnerOrManager {
        _pause();
    }

    function unpauseContract() external onlyOwnerOrManager {
        _unpause();
    }

    modifier onlyOwnerOrManager {
        require(
            msg.sender == owner() ||
                msg.sender == manager ||
                msg.sender == manager2,
            'Non-admin caller'
        );
        _;
    }

    modifier liquidationTimeElapsed {
        require(
            adminActiveTimestamp.add(LIQUIDATION_TIME_PERIOD) < block.timestamp,
            'Liquidation time not elapsed'
        );
        _;
    }

    /*
     * @notice manager == alternative admin caller to owner
     */
    function setManager(address _manager) external onlyOwner {
        manager = _manager;
    }

    /*
     * @notice manager2 == alternative admin caller to owner
     */
    function setManager2(address _manager2) external onlyOwner {
        manager2 = _manager2;
    }

    /*
     * @notice Inverse of fee i.e., a fee divisor of 100 == 1%
     * @notice Three fee types
     * @dev Mint fee 0 or <= 2%
     * @dev Burn fee 0 or <= 1%
     * @dev Claim fee 0 <= 4%
     */
    function setFeeDivisors(
        uint256 mintFeeDivisor,
        uint256 burnFeeDivisor,
        uint256 claimFeeDivisor
    ) external onlyOwner {
        _setFeeDivisors(mintFeeDivisor, burnFeeDivisor, claimFeeDivisor);
    }

    function _setFeeDivisors(
        uint256 _mintFeeDivisor,
        uint256 _burnFeeDivisor,
        uint256 _claimFeeDivisor
    ) private {
        require(_mintFeeDivisor == 0 || _mintFeeDivisor >= 50, 'Invalid fee');
        require(_burnFeeDivisor == 0 || _burnFeeDivisor >= 100, 'Invalid fee');
        require(_claimFeeDivisor >= 25, 'Invalid fee');
        feeDivisors.mintFee = _mintFeeDivisor;
        feeDivisors.burnFee = _burnFeeDivisor;
        feeDivisors.claimFee = _claimFeeDivisor;
    }

    /*
     * @notice Records admin activity
     * @notice If not certified for a period exceeding LIQUIDATION_TIME_PERIOD,
     * emergencyCooldown and emergencyRedeem become available to non-admin caller
     */
    function _updateAdminActiveTimestamp() private {
        adminActiveTimestamp = block.timestamp;
    }

    receive() external payable {
        require(msg.sender != tx.origin, "Errant ETH deposit");
    }
}
function getTargetBufferBalance() public view returns (uint256) {
    return getNav().div(BUFFER_TARGET);
}

/* ========================================================================================= */