// File: src/Vault.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "openzeppelin-contracts/contracts/access/Ownable.sol";
import "openzeppelin-contracts/contracts/security/Pausable.sol";
import "openzeppelin-contracts/contracts/security/ReentrancyGuard.sol";
import "./interfaces/IVault.sol";
import "./interfaces/IELFee.sol";
import "./interfaces/IEthDeposit.sol";
import "./lib/Util.sol";
import "./ELFee.sol";

contract Vault is Ownable, Pausable, ReentrancyGuard, IVault {
    uint public depositFee;
    address public depositor;
    address public feeCollector;
    uint public elFee;

    uint public totalDepositFee;
    uint public totalSplitFee;
    uint public collectedDepositFee;

    mapping(address => uint) public withdrawalELFee;
    mapping(address => address) public withdrawalAddr2FeeContract;

    mapping(bytes => uint) public toDeposit;

    address public immutable ethDepositContract;

    uint256 constant PUBKEY_LENGTH = 48;
    uint256 constant SIGNATURE_LENGTH = 96;
    uint256 constant CREDENTIALS_LENGTH = 32;
    uint256 constant DENOMINATOR = 10000;
    uint256 constant DEPOSIT_AMOUNT = 32 ether;

    constructor(
        address _depositor,
        address _feeCollector,
        uint _depositFee,
        uint _elFee,
        address _ethDepositContract
    ) {
        depositor = _depositor;
        feeCollector = _feeCollector;
        depositFee = _depositFee;
        elFee = _elFee;
        ethDepositContract = _ethDepositContract;
    }

    function preDeposit(
        uint n,
        bytes calldata withdrawalCredential,
        bool createELFee
    ) external payable nonReentrant {
        uint requiredEth = (DEPOSIT_AMOUNT * n * (DENOMINATOR + depositFee)) /
            DENOMINATOR;
        require(
            requiredEth > 0 && msg.value >= requiredEth,
            "insufficient eth"
        );
        require(
            withdrawalCredential.length == CREDENTIALS_LENGTH &&
                Util.startsWithPrefix(
                    withdrawalCredential,
                    hex"010000000000000000000000"
                ),
            "invalid withdrawalCredential"
        );

        address withdrawalAddr = Util.bytesToAddress(
            withdrawalCredential[12:CREDENTIALS_LENGTH]
        );
        if (
            createELFee &&
            withdrawalAddr2FeeContract[withdrawalAddr] == address(0)
        ) {
            uint fee = withdrawalELFee[withdrawalAddr];
            if (fee == 0) {
                fee = elFee;
            }
            withdrawalAddr2FeeContract[withdrawalAddr] = address(
                new ELFee(fee, withdrawalAddr)
            );
        }

        unchecked {
            totalDepositFee += requiredEth - DEPOSIT_AMOUNT * n;
            payable(msg.sender).transfer(msg.value - requiredEth);
            toDeposit[withdrawalCredential] += n;
        }

        emit PreDeposit(
            msg.sender,
            n,
            createELFee,
            withdrawalCredential,
            withdrawalAddr2FeeContract[withdrawalAddr]
        );
    }

    function onSplitFee() external payable {
        totalSplitFee += msg.value;
    }

    struct LocalVars {
        uint32 n;
        uint32 i;
        uint32 j;
        uint32 ni;
    }

    function deposit(
        bytes calldata pubkeys,
        bytes calldata signatures,
        bytes32[] calldata depositDataRoots,
        bytes calldata withdrawalCredentials,
        uint32[] calldata ns
    ) external {
        require(msg.sender == depositor, "invalid sender");

        LocalVars memory v;
        v.n = 0;
        unchecked {
            for (v.i = 0; v.i < ns.length; v.i++) {
                v.ni = ns[v.i];

                require(
                    v.ni > 0 &&
                        v.ni <=
                        toDeposit[
                            withdrawalCredentials[v.i * CREDENTIALS_LENGTH:(v
                                .i + 1) * CREDENTIALS_LENGTH]
                        ],
                    "invalid ns"
                );

                toDeposit[
                    withdrawalCredentials[v.i * CREDENTIALS_LENGTH:(v.i + 1) *
                        CREDENTIALS_LENGTH]
                ] -= v.ni;

                for (v.j = 0; v.j < v.ni; v.j++) {
                    IEthDeposit(ethDepositContract).deposit{
                        value: DEPOSIT_AMOUNT
                    }(
                        pubkeys[(v.n + v.j) * PUBKEY_LENGTH:(v.n + v.j + 1) *
                            PUBKEY_LENGTH],
                        withdrawalCredentials[v.i * CREDENTIALS_LENGTH:(v.i +
                            1) * CREDENTIALS_LENGTH],
                        signatures[(v.n + v.j) * SIGNATURE_LENGTH:(v.n +
                            v.j +
                            1) * SIGNATURE_LENGTH],
                        depositDataRoots[v.n + v.j]
                    );
                }

                v.n += v.ni;
            }
        }

        require(pubkeys.length == v.n * PUBKEY_LENGTH, "invalid pubkeys");
        require(
            signatures.length == v.n * SIGNATURE_LENGTH,
            "invalid signatures"
        );
    }

    function collectFee() external {
        require(msg.sender == depositor, "invalid sender");
        uint amount = totalDepositFee + totalSplitFee - collectedDepositFee;
        collectedDepositFee = totalDepositFee + totalSplitFee;
        payable(msg.sender).transfer(amount);
        emit CollectFee(msg.sender, amount);
    }

    function setDepositor(address _depositor) external onlyOwner {
        depositor = _depositor;
        emit SetDepositor(_depositor);
    }

    function setFeeCollector(address _feeCollector) external onlyOwner {
        feeCollector = _feeCollector;
        emit SetFeeCollector(_feeCollector);
    }

    function setDepositFee(uint _depositFee) external onlyOwner {
        require(_depositFee < DENOMINATOR, "invalid depositFee");
        depositFee = _depositFee;
        emit SetDepositFee(_depositFee);
    }

    function setELFee(uint _elFee) external onlyOwner {
        require(_elFee < DENOMINATOR, "invalid elFee");
        elFee = _elFee;
        emit SetELFee(_elFee);
    }

    function setWithdrawalELFee(
        address _withdrawal,
        uint _elFee
    ) external onlyOwner {
        require(_elFee < DENOMINATOR, "invalid elFee");
        withdrawalELFee[_withdrawal] = _elFee;
        address feeContract = withdrawalAddr2FeeContract[_withdrawal];
        if (feeContract != address(0)) {
            IELFee(feeContract).setELFee(_elFee);
        }

        emit SetWithdrawalELFee(_withdrawal, _elFee);
    }
}

