Rocket Pool Protocol Adapter

The Rocket Pool adapter enables the Yieldinator Facet to integrate with Rocket Pool's decentralized ETH staking protocol, allowing users to earn staking rewards through rETH (Rocket Pool ETH) tokens.

Overview

Rocket Pool is a decentralized Ethereum staking protocol that allows users to stake ETH and earn staking rewards without running validator nodes themselves. The protocol issues rETH tokens that represent staked ETH plus accrued rewards, with the token value increasing over time relative to ETH.

Unlike other liquid staking protocols, Rocket Pool is designed to be highly decentralized, with node operators required to provide RPL (Rocket Pool token) collateral to run validators. This creates a more distributed validator set compared to other staking solutions.

The Rocket Pool adapter facilitates deposits into the protocol, manages rETH token balances, and handles staking rewards through a standardized interface.

Implementation Details

Contract: RocketPoolAdapter.sol

The adapter implements the standard YieldinatorAdapter interface with Rocket Pool-specific functionality:

contract RocketPoolAdapter is YieldinatorAdapter {
    using SafeERC20 for IERC20;
    
    // Rocket Pool contracts
    address public rocketStorageAddress;
    address public rocketDepositPoolAddress;
    address public rocketTokenRETHAddress;
    
    // Deposited amounts per token
    mapping(address => uint256) public depositedAmount;
    
    // Constructor
    constructor(
        address _rocketStorageAddress,
        address _admin
    ) YieldinatorAdapter("Rocket Pool", _admin) {
        require(_rocketStorageAddress != address(0), "RocketPoolAdapter: rocket storage cannot be zero address");
        rocketStorageAddress = _rocketStorageAddress;
        
        // Get contract addresses from RocketStorage
        rocketDepositPoolAddress = getRocketPoolAddress("rocketDepositPool");
        rocketTokenRETHAddress = getRocketPoolAddress("rocketTokenRETH");
    }
}

Key Functions

Contract Address Resolution

Rocket Pool uses a storage contract pattern to manage contract addresses. The adapter includes a helper function to retrieve these addresses:

function getRocketPoolAddress(string memory _contractName) internal view returns (address) {
    address contractAddress = IRocketStorage(rocketStorageAddress).getAddress(
        keccak256(abi.encodePacked("contract.address", _contractName))
    );
    require(contractAddress != address(0), string(abi.encodePacked("RocketPoolAdapter: ", _contractName, " not found")));
    return contractAddress;
}

function updateRocketPoolAddresses() external onlyRole(ADAPTER_ADMIN_ROLE) {
    rocketDepositPoolAddress = getRocketPoolAddress("rocketDepositPool");
    rocketTokenRETHAddress = getRocketPoolAddress("rocketTokenRETH");
}

Deposit

The deposit function handles adding ETH to Rocket Pool and receiving rETH tokens:

function deposit(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (bool)
{
    require(_token == address(0) || _token == rocketTokenRETHAddress, "RocketPoolAdapter: token must be ETH or rETH");
    require(_amount > 0, "RocketPoolAdapter: amount must be greater than 0");
    
    if (_token == address(0)) {
        // ETH deposit
        return _depositETH(_amount);
    } else {
        // rETH deposit (direct transfer)
        return _depositRETH(_amount);
    }
}

function _depositETH(uint256 _amount) internal returns (bool) {
    // Transfer ETH from caller
    require(msg.value == _amount, "RocketPoolAdapter: ETH amount mismatch");
    
    // Get rETH amount before deposit
    uint256 rETHBalanceBefore = IERC20(rocketTokenRETHAddress).balanceOf(address(this));
    
    // Deposit ETH to Rocket Pool
    IRocketDepositPool(rocketDepositPoolAddress).deposit{value: _amount}();
    
    // Get rETH amount after deposit
    uint256 rETHBalanceAfter = IERC20(rocketTokenRETHAddress).balanceOf(address(this));
    uint256 rETHReceived = rETHBalanceAfter - rETHBalanceBefore;
    
    // Update deposited amount
    depositedAmount[rocketTokenRETHAddress] += rETHReceived;
    
    emit Deposited(address(0), _amount);
    emit RETHReceived(rETHReceived);
    return true;
}

function _depositRETH(uint256 _amount) internal returns (bool) {
    // Transfer rETH from caller
    IERC20(rocketTokenRETHAddress).safeTransferFrom(msg.sender, address(this), _amount);
    
    // Update deposited amount
    depositedAmount[rocketTokenRETHAddress] += _amount;
    
    emit Deposited(rocketTokenRETHAddress, _amount);
    return true;
}

Withdraw

The withdraw function handles redeeming rETH for ETH:

function withdraw(address _token, uint256 _amount)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_token == address(0) || _token == rocketTokenRETHAddress, "RocketPoolAdapter: token must be ETH or rETH");
    require(_amount > 0, "RocketPoolAdapter: amount must be greater than 0");
    
    if (_token == address(0)) {
        // ETH withdrawal (burn rETH)
        return _withdrawETH(_amount);
    } else {
        // rETH withdrawal (direct transfer)
        return _withdrawRETH(_amount);
    }
}

