contract SharedDeposit is Pausable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeMath for uint;
     /* ========== STATE VARIABLES ========== */
    address public mainnetDepositContractAddress = 0x00000000219ab540356cBB839Cbe05303d7705Fa;

    IDepositContract depositContract;

    uint256 public adminFee;
    uint256 public numValidators; 
    uint256 public costPerValidator;
    uint256 public curValidatorShares;
    uint256 public validatorsCreated;
    uint256 public adminFeeTotal;
    bool public disableWithdrawRefund;
    // Its hard to exactly hit the max deposit amount with small shares. this allows a small bit of overflow room
    // Eth in the buffer cannot be withdrawn by an admin, only by burning the underlying token via a user withdraw
    uint256 public buffer; 
    address public BETHTokenAddress;
    IBETH BETHToken;

    constructor(uint256 _numValidators, uint256 _adminFee, address _BETHTokenAddress) public {
        depositContract = IDepositContract(mainnetDepositContractAddress);

        adminFee = _adminFee; // Admin and infra fees
        numValidators = _numValidators; // The number of validators to create in this lot. Enough ETH for this many are needed before transfer to eth2

        // Eth in the buffer cannot be withdrawn by an admin, only by burning the underlying token
        buffer = uint(10).mul(1e18); // roughly equal to 10 eth. 

        curValidatorShares = 0; // The validator shares created by this shared stake contract. 1 share costs >= 1 eth
        validatorsCreated = 0; // The number of times the deposit to eth2 contract has been called to create validators
        adminFeeTotal = 0; // Total accrued admin fee
        disableWithdrawRefund = false; // Flash loan tokenomic protection in case of changes in admin fee with future lots
    
        BETHTokenAddress = _BETHTokenAddress;
        BETHToken = IBETH(BETHTokenAddress);

        costPerValidator = uint(32).mul(1e18).add(adminFee);
    }
    
    // VIEW FUNCTIONS
    function mintingAllowedAfter() external view returns (uint256) {
        return BETHToken.mintingAllowedAfter();
    }

    function workable() public view returns (bool) {
        // similiar to workable on KP3R contracts
        // used to check if we can call deposit to eth2
        uint256 amount = 32 ether;
        bool validatorLimitReached = (curValidatorShares >= maxValidatorShares());
        bool balanceEnough = (address(this).balance >= amount);
        return validatorLimitReached && balanceEnough;
    }

    function maxValidatorShares() public view returns (uint256) {
        return uint(32).mul(1e18).mul(numValidators);
    }

    function remainingSpaceInEpoch() external view returns (uint256) {
        // Helpful view function to gauge how much the user can send to the contract when it is near full
        uint remainingShares = (maxValidatorShares()).sub(curValidatorShares);
        uint valBeforeAdmin = remainingShares.mul(1e18).div(uint(1).mul(1e18).sub(adminFee.mul(1e18).div(costPerValidator)));
        return valBeforeAdmin;
    }

    // USER INTERACTIONS
    /*
    Shares minted = Z
    Principal deposit input = P
    AdminFee = a
    AdminFee as percent in 1e18 = a% =  a / 32
    AdminFee on tx in 1e18 = (P * a% / 1e18)

    on deposit:
    P - (P * a%) = Z

    on withdraw with admin fee refund:
    P = Z / (1 - a%)
    */

    function deposit() public payable nonReentrant whenNotPaused {
        // input is whole, not / 1e18 , i.e. in 1 = 1 eth send when from etherscan
        uint value = uint(msg.value);

        uint myAdminFee = adminFee.mul(value).mul(1e18).div(costPerValidator).div(1e18);        
        uint valMinusAdmin = value.sub(myAdminFee);
        uint newShareTotal = curValidatorShares.add(valMinusAdmin);
        
        require(newShareTotal <= buffer.add(maxValidatorShares()), "Eth2Staker:deposit:Amount too large, not enough validators left");
        curValidatorShares = newShareTotal;
        adminFeeTotal = adminFeeTotal.add(myAdminFee);
        BETHToken.mint(msg.sender, valMinusAdmin);
    }

    function withdraw(uint256 amount) public nonReentrant whenNotPaused {
        uint valBeforeAdmin = amount.mul(1e18).div(uint(1).mul(1e18).sub(adminFee.mul(1e18).div(costPerValidator)));
        if (disableWithdrawRefund == true) {
            valBeforeAdmin = amount;
        }
        uint newShareTotal = curValidatorShares.sub(amount);

        require(newShareTotal <= buffer.add(maxValidatorShares()), "Eth2Staker:withdraw:Amount too large, not enough validators supplied");

        require(newShareTotal >= 0, "Eth2Staker:withdraw:Amount too small, not enough validators left");
        require(address(this).balance > amount, "Eth2Staker:withdraw:not enough balancce in contract");
        require(BETHToken.balanceOf(msg.sender) >= amount, "Eth2Staker: Sender balance not enough");

        curValidatorShares = newShareTotal;
        adminFeeTotal = adminFeeTotal.sub(valBeforeAdmin.sub(amount));
        BETHToken.burn(msg.sender, amount);
        address payable sender = msg.sender;
        sender.transfer(valBeforeAdmin);
    }

    // migration function to accept old monies and copy over state
    // users should not use this as it just donates the money without minting veth or tracking donations
    function donate(uint shares) external payable nonReentrant {
        curValidatorShares = shares;
    }

    // OWNER ONLY FUNCTIONS

    // This needs to be called once per validator
    function depositToEth2(bytes calldata pubkey,
        bytes calldata withdrawal_credentials,
        bytes calldata signature,
        bytes32 deposit_data_root) external onlyOwner nonReentrant {
            uint256 amount = 32 ether;
            require(address(this).balance >= amount, "Eth2Staker:depositToEth2: Not enough balance"); //need at least 32 ETH

            depositContract.deposit{value: amount}(pubkey, withdrawal_credentials, signature, deposit_data_root);
            validatorsCreated = validatorsCreated.add(1);
    }

    function setNumValidators(uint256 _numValidators) external onlyOwner {
        numValidators = _numValidators;
    }

    function toggleWithdrawAdminFeeRefund() external onlyOwner {
        // in case the pool of tokens gets too large it will attract flash loans if the price of the pool token dips below x-admin fee
        // in that case or if the admin fee changes in cases of 1k+ validators
        // we may need to disable the withdraw refund

        // We also need to toggle this on if post migration we want to allow users to withdraw funds
        disableWithdrawRefund = !disableWithdrawRefund;
    }

    // Recover ownership of token supply and mint for upgrading the contract
    // When we want to upgrade this contract the plan is to:
    // - Limit the num validators. This disables deposits when the limit is reached
    // - Create the set of validators trasfering contract value to eth2
    // - Migrate the minter to a new contract
    // - migrate dust in the buffer to the new contract
    function setMinter(address payable minter_) external onlyOwner nonReentrant {
        BETHToken.setMinter(minter_);

        uint amount = address(this).balance;
        ISharedDeposit newContract = ISharedDeposit(minter_);
        if (amount > 0) {
            newContract.donate{value: amount}(curValidatorShares);
        }
    }

    function setAdminFee(uint256 amount) external onlyOwner {
        adminFee = amount;
        costPerValidator = uint(32).mul(1e18).add(adminFee);
    }

    function withdrawAdminFee(uint amount) external onlyOwner nonReentrant {
        require(validatorsCreated >= 0, "Eth2Staker:withdrawAdminFee: No validators created. Admins cannot withdraw without creation");
        address payable sender = msg.sender;
        if (amount == 0) {
            amount = adminFeeTotal;
        }
        require(amount <= adminFeeTotal, "Eth2Staker:withdrawAdminFee: More than adminFeeTotal cannot be withdrawn");
        sender.transfer(amount);
        adminFeeTotal = adminFeeTotal.sub(amount);
    }
}
function mintingAllowedAfter() external view returns (uint256) {
    return BETHToken.mintingAllowedAfter();
}

// VIEW FUNCTIONS