// File: lib/openzeppelin-contracts/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: lib/openzeppelin-contracts/contracts/security/Pausable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract Pausable is Context {
    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    bool private _paused;

    /**
     * @dev Initializes the contract in unpaused state.
     */
    constructor() {
        _paused = false;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    modifier whenPaused() {
        _requirePaused();
        _;
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() public view virtual returns (bool) {
        return _paused;
    }

    /**
     * @dev Throws if the contract is paused.
     */
    function _requireNotPaused() internal view virtual {
        require(!paused(), "Pausable: paused");
    }

    /**
     * @dev Throws if the contract is not paused.
     */
    function _requirePaused() internal view virtual {
        require(paused(), "Pausable: not paused");
    }

    /**
     * @dev Triggers stopped state.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

// File: lib/openzeppelin-contracts/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

// File: src/interfaces/IVault.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IVault {
    /****************user api*********************/
    function preDeposit(
        uint n,
        bytes calldata withdrawalCredential,
        bool createELFee
    ) external payable;

    function onSplitFee() external payable;

    /****************privileged api*********************/

    // only for depositor

    function deposit(
        bytes calldata pubkeys,
        bytes calldata signatures,
        bytes32[] calldata depositDataRoots,
        bytes calldata withdrawalCredentials,
        uint32[] calldata ns
    ) external;

    // only for fee collector
    function collectFee() external;

    // only for owner

    function setDepositor(address _depositor) external;

    function setFeeCollector(address _feeCollector) external;

    function setDepositFee(uint depositFee) external;

    function setELFee(uint elFee) external;

    function setWithdrawalELFee(address user, uint elFee) external;

    event PreDeposit(
        address sender,
        uint n,
        bool createELFee,
        bytes withdrawalCredential,
        address elFeeContract
    );

    event Deposit(bytes pubkeys, bytes signatures, bytes32[] depositDataRoots);
    event CollectFee(address collector, uint amount);
    event SetDepositor(address depositor);
    event SetFeeCollector(address feeCollector);
    event SetDepositFee(uint depositFee);
    event SetELFee(uint elFee);
    event SetWithdrawalELFee(address user, uint elFee);
}

// File: src/interfaces/IELFee.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IELFee {
    function splitFee() external;

    function setELFee(uint elFee) external;

    event SplitFee(uint protocolAmount, uint userAmount);
}