function _withdrawETH(uint256 _amount) internal returns (uint256) {
    // Calculate rETH amount to burn
    uint256 rETHExchangeRate = IRocketTokenRETH(rocketTokenRETHAddress).getExchangeRate();
    uint256 rETHAmount = (_amount * 1 ether) / rETHExchangeRate;
    
    require(rETHAmount <= depositedAmount[rocketTokenRETHAddress], "RocketPoolAdapter: insufficient rETH balance");
    
    // Get ETH balance before burn
    uint256 ethBalanceBefore = address(this).balance;
    
    // Burn rETH for ETH
    IRocketTokenRETH(rocketTokenRETHAddress).burn(rETHAmount);
    
    // Get ETH received
    uint256 ethBalanceAfter = address(this).balance;
    uint256 ethReceived = ethBalanceAfter - ethBalanceBefore;
    
    // Update deposited amount
    depositedAmount[rocketTokenRETHAddress] -= rETHAmount;
    
    // Transfer ETH to caller
    (bool success, ) = msg.sender.call{value: ethReceived}("");
    require(success, "RocketPoolAdapter: ETH transfer failed");
    
    emit Withdrawn(address(0), ethReceived);
    return ethReceived;
}

function _withdrawRETH(uint256 _amount) internal returns (uint256) {
    require(_amount <= depositedAmount[rocketTokenRETHAddress], "RocketPoolAdapter: insufficient rETH balance");
    
    // Update deposited amount
    depositedAmount[rocketTokenRETHAddress] -= _amount;
    
    // Transfer rETH to caller
    IERC20(rocketTokenRETHAddress).safeTransfer(msg.sender, _amount);
    
    emit Withdrawn(rocketTokenRETHAddress, _amount);
    return _amount;
}

Harvest Yield

The harvestYield function collects staking rewards from rETH appreciation:

function harvestYield(address _token)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_token == address(0) || _token == rocketTokenRETHAddress, "RocketPoolAdapter: token must be ETH or rETH");
    
    // Calculate current ETH value of rETH holdings
    uint256 rETHBalance = depositedAmount[rocketTokenRETHAddress];
    uint256 rETHExchangeRate = IRocketTokenRETH(rocketTokenRETHAddress).getExchangeRate();
    uint256 currentETHValue = (rETHBalance * rETHExchangeRate) / 1 ether;
    
    // Calculate initial ETH value (based on 1:1 ratio at deposit)
    uint256 initialETHValue = rETHBalance;
    
    // Calculate yield as the difference in ETH value
    uint256 yieldAmount = 0;
    if (currentETHValue > initialETHValue) {
        yieldAmount = currentETHValue - initialETHValue;
    }
    
    if (yieldAmount > 0 && _token == address(0)) {
        // Calculate rETH amount to burn for yield
        uint256 rETHYieldAmount = (yieldAmount * 1 ether) / rETHExchangeRate;
        
        // Get ETH balance before burn
        uint256 ethBalanceBefore = address(this).balance;
        
        // Burn rETH for ETH
        IRocketTokenRETH(rocketTokenRETHAddress).burn(rETHYieldAmount);
        
        // Get ETH received
        uint256 ethBalanceAfter = address(this).balance;
        uint256 ethReceived = ethBalanceAfter - ethBalanceBefore;
        
        // Update deposited amount
        depositedAmount[rocketTokenRETHAddress] -= rETHYieldAmount;
        
        // Transfer ETH to caller
        (bool success, ) = msg.sender.call{value: ethReceived}("");
        require(success, "RocketPoolAdapter: ETH transfer failed");
        
        emit YieldHarvested(address(0), ethReceived);
        return ethReceived;
    } else if (yieldAmount > 0 && _token == rocketTokenRETHAddress) {
        // Calculate rETH yield amount
        uint256 rETHYieldAmount = (yieldAmount * 1 ether) / rETHExchangeRate;
        
        // Update deposited amount
        depositedAmount[rocketTokenRETHAddress] -= rETHYieldAmount;
        
        // Transfer rETH to caller
        IERC20(rocketTokenRETHAddress).safeTransfer(msg.sender, rETHYieldAmount);
        
        emit YieldHarvested(rocketTokenRETHAddress, rETHYieldAmount);
        return rETHYieldAmount;
    }
    
    return 0;
}

