contract RBSTStaking is IRBSTStaking, Ownable, CheckContract, BaseMath {
    using SafeMath for uint;

    // --- Data ---
    string constant public NAME = "RBSTStaking";

    mapping( address => uint) public stakes;
    uint public totalRBSTStaked;

    uint public F_ETH;  // Running sum of ETH fees per-RBST-staked
    uint public F_RUBC; // Running sum of RBST fees per-RBST-staked

    // User snapshots of F_ETH and F_RUBC, taken at the point at which their latest deposit was made
    mapping (address => Snapshot) public snapshots; 

    struct Snapshot {
        uint F_ETH_Snapshot;
        uint F_RUBC_Snapshot;
    }
    
    IRBST public rbst;
    IRUBC public rubc;

    address public troveManagerAddress;
    address public borrowerOperationsAddress;
    address public activePoolAddress;

    // --- Events ---

    event RBSTAddressSet(address _rbstAddress);
    event RUBCAddressSet(address _rubcAddress);
    event TroveManagerAddressSet(address _troveManager);
    event BorrowerOperationsAddressSet(address _borrowerOperationsAddress);
    event ActivePoolAddressSet(address _activePoolAddress);

    event StakeChanged(address indexed staker, uint newStake);
    event StakingGainsWithdrawn(address indexed staker, uint RUBCGain, uint ETHGain);
    event F_ETHUpdated(uint _F_ETH);
    event F_RUBCUpdated(uint _F_RUBC);
    event TotalRBSTStakedUpdated(uint _totalRBSTStaked);
    event EtherSent(address _account, uint _amount);
    event StakerSnapshotsUpdated(address _staker, uint _F_ETH, uint _F_RUBC);

    // --- Functions ---

    function setAddresses
    (
        address _rbstAddress,
        address _rubcAddress,
        address _troveManagerAddress, 
        address _borrowerOperationsAddress,
        address _activePoolAddress
    ) 
        external 
        onlyOwner 
        override 
    {
        checkContract(_rbstAddress);
        checkContract(_rubcAddress);
        checkContract(_troveManagerAddress);
        checkContract(_borrowerOperationsAddress);
        checkContract(_activePoolAddress);

        rbst = IRBST(_rbstAddress);
        rubc = IRUBC(_rubcAddress);
        troveManagerAddress = _troveManagerAddress;
        borrowerOperationsAddress = _borrowerOperationsAddress;
        activePoolAddress = _activePoolAddress;

        emit RBSTAddressSet(_rbstAddress);
        emit RBSTAddressSet(_rubcAddress);
        emit TroveManagerAddressSet(_troveManagerAddress);
        emit BorrowerOperationsAddressSet(_borrowerOperationsAddress);
        emit ActivePoolAddressSet(_activePoolAddress);

        _renounceOwnership();
    }

    // If caller has a pre-existing stake, send any accumulated ETH and RUBC gains to them. 
    function stake(uint _RBSTamount) external override {
        _requireNonZeroAmount(_RBSTamount);

        uint currentStake = stakes[msg.sender];

        uint ETHGain;
        uint RUBCGain;
        // Grab any accumulated ETH and RUBC gains from the current stake
        if (currentStake != 0) {
            ETHGain = _getPendingETHGain(msg.sender);
            RUBCGain = _getPendingRUBCGain(msg.sender);
        }
    
       _updateUserSnapshots(msg.sender);

        uint newStake = currentStake.add(_RBSTamount);

        // Increase user’s stake and total RBST staked
        stakes[msg.sender] = newStake;
        totalRBSTStaked = totalRBSTStaked.add(_RBSTamount);
        emit TotalRBSTStakedUpdated(totalRBSTStaked);

        // Transfer RBST from caller to this contract
        rbst.sendToRBSTStaking(msg.sender, _RBSTamount);

        emit StakeChanged(msg.sender, newStake);
        emit StakingGainsWithdrawn(msg.sender, RUBCGain, ETHGain);

         // Send accumulated RUBC and ETH gains to the caller
        if (currentStake != 0) {
            rubc.transfer(msg.sender, RUBCGain);
            _sendETHGainToUser(ETHGain);
        }
    }

    // Unstake the RBST and send the it back to the caller, along with their accumulated RUBC & ETH gains. 
    // If requested amount > stake, send their entire stake.
    function unstake(uint _RBSTamount) external override {
        uint currentStake = stakes[msg.sender];
        _requireUserHasStake(currentStake);

        // Grab any accumulated ETH and RUBC gains from the current stake
        uint ETHGain = _getPendingETHGain(msg.sender);
        uint RUBCGain = _getPendingRUBCGain(msg.sender);
        
        _updateUserSnapshots(msg.sender);

        if (_RBSTamount > 0) {
            uint RBSTToWithdraw = LiquityMath._min(_RBSTamount, currentStake);

            uint newStake = currentStake.sub(RBSTToWithdraw);

            // Decrease user's stake and total RBST staked
            stakes[msg.sender] = newStake;
            totalRBSTStaked = totalRBSTStaked.sub(RBSTToWithdraw);
            emit TotalRBSTStakedUpdated(totalRBSTStaked);

            // Transfer unstaked RBST to user
            rbst.transfer(msg.sender, RBSTToWithdraw);

            emit StakeChanged(msg.sender, newStake);
        }

        emit StakingGainsWithdrawn(msg.sender, RUBCGain, ETHGain);

        // Send accumulated RUBC and ETH gains to the caller
        rubc.transfer(msg.sender, RUBCGain);
        _sendETHGainToUser(ETHGain);
    }

    // --- Reward-per-unit-staked increase functions. Called by Liquity core contracts ---

    function increaseF_ETH(uint _ETHFee) external override {
        _requireCallerIsTroveManager();
        uint ETHFeePerRBSTStaked;
     
        if (totalRBSTStaked > 0) {ETHFeePerRBSTStaked = _ETHFee.mul(DECIMAL_PRECISION).div(totalRBSTStaked);}

        F_ETH = F_ETH.add(ETHFeePerRBSTStaked); 
        emit F_ETHUpdated(F_ETH);
    }

    function increaseF_RUBC(uint _RUBCFee) external override {
        _requireCallerIsBorrowerOperations();
        uint RUBCFeePerRBSTStaked;
        
        if (totalRBSTStaked > 0) {RUBCFeePerRBSTStaked = _RUBCFee.mul(DECIMAL_PRECISION).div(totalRBSTStaked);}
        
        F_RUBC = F_RUBC.add(RUBCFeePerRBSTStaked);
        emit F_RUBCUpdated(F_RUBC);
    }

    // --- Pending reward functions ---

    function getPendingETHGain(address _user) external view override returns (uint) {
        return _getPendingETHGain(_user);
    }

    function _getPendingETHGain(address _user) internal view returns (uint) {
        uint F_ETH_Snapshot = snapshots[_user].F_ETH_Snapshot;
        uint ETHGain = stakes[_user].mul(F_ETH.sub(F_ETH_Snapshot)).div(DECIMAL_PRECISION);
        return ETHGain;
    }

    function getPendingRUBCGain(address _user) external view override returns (uint) {
        return _getPendingRUBCGain(_user);
    }

    function _getPendingRUBCGain(address _user) internal view returns (uint) {
        uint F_RUBC_Snapshot = snapshots[_user].F_RUBC_Snapshot;
        uint RUBCGain = stakes[_user].mul(F_RUBC.sub(F_RUBC_Snapshot)).div(DECIMAL_PRECISION);
        return RUBCGain;
    }

    // --- Internal helper functions ---

    function _updateUserSnapshots(address _user) internal {
        snapshots[_user].F_ETH_Snapshot = F_ETH;
        snapshots[_user].F_RUBC_Snapshot = F_RUBC;
        emit StakerSnapshotsUpdated(_user, F_ETH, F_RUBC);
    }

    function _sendETHGainToUser(uint ETHGain) internal {
        emit EtherSent(msg.sender, ETHGain);
        (bool success, ) = msg.sender.call{value: ETHGain}("");
        require(success, "RBSTStaking: Failed to send accumulated ETHGain");
    }

    // --- 'require' functions ---

    function _requireCallerIsTroveManager() internal view {
        require(msg.sender == troveManagerAddress, "RBSTStaking: caller is not TroveM");
    }

    function _requireCallerIsBorrowerOperations() internal view {
        require(msg.sender == borrowerOperationsAddress, "RBSTStaking: caller is not BorrowerOps");
    }

     function _requireCallerIsActivePool() internal view {
        require(msg.sender == activePoolAddress, "RBSTStaking: caller is not ActivePool");
    }

    function _requireUserHasStake(uint currentStake) internal pure {  
        require(currentStake > 0, 'RBSTStaking: User must have a non-zero stake');  
    }

    function _requireNonZeroAmount(uint _amount) internal pure {
        require(_amount > 0, 'RBSTStaking: Amount must be non-zero');
    }

    receive() external payable {
        _requireCallerIsActivePool();
    }
}
function setAddresses
(
    address _rbstAddress,
    address _rubcAddress,
    address _troveManagerAddress, 
    address _borrowerOperationsAddress,
    address _activePoolAddress
) 
    external 
    onlyOwner 
    override 
{
    checkContract(_rbstAddress);
    checkContract(_rubcAddress);
    checkContract(_troveManagerAddress);
    checkContract(_borrowerOperationsAddress);
    checkContract(_activePoolAddress);

    rbst = IRBST(_rbstAddress);
    rubc = IRUBC(_rubcAddress);
    troveManagerAddress = _troveManagerAddress;
    borrowerOperationsAddress = _borrowerOperationsAddress;
    activePoolAddress = _activePoolAddress;

    emit RBSTAddressSet(_rbstAddress);
    emit RBSTAddressSet(_rubcAddress);
    emit TroveManagerAddressSet(_troveManagerAddress);
    emit BorrowerOperationsAddressSet(_borrowerOperationsAddress);
    emit ActivePoolAddressSet(_activePoolAddress);

    _renounceOwnership();
}

// --- Functions ---