// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

// These files are strictly provided as-is
// without any express or implied guarantees, representations, or warranties
// as to any of these files or any deployments hereof.

// Any users of these files are doing so at their own risk.

// Solbase ReentrancyGuard
import {ReentrancyGuard} from "./ReentrancyGuard.sol";
// Solbase SafeTransferLib
import {SafeTransferLib} from "./SafeTransferLib.sol";

interface IERC20_DAOPool {
    function balanceOf(address owner) external view returns (uint256);

    // for 'depositWithPermit()'
    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;
}

interface IEnshroudProtocol_DAOPool {
    /// @notice 'mvoSigs' is an array of MVO signatures provided by the MVO server (first is lead MVO);
    /// 'mvoIDs' are the IDs of the MVOs, the first of which is the lead MVO which receives the deposit message
    struct MVOinfo {
        bytes[] mvoSigs;
        string[] mvoIDs;
    }

    function depositEth(address depositor) external payable;

    function depositTokens(
        address depositor,
        address tokenContract,
        uint256 amount
    ) external;

    // getter for public 'adminStatus[]' mapping
    function adminStatus(address addr) external view returns (bool);
}

/// @title contract that keeps state variables including checkpoints and user structs
abstract contract StateUtils is ReentrancyGuard {
    using SafeTransferLib for address;

    struct AddressCheckpoint {
        uint32 fromBlock;
        address _address;
    }

    struct Checkpoint {
        uint32 fromBlock;
        uint224 value;
    }

    /// @dev packing uints did not save gas with explicit conversions, so we keep all values at uint256
    struct User {
        Checkpoint[] shares;
        Checkpoint[] delegatedTo;
        AddressCheckpoint[] delegates;
        uint256 deposited; // deposited to pool, unlocked, but not staked
        uint256 unstakeAmount; // amount scheduled for unstaking
        uint256 unstakeShares; // shares scheduled for unstaking
        uint256 unstakeScheduledFor;
        uint256 lastDelegationUpdateTimestamp;
    }

	/// types to distinguish Requests
	enum ApprovalTypes { Protocol, FeeBot }

	/// @notice to track admin update requests; addresses truncated to bytes8 for size/gas considerations
	struct Requests {
		bytes8 requester1;
		bytes8 requester2;
		ApprovalTypes requestType;
	}

    uint256 internal constant ARRAY_LIMIT = 20; // max limit for dynamic arrays (token contracts supplied)
    uint256 internal constant DECIMALS = 1e18;
    uint256 internal constant MAX_UINT256 = 2 ** 256 - 1;

    /// @notice constant variable of one week for timestamp calculations
    uint256 public constant EPOCH_LENGTH = 1 weeks;

    /// @notice for block number calculations, approximated via 7150 blocks/day on ETH mainnet * 7 days.
    /// @dev should be altered accordingly for other deployments.
    uint256 public constant BLOCKS_PER_EPOCH = 50050;

    /// @notice block starting point for epoch calculations
    uint256 internal immutable genesisBlock;

    /// @notice Enshroud protocol contract. If a v2 protocol is deployed, updateProtocol() should be called by 3 admins, or else a DAOPool v2 may be deployed.
	/// @notice Without doing so, the functions claimETHYieldAsENFTs() and claimTokenYieldsAsENFTs() in address(this) would deposit value only to the v1 protocol contract.
	address public enshroudProtocol;
    IEnshroudProtocol_DAOPool public iProtocol;

    /// @notice $ENSHROUD token
    address public immutable enshroudToken;

    /// @notice TimelockManager contract, initialised by admin via TimelockUtils
    address public timelockManager;

	/// @notice address used by bot to import fees earned on other chains
	address internal feesBot;

	/// @notice total number of $ENSHROUD tokens deposited but not staked (some could be locked in a Timelock, but those cannot be staked)
	uint256 public depositedUnstaked;

    /// @notice total number of $ENSHROUD tokens staked in this contract
    uint256 public totalStake;

    /// @notice maps an address to their 'User' struct
    mapping(address => User) public users;

    /// @notice maps an address to the block.number of their most recent 'stake()'
    mapping(address => uint256) public mostRecentStake;

    /// @notice nesting mapping of user address to an asset's address to the number of blocks since such user has claimed their yield for such asset, to compare against epoch length in claim functions
    mapping(address => mapping(address => uint256)) public claimed;

    /// @notice keeps the total number of shares of the pool
    Checkpoint[] public poolShares;

	/// @notice used for tracking state of update*() invocations
	mapping(address => Requests) internal updateRequested;

    /*///////////////////////////////////////////////////////////////
                            ERRORS
    //////////////////////////////////////////////////////////////*/

    error AlreadyClaimedThisEpoch();
    error AlreadyDelegated();
    error AmountExceedsDeposited();
    error AmountExceedsStaked();
    error AmountExceedsUnlocked();
    error DAOPool_ArrayTooLong();
    error DAOPool_ExistingTimelock();
    error DAOPool_ReleaseNotStarted();
    error DAOPool_ReleaseEnded();
    error DAOPool_StartMustPrecedeEnd();
    error DelegateUpdatedTooRecently();
    error ExistingUnstake();
    error GreaterThanMaxValue();
    error InvalidChainId();
    error InvalidDelegate();
	error LengthMismatch();
    error LessThanOneEpochSinceStake();
    error MulDivFailed();
    error NoShares();
    error NotDepositOwner();
    error NotAdmin();
    error NotDelegated();
    error NotTimelockManager();
	error OnlyFeesBot();
    error UnstakeNotAvailable();
    error ZeroAddress();

    /*///////////////////////////////////////////////////////////////
                            MODIFIERS
    //////////////////////////////////////////////////////////////*/

    /// @notice ensures msg.sender has waited full epoch before calling modified function, to prevent flashloan attacks (for example take loan, depositAndStake, claim, unstake, return loan)
    modifier epochPassedSinceStake() {
        if (block.number - mostRecentStake[msg.sender] < BLOCKS_PER_EPOCH)
            revert LessThanOneEpochSinceStake();
        _;
    }

    modifier onlyTimelockManager() {
        if (msg.sender != timelockManager) revert NotTimelockManager();
        _;
    }

	// @notice guarantees caller is admin in latest-deployed EnshroudProtocol
	modifier onlyAdmin() {
        if (!iProtocol.adminStatus(msg.sender)) revert NotAdmin();
		_;
	}

    /*///////////////////////////////////////////////////////////////
                            FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @param _enshroudProtocol Enshroud protocol contract address
    /// @param _enshroudTokenAddress $ENSHROUD token contract address
    constructor(
        address _enshroudProtocol,
        address _enshroudTokenAddress
    ) payable {
        enshroudToken = _enshroudTokenAddress;
		enshroudProtocol = _enshroudProtocol;
        iProtocol = IEnshroudProtocol_DAOPool(_enshroudProtocol);
        genesisBlock = block.number;
        // Initialise the share amount and stake at 1
        totalStake = 1;
        _updateCheckpointArray(poolShares, 1);
    }

    /// @notice allow this contract to receive ETH, needed for fees earned from protocol contract and feesBot
    receive() external payable {}

    /// @notice admin may set or update the address of the timelock manager contract.
    /// @param _timelockManagerAddress timelock manager contract address
    function setTimelockManager(
		address _timelockManagerAddress
	) external onlyAdmin {
        timelockManager = _timelockManagerAddress;
    }

	/// @notice may set or update 'feesBot' with a new address
	/// @param _newFeesBot address of bot's EOA which imports fees earned on other chains
	function updateFeesBot(address _newFeesBot) external onlyAdmin {
		bytes8 _caller = bytes8(uint64(uint160(msg.sender)));
		if (_isRequestApproved(_newFeesBot, _caller, ApprovalTypes.FeeBot)) {
			// only update if three admins have requested, to thwart a single malicious admin
			feesBot = _newFeesBot;

			// reset after variable update
			delete updateRequested[_newFeesBot];
		}
	}

	/// @notice admin may update 'iProtocol' with a new protocol address (v2, v3, etc.)
	/// @param _newProtocol new EnshroudProtocol contract address
	function updateProtocol(address _newProtocol) external onlyAdmin {
		bytes8 _caller = bytes8(uint64(uint160(msg.sender)));
		if (_isRequestApproved(_newProtocol, _caller, ApprovalTypes.Protocol)) {
			// only update if three admins have requested, to thwart a single malicious admin attempting to hijack withdrawn earnings
			enshroudProtocol = _newProtocol;
			iProtocol = IEnshroudProtocol_DAOPool(_newProtocol);

			// reset after variable update
			delete updateRequested[_newProtocol];
		}
	}

    /// @notice called internally to update a checkpoint array by pushing a new checkpoint
    /// @dev assumes `block.number` will always fit in a uint32 (as this contract is only to be deployed on ETH mainnet) and `value` will always fit in a uint224. `value` will either be a raw token amount
    /// or a raw pool share amount so this assumption will be correct in practice with a token with 18 decimals and no overwhelming single-address centralisation.
    /// @param _checkpointArray checkpoint array
    /// @param _value value to be used to create the new checkpoint
    function _updateCheckpointArray(
        Checkpoint[] storage _checkpointArray,
        uint256 _value
    ) internal {
        if (_value > type(uint224).max) revert GreaterThanMaxValue();
        _checkpointArray.push(
            Checkpoint({
                fromBlock: uint32(block.number),
                value: uint224(_value)
            })
        );
    }

    /// @notice called internally to update an address-checkpoint array by pushing a new checkpoint
    /// @dev assumes block.number will always fit in a uint32, as this contract is only to be deployed on ETH mainnet
    /// @param addressCheckpointArray address-checkpoint array
    /// @param _addr address to be used to create the new checkpoint
    function _updateAddressCheckpointArray(
        AddressCheckpoint[] storage addressCheckpointArray,
        address _addr
    ) internal {
        addressCheckpointArray.push(
            AddressCheckpoint({
                fromBlock: uint32(block.number),
                _address: _addr
            })
        );
    }

	/// @notice for variable updates that require update requests from three unique admins
	/// @param _reqAddr the address passed as the new protocol address
	/// @param _caller shortened form of the address of the calling admin
	/// @param _reqType: disambiguation for type of update call
	function _isRequestApproved(
		address _reqAddr,
		bytes8 _caller,
		ApprovalTypes _reqType
	) internal returns (bool) {
		bytes8 _default;

		if (updateRequested[_reqAddr].requester1 == _default) {
			updateRequested[_reqAddr].requester1 = _caller;
			updateRequested[_reqAddr].requestType = _reqType;
		} else if (
			updateRequested[_reqAddr].requester1 != _default &&
			updateRequested[_reqAddr].requester2 == _default &&
			_caller != updateRequested[_reqAddr].requester1 &&
			_reqType == updateRequested[_reqAddr].requestType
		) updateRequested[_reqAddr].requester2 = _caller;
		else if (
			updateRequested[_reqAddr].requester1 != _default &&
			updateRequested[_reqAddr].requester2 != _default &&
			_caller != updateRequested[_reqAddr].requester1 &&
			_caller != updateRequested[_reqAddr].requester2 &&
			_reqType == updateRequested[_reqAddr].requestType
		) {
			return true;
		}
		return false;
	}

    /// @dev SolDAO gas-efficient multiplication and division function, see https://github.com/Sol-DAO/solbase/blob/main/src/utils/FixedPointMath.sol
    /// @param x: numerator, to be multiplied by 'y'
    /// @param y: numerator, to be multiplied by 'x'
    /// @param denominator: divisor for the product of 'x' and 'y'
    function _mulDivDown(
        uint256 x,
        uint256 y,
        uint256 denominator
    ) internal pure returns (uint256 z) {
        /// @solidity memory-safe-assembly
        assembly {
            // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y))
            if iszero(
                mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))
            ) {
                // Store the function selector of `MulDivFailed()`.
                mstore(0x00, 0xad251c27)
                // Revert with (offset, size).
                revert(0x1c, 0x04)
            }

            // Divide x * y by the denominator.
            z := div(mul(x, y), denominator)
        }
    }
}

/// @title Contract that implements getters
abstract contract GetterUtils is StateUtils {
    /// @notice Called to get the voting power of a user at a specific block
    /// @param _userAddress User address
    /// @param _block Block number for which the query is being made for
    /// @return Voting power of the user at the block
    function userVotingPowerAt(
        address _userAddress,
        uint256 _block
    ) external view returns (uint256) {
        // Users that have a delegate have no voting power
        if (userDelegateAt(_userAddress, _block) != address(0)) {
            return 0;
        }
        return
            userSharesAt(_userAddress, _block) +
            	delegatedToUserAt(_userAddress, _block);
    }

    /// @notice Called to get the total pool shares at a specific block
    /// @dev Total pool shares also corresponds to total voting power
    /// @param _block Block number for which the query is being made for
    /// @return Total pool shares at the block
    function totalSharesAt(uint256 _block) public view returns (uint256) {
        return _getValueAt(poolShares, _block);
    }

    /// @notice Called to get the pool shares of a user at a specific block
    /// @param userAddress User address
    /// @param _block Block number for which the query is being made for
    /// @return Pool shares of the user at the block
    function userSharesAt(
        address userAddress,
        uint256 _block
    ) public view returns (uint256) {
        return _getValueAt(users[userAddress].shares, _block);
    }

    /// @notice Called to get the current staked tokens of the user
    /// @dev any DAO voting app should call here to ensure a voter's stake and calculate weighting of vote
    /// @param _userAddress User address
    /// @return Current staked tokens of the user
    function userStake(address _userAddress) public view returns (uint256) {
        return
            _mulDivDown(
                userSharesAt(_userAddress, block.number),
                totalStake,
                totalSharesAt(block.number)
            );
    }

    /// @notice Called to get the voting power delegated to a user at a specific block
    /// @param _userAddress User address
    /// @param _block Block number for which the query is being made for
    /// @return Voting power delegated to the user at the block
    function delegatedToUserAt(
        address _userAddress,
        uint256 _block
    ) public view returns (uint256) {
        return _getValueAt(users[_userAddress].delegatedTo, _block);
    }

    /// @notice Called to get the delegate of the user at a specific block
    /// @param _userAddress User address
    /// @param _block Block number
    /// @return Delegate of the user at the specific block
    function userDelegateAt(
        address _userAddress,
        uint256 _block
    ) public view returns (address) {
        return _getAddressAt(users[_userAddress].delegates, _block);
    }

    /// @notice Called to get the current delegate of the user
    /// @param _userAddress User address
    /// @return Current delegate of the user
    function userDelegate(address _userAddress) public view returns (address) {
        return userDelegateAt(_userAddress, block.number);
    }

    /// @notice Called to get the value of a checkpoint array at a specific block using binary search.
    /// @dev Adapted from https://github.com/aragon/minime/blob/1d5251fc88eee5024ff318d95bc9f4c5de130430/contracts/MiniMeToken.sol#L431
    /// @param checkpoints Checkpoints array
    /// @param _block block number for which the query is being made
    /// @return Value of the checkpoint array at the block
    function _getValueAt(
        Checkpoint[] storage checkpoints,
        uint256 _block
    ) internal view returns (uint256) {
        if (checkpoints.length == 0) return 0;

        // Shortcut for the actual value
        if (_block >= checkpoints[checkpoints.length - 1].fromBlock)
            return checkpoints[checkpoints.length - 1].value;
        if (_block < checkpoints[0].fromBlock) return 0;

        // Limit the search to the last 1024 elements if the value being
        // searched falls within that window
        uint256 _min = 0;
        if (
            checkpoints.length > 1024 &&
            checkpoints[checkpoints.length - 1024].fromBlock < _block
        ) {
            _min = checkpoints.length - 1024;
        }

        // Binary search of the value in the array
        uint256 _max = checkpoints.length - 1;
        while (_max > _min) {
            uint256 _mid = (_max + _min + 1) / 2;
            if (checkpoints[_mid].fromBlock <= _block) {
                _min = _mid;
            } else {
                _max = _mid - 1;
            }
        }
        return checkpoints[_min].value;
    }

    /// @notice Called to get the value of an address-checkpoint array at a specific block using binary search
    /// @dev Adapted from https://github.com/aragon/minime/blob/1d5251fc88eee5024ff318d95bc9f4c5de130430/contracts/MiniMeToken.sol#L431
    /// @param checkpoints Address-checkpoint array
    /// @param _block Block number for which the query is being made
    /// @return Value of the address-checkpoint array at the block
    function _getAddressAt(
        AddressCheckpoint[] storage checkpoints,
        uint256 _block
    ) private view returns (address) {
        if (checkpoints.length == 0) return address(0);

        // Shortcut for the actual value
        if (_block >= checkpoints[checkpoints.length - 1].fromBlock)
            return checkpoints[checkpoints.length - 1]._address;
        if (_block < checkpoints[0].fromBlock) return address(0);

        // Limit the search to the last 1024 elements if the value being
        // searched falls within that window
        uint256 _min = 0;
        if (
            checkpoints.length > 1024 &&
            checkpoints[checkpoints.length - 1024].fromBlock < _block
        ) {
            _min = checkpoints.length - 1024;
        }

        // Binary search of the value in the array
        uint256 _max = checkpoints.length - 1;
        while (_max > _min) {
            uint256 _mid = (_max + _min + 1) / 2;
            if (checkpoints[_mid].fromBlock <= _block) {
                _min = _mid;
            } else {
                _max = _mid - 1;
            }
        }
        return checkpoints[_min]._address;
    }
}

/// @notice implements voting interface and voting power delegation
/// @dev any DAO voting app/contract can call userVotingPowerAt() in GetterUtils with a voter's address for their voting power (including delegation)
abstract contract VotingUtils is GetterUtils {
    event Delegated(
        address indexed user,
        address indexed delegate,
        uint256 shares,
        uint256 totalDelegatedTo
    );

    event Undelegated(
        address indexed user,
        address indexed delegate,
        uint256 shares,
        uint256 totalDelegatedTo
    );

    event UpdatedDelegation(
        address indexed user,
        address indexed delegate,
        bool delta,
        uint256 shares,
        uint256 totalDelegatedTo
    );

    /// @notice Called by the user to delegate voting power
    /// @param _delegate User address the voting power will be delegated to
    function delegateVotingPower(address _delegate) external {
        if (_delegate == address(0) || _delegate == msg.sender)
            revert InvalidDelegate();
        // verifying that the delegate is not currently delegating. However, the delegate may further delegate after they have been delegated to.
        if (userDelegate(_delegate) != address(0)) revert AlreadyDelegated();
        User storage user = users[msg.sender];
        // prohibit frequent delegation updates as that can be used to spam proposals; will not overflow on human timelines
        unchecked {
            if (
                user.lastDelegationUpdateTimestamp + EPOCH_LENGTH >=
                block.timestamp
            ) revert DelegateUpdatedTooRecently();
            user.lastDelegationUpdateTimestamp = block.timestamp;
        }

        uint256 userShares = userSharesAt(msg.sender, block.number);
        if (userShares == 0) revert NoShares();

        address _previousDelegate = userDelegate(msg.sender);
        if (_previousDelegate == _delegate) revert AlreadyDelegated();
        if (_previousDelegate != address(0)) {
            // revoke previous delegation
            _updateCheckpointArray(
                users[_previousDelegate].delegatedTo,
                delegatedToUserAt(_previousDelegate, block.number) - userShares
            );
        }

        // assign the new delegation
        uint256 delegatedToUpdate = delegatedToUserAt(_delegate, block.number) +
            userShares;
        _updateCheckpointArray(users[_delegate].delegatedTo, delegatedToUpdate);

        // record the new delegate for the user
        _updateAddressCheckpointArray(user.delegates, _delegate);
        emit Delegated(msg.sender, _delegate, userShares, delegatedToUpdate);
    }

    /// @notice called by the user to fully undelegate voting power
    function undelegateVotingPower() external {
        User storage user = users[msg.sender];
        address _previousDelegate = userDelegate(msg.sender);
        if (_previousDelegate == address(0)) revert NotDelegated();
        // will not overflow on human timelines
        unchecked {
            if (
                user.lastDelegationUpdateTimestamp + EPOCH_LENGTH >=
                block.timestamp
            ) revert DelegateUpdatedTooRecently();
            user.lastDelegationUpdateTimestamp = block.timestamp;
        }

        uint256 _userShares = userSharesAt(msg.sender, block.number);
        uint256 _delegatedToUpdate = delegatedToUserAt(
            _previousDelegate,
            block.number
        ) - _userShares;
        _updateCheckpointArray(
            users[_previousDelegate].delegatedTo,
            _delegatedToUpdate
        );
        _updateAddressCheckpointArray(user.delegates, address(0));
        emit Undelegated(
            msg.sender,
            _previousDelegate,
            _userShares,
            _delegatedToUpdate
        );
    }

    /// @notice called internally when the user shares are updated to update the delegated voting power
    /// @dev user shares only get updated while staking or scheduling unstaking
    /// @param _shares Amount of shares that will be added/removed
    /// @param _change Whether the shares will be added/removed (true = add, false = remove)
    function _updateDelegatedVotingPower(
        uint256 _shares,
        bool _change
    ) internal {
        address _delegate = userDelegate(msg.sender);
        if (_delegate == address(0)) {
            return;
        }
        uint256 _currentDelegatedTo = delegatedToUserAt(
            _delegate,
            block.number
        );
        uint256 _delegatedToUpdate = _change
            ? _currentDelegatedTo + _shares
            : _currentDelegatedTo - _shares;
        _updateCheckpointArray(
            users[_delegate].delegatedTo,
            _delegatedToUpdate
        );
        emit UpdatedDelegation(
            msg.sender,
            _delegate,
            _change,
            _shares,
            _delegatedToUpdate
        );
    }
}

/// @notice implements transferring (deposit/withdraw) functionality
abstract contract TransferUtils is VotingUtils {
    using SafeTransferLib for address;

    event Deposited(address indexed user, uint256 amount, uint256 userUnstaked);

    event Withdrawn(address indexed user, uint256 amount, uint256 userUnstaked);

	event ImportFeeRevenue(uint256 chainId, address tokenContract, uint256 amount);

    /// @param _amount: amount of tokens deposited, 18 decimals
    /// @param _deadline: deadline for permit approval usage
    /// @param v: ECDSA sig param
    /// @param r: ECDSA sig param
    /// @param s: ECDSA sig param
    function depositWithPermit(
        uint256 _amount,
        uint256 _deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external nonReentrant {
        IERC20_DAOPool(enshroudToken).permit(
            msg.sender,
            address(this),
            _amount,
            _deadline,
            v,
            r,
            s
        );

        // cannot overflow, users[msg.sender].deposited would have to be larger than type(uint256).max after the successful transferFrom
        unchecked {
            users[msg.sender].deposited += _amount;
			depositedUnstaked += _amount;
        }
        // will revert if unsuccessful so no need for an earlier balanceOf check
        enshroudToken.safeTransferFrom(msg.sender, address(this), _amount);

        emit Deposited(msg.sender, _amount, users[msg.sender].deposited);
    }

	/// @notice called by the feesBot to import fees earned by the DAOPool mirror contracts on other blockchains, to where they can be claimed by staking users
	/// @dev it is required that the feesBot execute prior approvals for xfers
	/// @dev ETH can be deposited by itself by passing 0-length arrays with a positive msg.value
	/// @param _chainId: the ID of the chain from which the fees are imported
	/// @param _tokenContracts: list of erc20 tokens being deposited
	/// @param _amounts: corresponding amounts for each token contract
	function importFees(
		uint256 _chainId,
        address[] calldata _tokenContracts,
		uint256[] calldata _amounts
	) external payable nonReentrant {
		// only callable by the fees bot
		if (msg.sender != feesBot) revert OnlyFeesBot();
		if (_tokenContracts.length != _amounts.length) revert LengthMismatch();
		if (_tokenContracts.length > ARRAY_LIMIT) revert DAOPool_ArrayTooLong();
		// verify it's indeed an import
		if (_chainId == block.chainid) revert InvalidChainId();

		// for each token, transfer the indicated amount from feesBot to here
        address _zero = address(0);
		address daoPool = address(this);
        for (uint256 i = 0; i < _tokenContracts.length; ) {
            address _addr = _tokenContracts[i];
			if (_addr == _zero) revert ZeroAddress();
			uint256 _amt = _amounts[i];
			if (_amt > 0) {
				// allow for possibility token has fee or is deflationary
				uint256 _ourBalanceBefore = IERC20_DAOPool(_addr).balanceOf(daoPool);
				/// @dev safeTransferFrom will revert on failure (for example if lacking proper permit or approve() from feesBot)
				_addr.safeTransferFrom(feesBot, daoPool, _amt);
				uint256 _ourBalanceAfter = IERC20_DAOPool(_addr).balanceOf(daoPool);
				require(_ourBalanceAfter >= _ourBalanceBefore);
				uint256 _netAmt = _ourBalanceAfter - _ourBalanceBefore;
				emit ImportFeeRevenue(_chainId, _addr, _netAmt);
			}
			unchecked {
				++i;
			}
		}

		// if any native tokens provided with message, log it
		if (msg.value > 0) {
			emit ImportFeeRevenue(_chainId, _zero, msg.value);
		}
	}

    /// @notice Called by the user to deposit tokens to this contract. Will be a separate call to stake.
    /// @dev The user should approve the pool to spend at least `_amount` tokens before calling this.
    /// Public visibility as this is called by depositAndStake()
    /// @param _amount Amount to be deposited
    function depositRegular(uint256 _amount) public nonReentrant {
        enshroudToken.safeTransferFrom(msg.sender, address(this), _amount); // will revert if unsuccessful so no need for an earlier balanceOf check
		// cannot overflow, users[msg.sender].deposited would have to be larger than type(uint256).max after the successful transferFrom
        unchecked {
            users[msg.sender].deposited += _amount;
			depositedUnstaked += _amount;
        }
        emit Deposited(msg.sender, _amount, users[msg.sender].deposited);
    }

    /// @notice called by the user to withdraw tokens to their wallet
    /// @dev The user should ensure that they have at least `_amount` unlocked tokens to withdraw.
	/// @dev user can call updateTimelockStatus(msg.sender) before this to move any now-unlocked tokens into their deposited balance
    /// named `withdrawRegular()` to be consistent with the name 'depositRegular()`. See `depositRegular()` for more context.
    /// @param _amount: amount to be withdrawn
    function withdrawRegular(uint256 _amount) public {
        _withdraw(_amount, msg.sender);
    }


    /// @notice called internally after the amount of locked tokens of the user is determined
	/// @dev user should call updateTimelockStatus(_recipient) before this to move any now-unlocked tokens into their deposited balance
    /// @param _amount: amount to be withdrawn
    /// @param _recipient: address receiving withdrawal
    function _withdraw(uint256 _amount, address _recipient) private {
        User storage user = users[_recipient];
        // ensure amount to be withdrawn does not exceed the user's unlocked deposited amount
		if (user.deposited < _amount) revert AmountExceedsDeposited();
        users[_recipient].deposited -= _amount;
		depositedUnstaked -= _amount;

        enshroudToken.safeTransfer(_recipient, _amount);

        emit Withdrawn(_recipient, _amount, users[_recipient].deposited);
    }
}

/// @notice implements staking functionality
abstract contract StakeUtils is TransferUtils {
    event Staked(
        address indexed user,
        uint256 amount,
        uint256 mintedShares,
        uint256 userUnstaked,
        uint256 userShares,
        uint256 totalShares,
        uint256 totalStake
    );

    event ScheduledUnstake(
        address indexed user,
        uint256 amount,
        uint256 shares,
        uint256 scheduledFor,
        uint256 userShares
    );

    event Unstaked(
        address indexed user,
        uint256 amount,
        uint256 userUnstaked,
        uint256 totalShares,
        uint256 totalStake
    );

    /// @notice convenience method to deposit and stake in a single transaction
    /// @param _amount: amount to be deposited and staked
    function depositAndStake(uint256 _amount) external {
        depositRegular(_amount);
        stake(_amount, msg.sender);
    }

    /// @notice called by the user to schedule unstaking of their tokens
    /// @dev While scheduling an unstake, `shares` get deducted from the user, meaning that they will not receive voting power or yield claim for them any longer.
    /// At unstaking-time, the user unstakes either the amount of tokens scheduled to unstake, or the amount of tokens `shares` corresponds to at such time, whichever is smaller.
    /// @param _amount Amount of tokens scheduled to unstake
    function scheduleUnstake(uint256 _amount) external {
        uint256 _userSharesNow = userSharesAt(msg.sender, block.number);
        uint256 _totalSharesNow = totalSharesAt(block.number);
        // ensures '_amount' does not exceed user's actual shares
        if (_mulDivDown(_userSharesNow, totalStake, _totalSharesNow) < _amount)
            revert AmountExceedsStaked();

        User storage user = users[msg.sender];
        if (user.unstakeScheduledFor != 0) revert ExistingUnstake();

        uint256 _sharesToUnstake = _mulDivDown(
            _amount,
            _totalSharesNow,
            totalStake
        );
        uint256 _userSharesUpdate = _userSharesNow - _sharesToUnstake;

        // if the user wants to schedule an unstake for a few Wei
        if (_sharesToUnstake == 0) revert NoShares();
        // will not overflow on human timelines
        unchecked {
            users[msg.sender].unstakeScheduledFor =
                block.timestamp + EPOCH_LENGTH;
        }
        users[msg.sender].unstakeAmount = _amount;
        users[msg.sender].unstakeShares = _sharesToUnstake;

        _updateCheckpointArray(user.shares, _userSharesUpdate);
        _updateDelegatedVotingPower(_sharesToUnstake, false);
        emit ScheduledUnstake(
            msg.sender,
            _amount,
            _sharesToUnstake,
            users[msg.sender].unstakeScheduledFor,
            _userSharesUpdate
        );
    }

    /// @notice convenience method to execute an unstake and withdraw to the user's wallet in a single transaction
    /// @dev withdrawal will revert if the user has less than `unstakeAmount` tokens that are withdrawable
    function unstakeAndWithdraw() external {
        withdrawRegular(unstake(msg.sender));
    }

    /// @notice called to stake tokens (from user's deposited amount) to receive shares in the pool
    /// @param _amount: amount of tokens to stake
    /// @param _staker: address staking _amount (msg.sender if calling directly, or the address which called 'depositAndStake()')
    function stake(uint256 _amount, address _staker) public nonReentrant {
		if (_staker != msg.sender) revert NotDepositOwner();
        User storage user = users[_staker];
        if (_amount > user.deposited) revert AmountExceedsDeposited();
        uint256 _totalSharesNow = totalSharesAt(block.number);
        uint256 _sharesToMint = _mulDivDown(
            _amount,
            _totalSharesNow,
            totalStake
        );
        uint256 _userSharesUpdate = userSharesAt(_staker, block.number) +
            _sharesToMint;
        uint256 _totalSharesUpdate = _totalSharesNow + _sharesToMint;

        users[_staker].deposited -= _amount;
		depositedUnstaked -= _amount;
        totalStake += _amount;
        mostRecentStake[_staker] = block.number;

        _updateCheckpointArray(user.shares, _userSharesUpdate);
        _updateCheckpointArray(poolShares, _totalSharesUpdate);
        _updateDelegatedVotingPower(_sharesToMint, true);

        emit Staked(
            _staker,
            _amount,
            _sharesToMint,
            users[_staker].deposited,
            _userSharesUpdate,
            _totalSharesUpdate,
            totalStake
        );
    }

    /// @notice called to execute a pre-scheduled unstake; unstaked tokens will become 'deposited'
    /// @dev anyone can execute a matured unstake. This is to allow the user to use bots, etc. to execute their unstaking as soon as possible.
    /// @param _userAddress: user address
    /// @return amount of tokens that are unstaked
    function unstake(address _userAddress) public returns (uint256) {
        User storage user = users[_userAddress];
        if (
            user.unstakeScheduledFor == 0 ||
            user.unstakeScheduledFor > block.timestamp
        ) revert UnstakeNotAvailable();
        uint256 _totalShares = totalSharesAt(block.number);
        uint256 _unstakeAmount = user.unstakeAmount;
        uint256 _unstakeAmountByShares = _mulDivDown(
            user.unstakeShares,
            totalStake,
            _totalShares
        );
        if (_unstakeAmount > _unstakeAmountByShares) {
            _unstakeAmount = _unstakeAmountByShares;
        }
        uint256 _totalSharesUpdate = _totalShares - user.unstakeShares;

        users[_userAddress].deposited += _unstakeAmount;
		depositedUnstaked += _unstakeAmount;
        totalStake -= _unstakeAmount;
        delete users[_userAddress].unstakeAmount;
        delete users[_userAddress].unstakeShares;
        delete users[_userAddress].unstakeScheduledFor;

        _updateCheckpointArray(poolShares, _totalSharesUpdate);

        emit Unstaked(
            _userAddress,
            _unstakeAmount,
            users[_userAddress].deposited,
            _totalSharesUpdate,
            totalStake
        );
        return _unstakeAmount;
    }
}

/// @notice implements unlocking functionality
/// @dev The TimelockManager contract interfaces with this contract to transfer $ENSHROUD tokens that are locked under an unlocking schedule.
abstract contract TimelockUtils is StakeUtils {
    using SafeTransferLib for address;

    event DepositedByTimelockManager(
        address indexed user,
        uint256 amount,
        uint256 userUnstaked
    );

    event DepositedUnlocking(
        address indexed user,
        uint256 amount,
        uint128 start,
        uint128 end
    );

    event UpdatedTimelock(
        address indexed user,
        uint256 amount,
        uint256 userUnlocking
    );

    struct Timelock {
        // total amount of tokens subject to lock
        uint256 totalAmount;
        // amount still locked
        uint256 remainingAmount;
        // block number of unlock schedule start
        uint128 releaseStart;
        // block number of unlock schedule end
        uint128 releaseEnd;
    }

    /// @notice maps user addresses to timelocks (user can only have one Timelock at a time)
    mapping(address => Timelock) public userToTimelock;

    /// @notice called by the TimelockManager contract to deposit unlocked withdrawable tokens on behalf of a user
    /// @dev this method is only usable by `TimelockManager.sol`.
    /// @param _source Token transfer source
    /// @param _userAddress User that the tokens will be deposited for
    /// @param _amount Amount to be deposited
    function deposit(
        address _source,
        address _userAddress,
        uint256 _amount
    ) external onlyTimelockManager {
        uint256 _unstakedUpdate = users[_userAddress].deposited + _amount;

        users[_userAddress].deposited = _unstakedUpdate;
		depositedUnstaked += _amount;

        enshroudToken.safeTransferFrom(_source, address(this), _amount);

        emit DepositedByTimelockManager(_userAddress, _amount, _unstakedUpdate);
    }

    /// @notice Called by the TimelockManager contract to deposit locked, non-withdrawable tokens on behalf of a user on a linear unlock schedule
	/// @dev to protect locked tokens from getting distributed as rewards, the depositedUnstaked total is incremented
	/// @dev locked tokens cannot be staked, but can be unlocked and become deposited (see updateTimelockStatus())
    /// @param _source token source address (Timelock Manager)
    /// @param _userAddress Address of the user who will receive the tokens
    /// @param _amount Token amount
    /// @param _releaseStart unlocking schedule starting block
    /// @param _releaseEnd unlocking schedule ending block
    function depositWithLocking(
        address _source,
        address _userAddress,
        uint256 _amount,
        uint128 _releaseStart,
        uint128 _releaseEnd
    ) external onlyTimelockManager {
        if (userToTimelock[_userAddress].remainingAmount != 0)
            revert DAOPool_ExistingTimelock();
        if (_releaseEnd <= _releaseStart) revert DAOPool_StartMustPrecedeEnd();
		// establish Timelock in this contract for user
        userToTimelock[_userAddress] = Timelock({
            totalAmount: _amount,
            remainingAmount: _amount,
            releaseStart: _releaseStart,
            releaseEnd: _releaseEnd
        });
		// treat locked tokens as deposited, but not credited to User.deposited
		depositedUnstaked += _amount;

        enshroudToken.safeTransferFrom(_source, address(this), _amount);

        emit DepositedUnlocking(
            _userAddress,
            _amount,
            _releaseStart,
            _releaseEnd
        );
    }

    /// @notice Called to release tokens locked by the timelock and update state
    /// @dev must be called in order to update a timelock user's 'deposited' amount -- it initialises at 0 regardless of the timelock's _releaseStart
    /// @param _userAddress Address of the user whose timelock status will be updated
    function updateTimelockStatus(address _userAddress) external {
        Timelock storage timelock = userToTimelock[_userAddress];
        uint256 _start = uint256(timelock.releaseStart);
        uint256 _end = uint256(timelock.releaseEnd);
        if (block.number <= _start) revert DAOPool_ReleaseNotStarted();

        // if the timelock is complete and has already been updated previously
        if (timelock.remainingAmount == 0) revert DAOPool_ReleaseEnded();

        uint256 _totalUnlocked;
        if (block.number >= _end) {
            _totalUnlocked = timelock.totalAmount;
        } else {
            uint256 passedTime = block.number - _start;
            uint256 totalTime = _end - _start;
            _totalUnlocked = _mulDivDown(
                timelock.totalAmount,
                passedTime,
                totalTime
            );
        }

        uint256 _newlyUnlocked = _totalUnlocked -
            (timelock.totalAmount - timelock.remainingAmount);
        // newly unlocked tokens are added to user's deposited amount and deducted from the remainingAmount member of the timelock struct
        users[_userAddress].deposited += _newlyUnlocked;
        userToTimelock[_userAddress].remainingAmount -= _newlyUnlocked;

        emit UpdatedTimelock(
            _userAddress,
            _newlyUnlocked,
            userToTimelock[_userAddress].remainingAmount
        );
    }
}

/// @title DAO pool contract
/// @notice Users can stake $ENSHROUD tokens at the pool contract to be granted shares. These shares give the user voting power at the DAO and ability to claim pro-rata in-kind protocol yield.
/// @dev functionalities are distributed to a chain of inheritance:
/// (1) DAOPool.sol
/// (2) TimelockUtils.sol
/// (3) StakeUtils.sol
/// (4) TransferUtils.sol
/// (5) VotingUtils.sol
/// (6) GetterUtils.sol
/// (7) StateUtils.sol
contract DAOPool is TimelockUtils {
    using SafeTransferLib for address;

    /// @param _enshroudProtocol: Enshroud protocol contract address
    /// @param _enshroudTokenAddress: $ENSHROUD token contract address
    constructor(
        address _enshroudProtocol,
        address _enshroudTokenAddress
    ) payable StateUtils(_enshroudProtocol, _enshroudTokenAddress) {}

    /// @notice for a DAO staker to claim their pro rata yields as of block.number. ETH is paid out regardless of token contracts supplied by caller once per epoch.
    /// smaller stakers have the option to time their claims (for epochs in which there are large amounts in this contract, or at least as much yield as the gas expenditure to claim)
    /// @dev claimed mapping is checked for each address and address(0) for ETH every call and userStake (as a ratio of totalStake) is calculated as of block.number; epochPassedSinceStake modifier protects against flash loan + stake + claim
    /// @param _tokenContracts array of token contract addresses which msg.sender seeks to claim in this epoch. Null address not permitted as an input as it used for the ETH claimed mapping.
    function claimYield(
        address[] memory _tokenContracts
    ) external nonReentrant epochPassedSinceStake {
        if (_tokenContracts.length > ARRAY_LIMIT) revert DAOPool_ArrayTooLong();

        // calculate userStake as of block.number
        uint256 _userStake = userStake(msg.sender);
        if (_userStake == 0) revert NoShares(); // ensure msg.sender has share to claim
        address _zero = address(0);

		// process all contracts for which user wants to claim their earnings
        for (uint256 i = 0; i < _tokenContracts.length; ) {
            address _addr = _tokenContracts[i];
            if (_addr == _zero) revert ZeroAddress();
            if (BLOCKS_PER_EPOCH > (block.number - claimed[msg.sender][_addr]))
                revert AlreadyClaimedThisEpoch();

            uint256 _claim;

            // calculate % claim by the ratio of msg.sender's staked amount to total stake, multiplied by the balanceOf each token in the DAOPool
            if (_addr == enshroudToken) {
				// if user is claiming $ENSHROUD tokens, must subtract the staked amount and the deposited unstaked amount from the balance to get the claimable amount
                _claim = _mulDivDown(
                    _userStake,
                    IERC20_DAOPool(_addr).balanceOf(address(this))
						- totalStake - depositedUnstaked,
                    totalStake
                );
            } else {
                _claim = _mulDivDown(
                    _userStake,
                    IERC20_DAOPool(_addr).balanceOf(address(this)),
                    totalStake
                );
            }
            if (_claim != 0) {
                claimed[msg.sender][_addr] = block.number; // update claimed mapping for this asset
                _addr.safeTransfer(msg.sender, _claim); // transfer tokens to msg.sender if there's a claimable balance
            }
            unchecked {
                ++i; // interate loop, cannot overflow without hitting gas limit
            }
        }

        // only auto-claim ETH once per epoch regardless of tokenContract inputs. Use null address since it is impermissible as an input.
        // calculate ETH claim and transfer ETH to msg.sender if > 0 and at least an epoch has passed since last claim, otherwise ignore via else{}
        if (BLOCKS_PER_EPOCH < (block.number - claimed[msg.sender][_zero])) {
            uint256 _ethClaim = _mulDivDown(
                _userStake,
                address(this).balance,
                totalStake
            );
            if (_ethClaim != 0) {
                claimed[msg.sender][_zero] = block.number; // update claimed mapping for ETH
                SafeTransferLib.safeTransferETH(msg.sender, _ethClaim);
            }
        } else {}
    }

    /// @notice for a staker to claim their pro rata token and ETH yields as deposits to the Enshroud Protocol contract
    /// @dev epoch is checked/updated every call and userStake (as a ratio of totalStake) is calculated as of block.number; epochPassedSinceStake modifier protects against flash loan + stake + claim
    /// msg.sender MUST APPROVE() THE ENSHROUD PROTOCOL CONTRACT ADDRESS IN EACH '_tokenContract' FOR THE APPLICABLE CLAIM AMOUNTS
    /// @param _tokenContracts array of token contract addresses which msg.sender seeks to claim in this epoch. Null address not permitted as an input as it used for the ETH claimed mapping.
    function claimTokenYieldAsENFTs(
        address[] memory _tokenContracts
    ) external nonReentrant epochPassedSinceStake {
        if (_tokenContracts.length > ARRAY_LIMIT) revert DAOPool_ArrayTooLong();

        // calculate userStake as of block.number
        uint256 _userStake = userStake(msg.sender);
        if (_userStake == 0) revert NoShares(); // ensure msg.sender has share to claim
        address _zero = address(0);

		// process each token contract for which user wants to claim earnings
        for (uint256 i = 0; i < _tokenContracts.length; ) {
            address _addr = _tokenContracts[i];
            if (_addr == _zero) revert ZeroAddress();
            if (BLOCKS_PER_EPOCH > (block.number - claimed[msg.sender][_addr]))
                revert AlreadyClaimedThisEpoch();

            uint256 _claim;

            // calculate % claim by the ratio of msg.sender's staked amount to total stake, multiplied by the balanceOf each token in the DAOPool
            if (_addr == enshroudToken) {
				// if user is claiming $ENSHROUD tokens, must subtract the staked amount and the deposited unstaked from the balance to get the claimable amount
                _claim = _mulDivDown(
                    _userStake,
                    IERC20_DAOPool(_addr).balanceOf(address(this))
						- totalStake - depositedUnstaked,
                    totalStake
                );
            } else {
                _claim = _mulDivDown(
                    _userStake,
                    IERC20_DAOPool(_addr).balanceOf(address(this)),
                    totalStake
                );
            }
            if (_claim != 0) {
                claimed[msg.sender][_addr] = block.number; // update claimed mapping for this asset

				// account for possibility this ERC20 is fee-on-transfer or a deflationary token
				uint256 _epBalanceBefore = IERC20_DAOPool(_addr).balanceOf(enshroudProtocol);
				/// @dev because SafeTransferLib.safeTransfer() will revert if we attempt to call it as msg.sender in iProtocol._depositTokens(), we do it here
                _addr.safeTransfer(enshroudProtocol, _claim);
				uint256 _epBalanceAfter = IERC20_DAOPool(_addr).balanceOf(enshroudProtocol);
				require(_epBalanceAfter >= _epBalanceBefore);
				uint256 _netClaim = _epBalanceAfter - _epBalanceBefore;
				// tell EnshroudProtocol to credit the net claim to msg.sender
                iProtocol.depositTokens(msg.sender, _addr, _netClaim);
            }
            unchecked {
                ++i; // interate loop, cannot overflow without hitting gas limit
            }
        }

        // only auto-claim ETH once per epoch. Null address used since it is impermissible as an input
        // calculate ETH claim and deposit in protocol contract if > 0 and if at least an epoch has passed since last claim, otherwise ignore via else{}
        if (BLOCKS_PER_EPOCH < (block.number - claimed[msg.sender][_zero])) {
            uint256 _ethClaim = _mulDivDown(
                _userStake,
                address(this).balance,
                totalStake
            );
            if (_ethClaim != 0) {
                claimed[msg.sender][_zero] = block.number; // update claimed mapping for ETH
				// @dev deposited amount will be auto-converted to wETH
                iProtocol.depositEth{value: _ethClaim}(msg.sender);
            }
        } else {}
    }

    /// @notice for a staker to claim their pro rata ETH yield separately as a deposit to the Enshroud Protocol contract
	/// @notice claimTokenYieldAsENFTs() above also claims ETH after claiming yields on the passed list of tokens
    /// @dev epoch is checked/updated every call and userStake (as a ratio of totalStake) is calculated as of block.number; epochPassedSinceStake modifier protects against flash loan + stake + claim
    function claimETHYieldAsENFTs() external nonReentrant epochPassedSinceStake
    {
        // calculate userStake as of block.number
		uint256 _userStake = userStake(msg.sender);
        if (_userStake == 0) revert NoShares(); // ensure msg.sender has share to claim
        address _zero = address(0);

        // only auto-claim ETH once per epoch. Null address used since it is impermissible as an input
        // calculate ETH claim and deposit in protocol contract if > 0 and if at least an epoch has passed since last claim, otherwise ignore via else{}
        if (BLOCKS_PER_EPOCH < (block.number - claimed[msg.sender][_zero])) {
            uint256 _ethClaim = _mulDivDown(
                _userStake,
                address(this).balance,
                totalStake
            );
            if (_ethClaim != 0) {
                claimed[msg.sender][_zero] = block.number; // update claimed mapping for ETH
                iProtocol.depositEth{value: _ethClaim}(msg.sender);
            }
        } else {}
    }
}
