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
RocketPoolAdapter.solThe 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:
Slippage Protection: When withdrawing ETH, the adapter verifies that the amount received is within acceptable bounds.
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:
Deposit Pool Capacity: The adapter monitors deposit pool capacity and can pause deposits when the pool is full.
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:
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.
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.