Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arbitrage #248

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions contracts/contract/minipool/RocketMinipoolDelegate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
// Only accepts calls from the RocketDepositPool contract
function userDeposit() override external payable onlyLatestContract("rocketDepositPool", msg.sender) onlyInitialised {
// Check current status & user deposit status
require(status >= MinipoolStatus.Initialised && status <= MinipoolStatus.Staking, "The user deposit can only be assigned while initialised, in prelaunch, or staking");
require(status == MinipoolStatus.Initialised || status == MinipoolStatus.Prelaunch || status == MinipoolStatus.Staking || status == MinipoolStatus.RequestedWithdrawable,
"The user deposit can only be assigned while initialised, in prelaunch, staking, or requestedWithdrawable");
require(userDepositAssignedTime == 0, "The user deposit has already been assigned");
// Progress initialised minipool to prelaunch
if (status == MinipoolStatus.Initialised) { setStatus(MinipoolStatus.Prelaunch); }
Expand Down Expand Up @@ -256,7 +257,7 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
// Get contracts
RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress("rocketMinipoolManager"));
// Check current status
require(status == MinipoolStatus.Staking, "The minipool can only become withdrawable while staking");
require(status == MinipoolStatus.Staking || status = RequestedWithdrawable, "The minipool can only become withdrawable while staking or requested withdrawable");
// Progress to withdrawable
setStatus(MinipoolStatus.Withdrawable);
// Remove minipool from queue
Expand All @@ -269,6 +270,15 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
rocketMinipoolManager.decrementNodeStakingMinipoolCount(nodeAddress);
}

// Only needs to be called if exiting owner is arbitraging
// Should be called before the validator is fully exited (including going through exit queue)
// Once this is called, distributeBalance() will lock out non-owners for 14 days
// This allows owner to bundle distributeBalance() call with a rETH burn to arbitrage
function requestWithdrawable() override external onlyInitialised onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) {
require(status == MinipoolStatus.Staking);
setStatus(MinipoolStatus.RequestWithdrawable);
}

// Distributes the contract's balance and finalises the pool
function distributeBalanceAndFinalise() override external onlyInitialised onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) {
// Can only call if withdrawable and can only be called once
Expand All @@ -282,22 +292,39 @@ contract RocketMinipoolDelegate is RocketMinipoolStorageLayout, RocketMinipoolIn
}