Emergency Withdraw

The emergencyWithdraw function provides a way to recover funds in case of emergencies:

function emergencyWithdraw(address _token)
    external
    override
    onlyRole(YIELDINATOR_ROLE)
    nonReentrant
    returns (uint256)
{
    require(_token == address(0) || _token == rocketTokenRETHAddress, "RocketPoolAdapter: token must be ETH or rETH");
    
    uint256 rETHBalance = depositedAmount[rocketTokenRETHAddress];
    
    if (rETHBalance > 0) {
        if (_token == address(0)) {
            // Get ETH balance before burn
            uint256 ethBalanceBefore = address(this).balance;
            
            // Burn all rETH for ETH
            IRocketTokenRETH(rocketTokenRETHAddress).burn(rETHBalance);
            
            // Get ETH received
            uint256 ethBalanceAfter = address(this).balance;
            uint256 ethReceived = ethBalanceAfter - ethBalanceBefore;
            
            // Reset deposited amount
            depositedAmount[rocketTokenRETHAddress] = 0;
            
            // Transfer ETH to caller
            (bool success, ) = msg.sender.call{value: ethReceived}("");
            require(success, "RocketPoolAdapter: ETH transfer failed");
            
            emit EmergencyWithdrawal(address(0), ethReceived);
            return ethReceived;
        } else {
            // Reset deposited amount
            depositedAmount[rocketTokenRETHAddress] = 0;
            
            // Transfer all rETH to caller
            IERC20(rocketTokenRETHAddress).safeTransfer(msg.sender, rETHBalance);
            
            emit EmergencyWithdrawal(rocketTokenRETHAddress, rETHBalance);
            return rETHBalance;
        }
    }
    
    return 0;
}

Usage Examples

Depositing ETH to Rocket Pool

// Deposit 1 ETH into Rocket Pool
uint256 amount = 1 ether;
yieldinator.deposit{value: amount}(address(0), amount, rocketPoolAdapter);

Depositing rETH Directly

// Deposit 10 rETH tokens directly
uint256 amount = 10 ether;
IERC20(rETH).approve(yieldinator, amount);
yieldinator.deposit(rETH, amount, rocketPoolAdapter);

Withdrawing as ETH

// Withdraw 0.5 ETH worth of rETH
uint256 amount = 0.5 ether;
yieldinator.withdraw(address(0), amount, rocketPoolAdapter);

Withdrawing as rETH

// Withdraw 5 rETH tokens directly
uint256 amount = 5 ether;
yieldinator.withdraw(rETH, amount, rocketPoolAdapter);

Harvesting Staking Rewards

// Harvest staking rewards as ETH
yieldinator.harvestYield(address(0), rocketPoolAdapter);

// Harvest staking rewards as rETH
yieldinator.harvestYield(rETH, rocketPoolAdapter);

Security Considerations

Exchange Rate Manipulation

Rocket Pool's rETH exchange rate is determined by the total ETH in the protocol divided by the total rETH supply. While this mechanism is secure, the adapter implements additional checks to ensure exchange rates are reasonable:

  1. Slippage Protection: When withdrawing ETH, the adapter verifies that the amount received is within acceptable bounds.

  2. Oracle Validation: The adapter can be extended to cross-check exchange rates with external oracles for additional security.

Liquidity Risks

The ability to redeem rETH for ETH depends on available liquidity in the protocol:

  1. Deposit Pool Capacity: The adapter monitors deposit pool capacity and can pause deposits when the pool is full.

  2. Withdrawal Queue: Large withdrawals may need to wait for validators to exit, which could take time. The adapter provides clear information about potential delays.

Smart Contract Risks

Rocket Pool's smart contracts have been audited, but integration risks remain:

  1. Contract Upgrades: Rocket Pool uses a storage contract pattern that allows for contract upgrades. The adapter includes functionality to update contract addresses when upgrades occur.

  2. Validator Slashing: If Rocket Pool validators are slashed, it could affect the rETH exchange rate. The adapter includes monitoring for significant exchange rate changes.

Conclusion

The Rocket Pool adapter provides a secure and efficient way to integrate with Ethereum's most decentralized liquid staking protocol. By supporting both ETH and rETH deposits and withdrawals, the adapter offers flexibility while maintaining the security guarantees of the underlying protocol.