contract GenesisSC is Ownable {

    using SafeMath for uint256;
    using Math for uint256;
    using Address for address;

    enum States{Initializing, Staking, Validating, Finalized, Retired}

    // EVENTS
    event StakeDeposited(address indexed account, uint256 amount);
    event StakeWithdrawn(address indexed account, uint256 amount);
    event StateChanged(States fromState, States toState);

    // STRUCT DECLARATIONS
    struct StakingNode {
        bytes32 blsKeyHash;
        bytes32 elrondAddressHash;
        bool approved;
        bool exists;
    }

    struct WhitelistedAccount {
        uint256 numberOfNodes;
        uint256 amountStaked;
        StakingNode[] stakingNodes;
        bool exists;
        mapping(bytes32 => uint256) blsKeyHashToStakingNodeIndex;
    }

    struct DelegationDeposit {
        uint256 amount;
        bytes32 elrondAddressHash;
        bool exists;
    }

    // CONTRACT STATE VARIABLES
    uint256 public nodePrice;
    uint256 public delegationNodesLimit;
    uint256 public delegationAmountLimit;
    uint256 public currentTotalDelegated;
    address[] private _whitelistedAccountAddresses;

    ERC20Burnable public token;
    States public contractState = States.Initializing;

    mapping(address => WhitelistedAccount) private _whitelistedAccounts;
    mapping(address => DelegationDeposit) private _delegationDeposits;
    mapping (bytes32 => bool) private _approvedBlsKeyHashes;

    // MODIFIERS
    modifier onlyContract(address account)
    {
        require(account.isContract(), "[Validation] The address does not contain a contract");
        _;
    }

    modifier guardMaxDelegationLimit(uint256 amount)
    {
        require(amount <= (delegationAmountLimit - currentTotalDelegated), "[DepositDelegateStake] Your deposit would exceed the delegation limit");
        _;
    }

    modifier onlyWhitelistedAccounts(address who)
    {
        WhitelistedAccount memory account = _whitelistedAccounts[who];
        require(account.exists, "[Validation] The provided address is not whitelisted");
        _;
    }

    modifier onlyAccountsWithNodes()
    {
        require(_whitelistedAccounts[msg.sender].stakingNodes.length > 0, "[Validation] Your account has 0 nodes submitted");
        _;
    }

    modifier onlyNotWhitelistedAccounts(address who)
    {
        WhitelistedAccount memory account = _whitelistedAccounts[who];
        require(!account.exists, "[Validation] Address is already whitelisted");
        _;
    }

    // STATE GUARD MODIFIERS
    modifier whenStaking()
    {
        require(contractState == States.Staking, "[Validation] This function can be called only when contract is in staking phase");
        _;
    }

    modifier whenInitializedAndNotValidating()
    {
        require(contractState != States.Initializing, "[Validation] This function cannot be called in the initialization phase");
        require(contractState != States.Validating, "[Validation] This function cannot be called while your submitted nodes are in the validation process");
        _;
    }

    modifier whenFinalized()
    {
        require(contractState == States.Finalized, "[Validation] This function can be called only when the contract is finalized");
        _;
    }

    modifier whenNotFinalized()
    {
        require(contractState != States.Finalized, "[Validation] This function cannot be called when the contract is finalized");
        _;
    }

    modifier whenNotRetired()
    {
        require(contractState != States.Retired, "[Validation] This function cannot be called when the contract is retired");
        _;
    }

    modifier whenRetired()
    {
        require(contractState == States.Retired, "[Validation] This function can be called only when the contract is retired");
        _;
    }

    // PUBLIC FUNCTIONS
    constructor(ERC20Burnable _token, uint256 _nodePrice, uint256 _delegationNodesLimit)
    public
    {
        require(_nodePrice > 0, "[Validation] Node price must be greater than 0");

        token = _token;
        nodePrice = _nodePrice;
        delegationNodesLimit = _delegationNodesLimit;
        delegationAmountLimit = _delegationNodesLimit.mul(_nodePrice);
    }

    /**
    * submitStake can be called in the staking phase by any account that has been previously whitelisted by the Elrond
    *  team. An account can submit hashes of BLS keys to this contract (in a number that adds up to less or equal than
    *  what has been set up for that account) and an associated reward address hash for them. The total amount of ERD
    *  tokens that will be transferred from that account will be fixed to nrOfSubmittedNodes*nodePrice
    *
    * @param blsKeyHashes A list where each element represents the hash of an Elrond's native node public key
    * @param elrondAddressHash Represents the hash of an Elrond's native wallet address
    */
    function submitStake(bytes32[] calldata blsKeyHashes, bytes32 elrondAddressHash)
    external
    whenStaking
    onlyWhitelistedAccounts(msg.sender)
    {
        require(elrondAddressHash != 0, "[Validation] Elrond address hash should not be 0");

        WhitelistedAccount storage whitelistedAccount = _whitelistedAccounts[msg.sender];
        _validateStakeParameters(whitelistedAccount, blsKeyHashes);
        _addStakingNodes(whitelistedAccount, blsKeyHashes, elrondAddressHash);

        uint256 transferAmount = nodePrice.mul(blsKeyHashes.length);
        require(token.transferFrom(msg.sender, address(this), transferAmount));

        whitelistedAccount.amountStaked = whitelistedAccount.amountStaked.add(transferAmount);

        emit StakeDeposited(msg.sender, transferAmount);
    }

    /**
    * withdraw can be called by any account that has been whitelisted and has submitted BLS key hashes for their nodes
    *  and implicitly ERD tokens as staking value for them. This function will withdraw all associated tokens for
    *  nodes that have not been validated off-chain and approved by the Elrond team. If an account wants to give up
    *  and withdraw tokens for an already approved node (if the phase the contract is in still permits it), he/she
    *  can use withdrawPerNodes function
    */
    function withdraw()
    external
    whenInitializedAndNotValidating
    onlyWhitelistedAccounts(msg.sender)
    onlyAccountsWithNodes
    {
        uint256 totalSumToWithdraw;
        WhitelistedAccount storage account = _whitelistedAccounts[msg.sender];

        uint256 length = account.stakingNodes.length - 1;
        for (uint256 i = length; i <= length; i--) {
            StakingNode storage stakingNode = account.stakingNodes[i];
            if ((!stakingNode.exists) || (stakingNode.approved)) {
                continue;
            }

            totalSumToWithdraw = totalSumToWithdraw.add(nodePrice);

            _removeStakingNode(account, stakingNode.blsKeyHash);
        }

        if (totalSumToWithdraw == 0) {
            emit StakeWithdrawn(msg.sender, 0);
            return;
        }

        account.amountStaked = account.amountStaked.sub(totalSumToWithdraw);

        require(token.transfer(msg.sender, totalSumToWithdraw));

        emit StakeWithdrawn(msg.sender, totalSumToWithdraw);
    }

    /**
    * withdrawPerNodes gives the user the possibility to withdraw funds associated with the provided BLS key hashes.
    *  This function allows withdrawal also for nodes that were approved by the Elrond team, with the mention that
    *  it should happen before the contract gets in the finalized or retired state (meaning the genesis of the Elrond blockchain
    *  is established and those tokens will be minted on the main chain)
    *
    * @param blsKeyHashes A list where each element represents the hash of an Elrond's native node public key
    */
    function withdrawPerNodes(bytes32[] calldata blsKeyHashes)
    external
    whenInitializedAndNotValidating
    onlyWhitelistedAccounts(msg.sender)
    onlyAccountsWithNodes
    {
        require(blsKeyHashes.length > 0, "[Validation] You must provide at least one BLS key");

        WhitelistedAccount storage account = _whitelistedAccounts[msg.sender];
        for (uint256 i; i < blsKeyHashes.length; i++) {
            _validateBlsKeyHashForWithdrawal(account, blsKeyHashes[i]);
            _removeStakingNode(account, blsKeyHashes[i]);
        }

        uint256 totalSumToWithdraw = nodePrice.mul(blsKeyHashes.length);
        account.amountStaked = account.amountStaked.sub(totalSumToWithdraw);

        require(token.transfer(msg.sender, totalSumToWithdraw));

        emit StakeWithdrawn(msg.sender, totalSumToWithdraw);
    }

    /**
    * depositDelegateStake provides users that were not whitelisted to run nodes at the start of the
    *  Elrond blockchain with the possibility to take part anyway in the genesis of the network
    *  by delegating stake to nodes that will be ran by Elrond. The rewards will be received
    *  by the user according to the Elrond's delegation smart contract in the provided wallet address
    *
    * @param elrondAddressHash The Elrond native address hash where the user wants to receive the rewards
    * @param amount The ERD amount to be staked
    */
    function depositDelegateStake(uint256 amount, bytes32 elrondAddressHash)
    external
    whenStaking
    guardMaxDelegationLimit(amount)
    {
        require(amount > 0, "[Validation] The stake amount has to be larger than 0");
        require(!_delegationDeposits[msg.sender].exists, "[Validation] You already delegated a stake");

        _delegationDeposits[msg.sender] = DelegationDeposit(amount, elrondAddressHash, true);

        currentTotalDelegated = currentTotalDelegated.add(amount);

        require(token.transferFrom(msg.sender, address(this), amount));

        emit StakeDeposited(msg.sender, amount);
    }

    /**
    * increaseDelegatedAmount lets a user that has already delegated a number of tokens to increase that amount
    *
    * @param amount The ERD amount to be added to the existing stake
    */
    function increaseDelegatedAmount(uint256 amount)
    external
    whenStaking
    guardMaxDelegationLimit(amount)
    {
        require(amount > 0, "[Validation] The amount has to be larger than 0");

        DelegationDeposit storage deposit = _delegationDeposits[msg.sender];
        require(deposit.exists, "[Validation] You don't have a delegated stake");

        deposit.amount = deposit.amount.add(amount);
        currentTotalDelegated = currentTotalDelegated.add(amount);

        require(token.transferFrom(msg.sender, address(this), amount));

        emit StakeDeposited(msg.sender, amount);
    }

    /**
    * withdrawDelegatedStake lets a user that has already delegated a number of tokens to decrease that amount
    *
    * @param amount The ERD amount to be removed to the existing stake
    */
    function withdrawDelegatedStake(uint256 amount)
    external
    whenStaking
    {
        require(amount > 0, "[Validation] The withdraw amount has to be larger than 0");

        DelegationDeposit storage deposit = _delegationDeposits[msg.sender];
        require(deposit.exists, "[Validation] You don't have a delegated stake");
        require(amount <= deposit.amount, "[Validation] Not enough stake deposit to withdraw");

        deposit.amount = deposit.amount.sub(amount);
        currentTotalDelegated = currentTotalDelegated.sub(amount);
        require(token.transfer(msg.sender, amount));

        emit StakeWithdrawn(msg.sender, amount);
    }

    // OWNER ONLY FUNCTIONS

    /**
    * changeStateToStaking allows the owner to change the state of the contract into the staking phase
    */
    function changeStateToStaking()
    external
    onlyOwner
    whenNotRetired
    {
        emit StateChanged(contractState, States.Staking);
        contractState = States.Staking;
    }

    /**
    * changeStateToValidating allows the owner to change the state of the contract into the validating phase. With the
    *  mention that we can go into validating phase only from the staking phase.
    */
    function changeStateToValidating()
    external
    onlyOwner
    whenStaking
    {
        emit StateChanged(contractState, States.Validating);
        contractState = States.Validating;
    }

    /**
    * changeStateToFinalized allows the owner to change the state of the contract into the finalized phase
    */
    function changeStateToFinalized()
    external
    onlyOwner
    whenNotRetired
    {
        emit StateChanged(contractState, States.Finalized);
        contractState = States.Finalized;
    }

    /**
   * changeStateToRetired allows the owner to change the state of the contract into the retired phase.
   *  this can only happen if the contract is finalized - in order to prevent retiring it by mistake,
   *  since there is no turning back from this state
   */
    function changeStateToRetired()
    external
    onlyOwner
    whenFinalized
    {
        emit StateChanged(contractState, States.Retired);
        contractState = States.Retired;
    }

    /**
    * whitelistAccount allows the owner to whitelist an ethereum address to stake ERD and add nodes to run
    *  on the Elrond blockchain
    */
    function whitelistAccount(address who, uint256 numberOfNodes)
    external
    onlyOwner
    whenNotFinalized
    whenNotRetired
    onlyNotWhitelistedAccounts(who)
    {
        WhitelistedAccount storage whitelistedAccount = _whitelistedAccounts[who];
        whitelistedAccount.numberOfNodes = numberOfNodes;
        whitelistedAccount.exists = true;

        _whitelistedAccountAddresses.push(who);
    }

    /**
    * approveBlsKeyHashes gives the owner the possibility to mark some BLS key hashes submitted by an account
    *  as approved after an off-chain validation
    */
    function approveBlsKeyHashes(address who, bytes32[] calldata blsHashes)
    external
    onlyOwner
    whenNotFinalized
    whenNotRetired
    onlyWhitelistedAccounts(who)
    {
        WhitelistedAccount storage whitelistedAccount = _whitelistedAccounts[who];

        for (uint256 i = 0; i < blsHashes.length; i++) {
            require(_accountHasNode(whitelistedAccount, blsHashes[i]), "[Validation] BLS key does not exist for this account");
            require(!_approvedBlsKeyHashes[blsHashes[i]], "[Validation] Provided BLS key was already approved");

            uint256 accountIndex = whitelistedAccount.blsKeyHashToStakingNodeIndex[blsHashes[i]];
            StakingNode storage stakingNode = whitelistedAccount.stakingNodes[accountIndex];
            require(stakingNode.exists, '[Validation] Bls key does not exist for this account');
            stakingNode.approved = true;
            _approvedBlsKeyHashes[blsHashes[i]] = true;
        }
    }

    /**
    * unapproveBlsKeyHashes the same as approveBlsKeyHashes, but changing the approved flag to false for selected keys
    */
    function unapproveBlsKeyHashes(address who, bytes32[] calldata blsHashes)
    external
    onlyOwner
    whenNotFinalized
    whenNotRetired
    onlyWhitelistedAccounts(who)
    {
        WhitelistedAccount storage whitelistedAccount = _whitelistedAccounts[who];

        for (uint256 i = 0; i < blsHashes.length; i++) {
            require(_accountHasNode(whitelistedAccount, blsHashes[i]), "[Validation] BLS key does not exist for this account");
            require(_approvedBlsKeyHashes[blsHashes[i]], "[Validation] Provided BLS key was not previously approved");

            uint256 accountIndex = whitelistedAccount.blsKeyHashToStakingNodeIndex[blsHashes[i]];
            StakingNode storage stakingNode = whitelistedAccount.stakingNodes[accountIndex];
            require(stakingNode.exists, '[Validation] Bls key does not exist for this account');
            stakingNode.approved = false;
            _approvedBlsKeyHashes[blsHashes[i]] = false;
        }
    }

    /**
    * editWhitelistedAccountNumberOfNodes gives the owner the possibility to change the number of nodes a user can
    *  stake for. The number cannot be set lower than the number of nodes the user already submitted
    */
    function editWhitelistedAccountNumberOfNodes(address who, uint256 numberOfNodes)
    external
    onlyOwner
    whenNotFinalized
    whenNotRetired
    onlyWhitelistedAccounts(who)
    {
        WhitelistedAccount storage whitelistedAccount = _whitelistedAccounts[who];
        require(numberOfNodes >= whitelistedAccount.stakingNodes.length, "[Validation] Whitelisted account already submitted more nodes than you wish to allow");

        whitelistedAccount.numberOfNodes = numberOfNodes;
    }

    /**
    * burnCommittedFunds can be called by the owner after this contract is retired. This function burns the amount
    *  of tokens associated with approved nodes and delegated stake. The equivalent will be minted on the
    *  Elrond blockchain
    */
    function burnCommittedFunds()
    external
    onlyOwner
    whenRetired
    {
        uint256 totalToBurn = currentTotalDelegated;
        for(uint256 i; i < _whitelistedAccountAddresses.length; i++) {
            WhitelistedAccount memory account = _whitelistedAccounts[_whitelistedAccountAddresses[i]];
            if (!account.exists) {
                continue;
            }

            uint256 approvedNodes = _approvedNodesCount(account);
            totalToBurn = totalToBurn.add(nodePrice.mul(approvedNodes));
        }

        token.burn(totalToBurn);
    }

    /**
    * recoverLostFunds helps us recover funds for users that accidentally send tokens directly to this contract
    */
    function recoverLostFunds(address who, uint256 amount)
    external
    onlyOwner
    {
        uint256 currentBalance = token.balanceOf(address(this));
        require(amount <= currentBalance, "[Validation] Recover amount exceeds contract balance");

        uint256 correctDepositAmount = _correctDepositAmount();
        uint256 lostFundsAmount = currentBalance.sub(correctDepositAmount);
        require(amount <= lostFundsAmount, "[Validation] Recover amount exceeds lost funds amount");

        token.transfer(who, amount);
    }

    // VIEW FUNCTIONS
    function whitelistedAccountAddresses()
    external
    view
    returns (address[] memory, uint256[] memory)
    {
        address[] memory whitelistedAddresses = new address[](_whitelistedAccountAddresses.length);
        uint256[] memory whitelistedAddressesNodes = new uint256[](_whitelistedAccountAddresses.length);

        for (uint256 i = 0; i < _whitelistedAccountAddresses.length; i++) {
            whitelistedAddresses[i] = _whitelistedAccountAddresses[i];
            WhitelistedAccount storage whitelistedAccount = _whitelistedAccounts[_whitelistedAccountAddresses[i]];
            whitelistedAddressesNodes[i] = whitelistedAccount.numberOfNodes;

        }

        return (whitelistedAddresses, whitelistedAddressesNodes);
    }

    function whitelistedAccount(address who)
    external
    view
    returns (uint256 maxNumberOfNodes, uint256 amountStaked)
    {
        require(_whitelistedAccounts[who].exists, "[WhitelistedAddress] Address is not whitelisted");

        return (_whitelistedAccounts[who].numberOfNodes, _whitelistedAccounts[who].amountStaked);
    }

    function stakingNodesHashes(address who)
    external
    view
    returns (bytes32[] memory, bool[] memory, bytes32[] memory)
    {
        require(_whitelistedAccounts[who].exists, "[StakingNodesHashes] Address is not whitelisted");

        StakingNode[] memory stakingNodes = _whitelistedAccounts[who].stakingNodes;
        bytes32[] memory blsKeyHashes = new bytes32[](stakingNodes.length);
        bool[] memory blsKeyHashesStatus = new bool[](stakingNodes.length);
        bytes32[] memory rewardAddresses = new bytes32[](stakingNodes.length);

        for (uint256 i = 0; i < stakingNodes.length; i++) {
            blsKeyHashes[i] = stakingNodes[i].blsKeyHash;
            blsKeyHashesStatus[i] = stakingNodes[i].approved;
            rewardAddresses[i] = stakingNodes[i].elrondAddressHash;
        }

        return (blsKeyHashes, blsKeyHashesStatus, rewardAddresses);
    }

    function stakingNodeInfo(address who, bytes32 blsKeyHash)
    external
    view
    returns(bytes32, bool)
    {
        require(_whitelistedAccounts[who].exists, "[StakingNodeInfo] Address is not whitelisted");
        require(_accountHasNode(_whitelistedAccounts[who], blsKeyHash), "[StakingNodeInfo] Address does not have the provided node");

        WhitelistedAccount storage account = _whitelistedAccounts[who];
        uint256 nodeIndex = account.blsKeyHashToStakingNodeIndex[blsKeyHash];
        return (account.stakingNodes[nodeIndex].elrondAddressHash, account.stakingNodes[nodeIndex].approved);
    }

    function delegationDeposit(address who)
    external
    view
    returns (uint256, bytes32)
    {
        return (_delegationDeposits[who].amount, _delegationDeposits[who].elrondAddressHash);
    }

    function lostFundsAmount()
    external
    view
    returns (uint256)
    {
        uint256 currentBalance = token.balanceOf(address(this));
        uint256 correctDepositAmount = _correctDepositAmount();

        return currentBalance.sub(correctDepositAmount);
    }

    // PRIVATE FUNCTIONS
    function _addStakingNodes(WhitelistedAccount storage account, bytes32[] memory blsKeyHashes, bytes32 elrondAddressHash)
    internal
    {
        for (uint256 i = 0; i < blsKeyHashes.length; i++) {
            _insertStakingNode(account, blsKeyHashes[i], elrondAddressHash);
        }
    }

    function _validateStakeParameters(WhitelistedAccount memory account, bytes32[] memory blsKeyHashes)
    internal
    pure
    {
        require(
            account.numberOfNodes >= account.stakingNodes.length + blsKeyHashes.length,
            "[Validation] Adding this many nodes would exceed the maximum number of allowed nodes per this account"
        );
    }

    function _correctDepositAmount()
    internal
    view
    returns (uint256)
    {
        uint256 correctDepositAmount = currentTotalDelegated;
        for(uint256 i; i < _whitelistedAccountAddresses.length; i++) {
            WhitelistedAccount memory account = _whitelistedAccounts[_whitelistedAccountAddresses[i]];
            if (!account.exists) {
                continue;
            }

            correctDepositAmount = correctDepositAmount.add(nodePrice.mul(account.stakingNodes.length));
        }

        return correctDepositAmount;
    }

    // StakingNode list manipulation
    function _accountHasNode(WhitelistedAccount storage account, bytes32 blsKeyHash)
    internal
    view
    returns (bool)
    {
        if (account.stakingNodes.length == 0) {
            return false;
        }

        uint256 nodeIndex = account.blsKeyHashToStakingNodeIndex[blsKeyHash];

        return (account.stakingNodes[nodeIndex].blsKeyHash == blsKeyHash) && account.stakingNodes[nodeIndex].exists;
    }

    function _approvedNodesCount(WhitelistedAccount memory account)
    internal
    pure
    returns(uint256)
    {
        uint256 nodesCount = 0;

        for(uint256 i = 0; i < account.stakingNodes.length; i++) {
            if (account.stakingNodes[i].exists && account.stakingNodes[i].approved) {
                nodesCount++;
            }
        }

        return nodesCount;
    }

    // Node operations
    function _insertStakingNode(WhitelistedAccount storage account, bytes32 blsKeyHash, bytes32 elrondAddressHash)
    internal
    {
        require(blsKeyHash != 0, "[Validation] BLS key hash should not be 0");
        require(!_accountHasNode(account, blsKeyHash), "[Validation] BLS key was already added for this account");

        account.blsKeyHashToStakingNodeIndex[blsKeyHash] = account.stakingNodes.length;
        StakingNode memory newNode = StakingNode(blsKeyHash, elrondAddressHash, false, true);
        account.stakingNodes.push(newNode);
    }

    function _removeStakingNode(WhitelistedAccount storage account, bytes32 blsKeyHash)
    internal
    {
        uint256 nodeIndex = account.blsKeyHashToStakingNodeIndex[blsKeyHash];
        uint256 lastNodeIndex = account.stakingNodes.length - 1;

        bool stakingNodeIsApproved = account.stakingNodes[nodeIndex].approved;

        // It's not the last StakingNode so we replace this one with the last one
        if (nodeIndex != lastNodeIndex) {
            bytes32 lastHash = account.stakingNodes[lastNodeIndex].blsKeyHash;
            account.blsKeyHashToStakingNodeIndex[lastHash] = nodeIndex;
            account.stakingNodes[nodeIndex] = account.stakingNodes[lastNodeIndex];
        }

        if (stakingNodeIsApproved) {
            delete _approvedBlsKeyHashes[blsKeyHash];
        }

        account.stakingNodes.pop();
        delete account.blsKeyHashToStakingNodeIndex[blsKeyHash];
    }

    function _validateBlsKeyHashForWithdrawal(WhitelistedAccount storage account, bytes32 blsKeyHash)
    internal
    view
    {
        require(_accountHasNode(account, blsKeyHash), "[Validation] BLS key does not exist for this account");
        if (contractState == States.Finalized || contractState == States.Retired) {
            require(
                !account.stakingNodes[account.blsKeyHashToStakingNodeIndex[blsKeyHash]].approved,
                "[Validation] BLS key was already approved, you cannot withdraw the associated amount"
            );
        }
    }
}
function withdrawDelegatedStake(uint256 amount)
external
whenStaking
{
    require(amount > 0, "[Validation] The withdraw amount has to be larger than 0");

    DelegationDeposit storage deposit = _delegationDeposits[msg.sender];
    require(deposit.exists, "[Validation] You don't have a delegated stake");
    require(amount <= deposit.amount, "[Validation] Not enough stake deposit to withdraw");

    deposit.amount = deposit.amount.sub(amount);
    currentTotalDelegated = currentTotalDelegated.sub(amount);
    require(token.transfer(msg.sender, amount));

    emit StakeWithdrawn(msg.sender, amount);
}

/**
 * withdrawDelegatedStake lets a user that has already delegated a number of tokens to decrease that amount
 *
 * @param amount The ERD amount to be removed to the existing stake
 */