// Distributes the contract's balance
// When called during staking status, requires 16 ether in the pool
// When called by non-owner with less than 16 ether, requires 14 days to have passed since being made withdrawable
// Can be called by:
// - Owner; at any time once minipool is withdrawable (withdrawable state set by oDAO)
// - Owner; when minipool state has been requestedWithdrawable for more than 2 days (no reliance on oDAO)
// - Non-owner; when minipool state is staking and balance is > 16 ETH (no reliance on owner or oDAO)
// - Non-owner; when minipool state has been requestedWithdrawable for at least 14 days and balance is > 16 ETH (no reliance on owner or oDAO)
// - Non-owner; when minipool state has been withdrawable for 14 days and balance is between > 4 ETH (no reliance on owner)
// The first two can be used for arbitrage by an owner (along with a bundled rETH burn)
//
// There is currently no solution for the following scenarios:
// - Balance below 4 ETH
// - Balance between 4 and 16 ETH _and_ oDAO does not update state to Withdrawable
function distributeBalance() override external onlyInitialised {
// Must be called while staking or withdrawable
require(status == MinipoolStatus.Staking || status == MinipoolStatus.Withdrawable, "Minipool must be staking or withdrawable");
// Get withdrawal amount, we must also account for a possible node refund balance on the contract from users staking 32 ETH that have received a 16 ETH refund after the protocol bought out 16 ETH
uint256 totalBalance = address(this).balance.sub(nodeRefundBalance);
// Get node withdrawal address
address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress);
// If it's not the owner calling
if (msg.sender != nodeAddress && msg.sender != nodeWithdrawalAddress) {
// And the pool is in staking status
if (status == MinipoolStatus.Staking) {
if (msg.sender == nodeAddress || msg.sender == nodeWithdrawalAddress) {
if (status == MinipoolStatus.Withdrawable) {
// Owner and state is withdrawable - no further requirements
} else if (status == MinipoolStatus.RequestedWithdrawable) {
require(block.timestamp > statusTime.add(2 days), "After requesting withdrawable, owner must wait either 2 days OR for the state to get updated by oDAO");
} else {
require(false, "Minipool state must be Withdrawable or RequestedWithdrawable");
}
} else {
if (status == MinipoolStatus.Staking || status == MinipoolStatus.RequestedWithdrawable) {
// Then balance must be greater than 16 ETH
require(totalBalance >= 16 ether, "Balance must be greater than 16 ETH");
if (status == MinipoolStatus.RequestedWithdrawable) {
require(block.timestamp > statusTime.add(14 days), "Only owner can distribute balance for 2 weeks after requestWithdrawable is called");
}
} else {
require(status == MinipoolStatus.Withdrawable)
// Then enough time must have elapsed
require(block.timestamp > statusTime.add(14 days), "Non-owner must wait 14 days after withdrawal to distribute balance");
// And balance must be greater than 4 ETH
Expand Down
6 changes: 4 additions & 2 deletions contracts/contract/minipool/RocketMinipoolManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface {
}

// Get the number of minipools in each status.
// Returns the counts for Initialised, Prelaunch, Staking, Withdrawable, and Dissolved in that order.
// Returns the counts for Initialised, Prelaunch, Staking, RequestedWithdrwawable, Withdrawable, and Dissolved in that order.
function getMinipoolCountPerStatus(uint256 offset, uint256 limit) override external view
returns (uint256 initialisedCount, uint256 prelaunchCount, uint256 stakingCount, uint256 withdrawableCount, uint256 dissolvedCount) {
returns (uint256 initialisedCount, uint256 prelaunchCount, uint256 stakingCount, uint256 requestedWithdrawableCount, uint256 withdrawableCount, uint256 dissolvedCount) {
// Get contracts
AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress("addressSetStorage"));
// Precompute minipool key
Expand All @@ -83,6 +83,8 @@ contract RocketMinipoolManager is RocketBase, RocketMinipoolManagerInterface {
}
else if (status == MinipoolStatus.Staking) {
stakingCount++;
} else if (status == MinipoolStatus.RequestedWithdrawable) {
requestedWithdrawableCount++;
}
else if (status == MinipoolStatus.Withdrawable) {
withdrawableCount++;
Expand Down
6 changes: 4 additions & 2 deletions contracts/contract/minipool/RocketMinipoolStatus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ contract RocketMinipoolStatus is RocketBase, RocketMinipoolStatusInterface {
require(rocketDAOProtocolSettingsMinipool.getSubmitWithdrawableEnabled(), "Submitting withdrawable status is currently disabled");
// Check minipool status
RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipoolAddress);
require(minipool.getStatus() == MinipoolStatus.Staking, "Minipool can only be set as withdrawable while staking");
MinipoolStatus status = minipool.getStatus()
require(status == MinipoolStatus.Staking || status == MinipoolStatus.RequestedWithdrawable, "Minipool can only be set as withdrawable while staking or requested withdrawable");
// Get submission keys
bytes32 nodeSubmissionKey = keccak256(abi.encodePacked("minipool.withdrawable.submitted.node", msg.sender, _minipoolAddress));
bytes32 submissionCountKey = keccak256(abi.encodePacked("minipool.withdrawable.submitted.count", _minipoolAddress));
Expand Down Expand Up @@ -71,7 +72,8 @@ contract RocketMinipoolStatus is RocketBase, RocketMinipoolStatusInterface {
require(rocketDAOProtocolSettingsMinipool.getSubmitWithdrawableEnabled(), "Submitting withdrawable status is currently disabled");
// Check minipool status
RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipoolAddress);
require(minipool.getStatus() == MinipoolStatus.Staking, "Minipool can only be set as withdrawable while staking");
MinipoolStatus status = minipool.getStatus()
require(status == MinipoolStatus.Staking || status == MinipoolStatus.RequestedWithdrawable, "Minipool can only be set as withdrawable while staking or requested withdrawable");
// Get submission keys
bytes32 submissionCountKey = keccak256(abi.encodePacked("minipool.withdrawable.submitted.count", _minipoolAddress));
// Get submission count
Expand Down
11 changes: 6 additions & 5 deletions contracts/types/MinipoolStatus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ pragma solidity 0.7.6;
// Represents a minipool's status within the network

enum MinipoolStatus {
Initialised, // The minipool has been initialised and is awaiting a deposit of user ETH
Prelaunch, // The minipool has enough ETH to begin staking and is awaiting launch by the node operator
Staking, // The minipool is currently staking
Withdrawable, // The minipool has become withdrawable on the beacon chain and can be withdrawn from by the node operator
Dissolved // The minipool has been dissolved and its user deposited ETH has been returned to the deposit pool
Initialised, // The minipool has been initialised and is awaiting a deposit of user ETH
Prelaunch, // The minipool has enough ETH to begin staking and is awaiting launch by the node operator
Staking, // The minipool is currently staking
RequestedWithdrawable, // The node operator has requested withdrawable state; functionally equivalent to staking, except when arbing
Withdrawable, // The minipool has become withdrawable on the beacon chain and can be withdrawn from by the node operator
Dissolved // The minipool has been dissolved and its user deposited ETH has been returned to the deposit pool
}