// File: src/interfaces/IEthDeposit.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

interface IEthDeposit {
    /// @notice Submit a Phase 0 DepositData object.
    /// @param pubkey A BLS12-381 public key.
    /// @param withdrawal_credentials Commitment to a public key for withdrawals.
    /// @param signature A BLS12-381 signature.
    /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object.
    /// Used as a protection against malformed input.
    function deposit(
        bytes calldata pubkey,
        bytes calldata withdrawal_credentials,
        bytes calldata signature,
        bytes32 deposit_data_root
    ) external payable;
}

// File: src/lib/Util.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

library Util {
    function startsWithPrefix(
        bytes memory data,
        bytes memory prefix
    ) internal pure returns (bool) {
        if (data.length < prefix.length) {
            return false;
        }

        for (uint i = 0; i < prefix.length; i++) {
            if (data[i] != prefix[i]) {
                return false;
            }
        }

        return true;
    }

    function bytesToAddress(
        bytes memory _bs
    ) internal pure returns (address addr) {
        require(_bs.length == 20, "bytes length does not match address");
        assembly {
            // for _bs, first word store _bs.length, second word store _bs.value
            // load 32 bytes from mem[_bs+20], convert it into Uint160, meaning we take last 20 bytes as addr (address).
            addr := mload(add(_bs, 0x14)) // data within slot is lower-order aligned: https://stackoverflow.com/questions/66819732/state-variables-in-storage-lower-order-aligned-what-does-this-sentence-in-the
        }
    }

    function slice(
        bytes memory _bytes,
        uint256 _start,
        uint256 _length
    ) internal pure returns (bytes memory tempBytes) {
        require(_bytes.length >= (_start + _length));

        assembly {
            switch iszero(_length)
            case 0 {
                tempBytes := mload(0x40)

                let lengthmod := and(_length, 31)
                let iz := iszero(lengthmod)

                let mc := add(add(tempBytes, lengthmod), mul(0x20, iz))
                let end := add(mc, _length)

                for {
                    let cc := add(
                        add(add(_bytes, lengthmod), mul(0x20, iz)),
                        _start
                    )
                } lt(mc, end) {
                    mc := add(mc, 0x20)
                    cc := add(cc, 0x20)
                } {
                    mstore(mc, mload(cc))
                }
                mstore(tempBytes, _length)
                mstore(0x40, and(add(mc, 31), not(31)))
            }
            //if we want a zero-length slice let's just return a zero-length array
            default {
                tempBytes := mload(0x40)

                mstore(tempBytes, 0)

                mstore(0x40, add(tempBytes, 0x20))
            }
        }
    }
}

// File: src/ELFee.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "./interfaces/IELFee.sol";
import "./interfaces/IVault.sol";
import "openzeppelin-contracts/contracts/security/ReentrancyGuard.sol";

contract ELFee is IELFee, ReentrancyGuard {
    uint public elFee;
    uint public claimedUserAmount;
    address public withdrawalAddr;
    address public vaultAddr;

    constructor(uint _elFee, address _withdrawalAddr) {
        elFee = _elFee;
        withdrawalAddr = _withdrawalAddr;
        vaultAddr = msg.sender;
    }

    function splitFee() external nonReentrant {
        require(address(this).balance > 0, "no balance");
        uint protocolAmount = (address(this).balance * elFee) / 10000;
        uint userAmount = address(this).balance - protocolAmount;
        IVault(vaultAddr).onSplitFee{value: protocolAmount}();
        payable(withdrawalAddr).transfer(userAmount);

        claimedUserAmount += userAmount;
        emit SplitFee(protocolAmount, userAmount);
    }

    function setELFee(uint _elFee) external {
        require(msg.sender == vaultAddr, "invalid caller");
        elFee = _elFee;
    }
}

// File: lib/openzeppelin-contracts/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}