MinipoolManager

Created Diff never expires
119 removals
531 lines
123 additions
538 lines
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.17;
pragma solidity 0.8.17;


import {Base} from "./Base.sol";
import {Base} from "./Base.sol";
import {IWithdrawer} from "../interface/IWithdrawer.sol";
import {IWithdrawer} from "../interface/IWithdrawer.sol";
import {MinipoolStatus} from "../types/MinipoolStatus.sol";
import {MinipoolStatus} from "../types/MinipoolStatus.sol";
import {MultisigManager} from "./MultisigManager.sol";
import {MultisigManager} from "./MultisigManager.sol";
import {Oracle} from "./Oracle.sol";
import {Oracle} from "./Oracle.sol";
import {ProtocolDAO} from "./ProtocolDAO.sol";
import {ProtocolDAO} from "./ProtocolDAO.sol";
import {Staking} from "./Staking.sol";
import {Staking} from "./Staking.sol";
import {Storage} from "./Storage.sol";
import {Storage} from "./Storage.sol";
import {TokenggAVAX} from "./tokens/TokenggAVAX.sol";
import {TokenggAVAX} from "./tokens/TokenggAVAX.sol";
import {Vault} from "./Vault.sol";
import {Vault} from "./Vault.sol";
import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol";
import {FixedPointMathLib} from "@rari-capital/solmate/src/utils/FixedPointMathLib.sol";
import {ReentrancyGuard} from "@rari-capital/solmate/src/utils/ReentrancyGuard.sol";
import {ReentrancyGuard} from "@rari-capital/solmate/src/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol";
import {SafeTransferLib} from "@rari-capital/solmate/src/utils/SafeTransferLib.sol";


/*
/*
Data Storage Schema
Data Storage Schema
NodeIDs are 20 bytes so can use Solidity 'address' as storage type for them
NodeIDs are 20 bytes so can use Solidity 'address' as storage type for them
NodeIDs can be added, but never removed. If a nodeID submits another validation request,
NodeIDs can be added, but never removed. If a nodeID submits another validation request,
it will overwrite the old one (only allowed for specific statuses).
it will overwrite the old one (only allowed for specific statuses).


MinipoolManager.TotalAVAXLiquidStakerAmt = total for all active minipools (Prelaunch/Launched/Staking)
MinipoolManager.TotalAVAXLiquidStakerAmt = total for all active minipools (Prelaunch/Launched/Staking)


minipool.count = Starts at 0 and counts up by 1 after a node is added.
minipool.count = Starts at 0 and counts up by 1 after a node is added.


minipool.index<nodeID> = <index> of nodeID
minipool.index<nodeID> = <index> of nodeID
minipool.item<index>.nodeID = nodeID used as primary key (NOT the ascii "Node-123..." but the actual 20 bytes)
minipool.item<index>.nodeID = nodeID used as primary key (NOT the ascii "Node-123..." but the actual 20 bytes)
minipool.item<index>.status = enum
minipool.item<index>.status = enum
minipool.item<index>.duration = requested validation duration in seconds (performed as 14 day cycles)
minipool.item<index>.duration = requested validation duration in seconds (performed as 14 day cycles)
minipool.item<index>.delegationFee = node operator specified fee (must be between 0 and 1 ether) 2% is 0.2 ether
minipool.item<index>.delegationFee = node operator specified fee (must be between 0 and 1 ether) 2% is 0.2 ether
minipool.item<index>.owner = owner address
minipool.item<index>.owner = owner address
minipool.item<index>.multisigAddr = which Rialto multisig is assigned to manage this validation
minipool.item<index>.multisigAddr = which Rialto multisig is assigned to manage this validation
minipool.item<index>.avaxNodeOpAmt = avax deposited by node operator (for this cycle)
minipool.item<index>.avaxNodeOpAmt = avax deposited by node operator (for this cycle)
minipool.item<index>.avaxNodeOpInitialAmt = avax deposited by node operator for the **first** validation cycle
minipool.item<index>.avaxNodeOpInitialAmt = avax deposited by node operator for the **first** validation cycle
minipool.item<index>.avaxLiquidStakerAmt = avax deposited by users and assigned to this nodeID
minipool.item<index>.avaxLiquidStakerAmt = avax deposited by users and assigned to this nodeID
minipool.item<index>.creationTime = actual time the minipool was created
minipool.item<index>.creationTime = actual time the minipool was created
minipool.item<index>.blsPubkeyAndSig = pub key [48 bytes] + sig [96 bytes]
minipool.item<index>.hardwareProvider = enum representing the hardware provider


// Submitted by the Rialto oracle
// Submitted by the Rialto oracle
minipool.item<index>.txID = transaction id of the AddValidatorTx
minipool.item<index>.txID = transaction id of the AddValidatorTx
minipool.item<index>.initialStartTime = actual time the **first** validation cycle was started
minipool.item<index>.initialStartTime = actual time the **first** validation cycle was started
minipool.item<index>.startTime = actual time validation was started
minipool.item<index>.startTime = actual time validation was started
minipool.item<index>.endTime = actual time validation was finished
minipool.item<index>.endTime = actual time validation was finished
minipool.item<index>.avaxTotalRewardAmt = Actual total avax rewards paid by avalanchego to the TSS P-chain addr
minipool.item<index>.avaxTotalRewardAmt = Actual total avax rewards paid by avalanchego to the TSS P-chain addr
minipool.item<index>.errorCode = bytes32 that encodes an error msg if something went wrong during launch of minipool
minipool.item<index>.errorCode = bytes32 that encodes an error msg if something went wrong during launch of minipool


// Calculated in recordStakingEnd()
// Calculated in recordStakingEnd()
minipool.item<index>.avaxNodeOpRewardAmt
minipool.item<index>.avaxNodeOpRewardAmt
minipool.item<index>.avaxLiquidStakerRewardAmt
minipool.item<index>.avaxLiquidStakerRewardAmt
minipool.item<index>.ggpSlashAmt = amt of ggp bond that was slashed if necessary (expected reward amt = avaxLiquidStakerAmt * x%/yr / ggpPriceInAvax)
minipool.item<index>.ggpSlashAmt = amt of ggp bond that was slashed if necessary (expected reward amt = avaxLiquidStakerAmt * x%/yr / ggpPriceInAvax)
*/
*/


/// @title Minipool creation and management
/// @title Minipool creation and management
contract MinipoolManager is Base, ReentrancyGuard, IWithdrawer {
contract MinipoolManager is Base, ReentrancyGuard, IWithdrawer {
using FixedPointMathLib for uint256;
using FixedPointMathLib for uint256;
using SafeTransferLib for address;
using SafeTransferLib for address;


error CancellationTooEarly();
error CancellationTooEarly();
error DurationOutOfBounds();
error DurationOutOfBounds();
error DelegationFeeOutOfBounds();
error DelegationFeeOutOfBounds();
error InsufficientGGPCollateralization();
error InsufficientGGPCollateralization();
error InsufficientAVAXForMinipoolCreation();
error InsufficientAVAXForMinipoolCreation();
error InvalidAmount();
error InvalidAmount();
error InvalidAVAXAssignmentRequest();
error InvalidAVAXAssignmentRequest();
error InvalidStartTime();
error InvalidStartTime();
error InvalidEndTime();
error InvalidEndTime();
error InvalidMultisigAddress();
error InvalidMultisigAddress();
error InvalidNodeID();
error InvalidNodeID();
error InvalidStateTransition();
error InvalidStateTransition();
error MinipoolNotFound();
error MinipoolNotFound();
error MinipoolDurationExceeded();
error MinipoolDurationExceeded();
error NegativeCycleDuration();
error NegativeCycleDuration();
error OnlyOwner();
error OnlyOwner();
error OnlyRole();
error WithdrawAmountTooLarge();
error WithdrawAmountTooLarge();
error WithdrawForDelegationDisabled();


event GGPSlashed(address indexed nodeID, uint256 ggp);
event GGPSlashed(address indexed nodeID, uint256 ggp);
event MinipoolStatusChanged(address indexed nodeID, MinipoolStatus indexed status);
event MinipoolStatusChanged(address indexed nodeID, MinipoolStatus indexed status);
event WithdrawForDelegation(address indexed nodeID, uint256 amount);
event DepositFromDelegation(address indexed nodeID, uint256 amount, uint256 rewardsAmount);
event BLSKeysAdded(address indexed nodeID, bytes blsPubkeyAndSig);
event MinipoolLaunched(address indexed nodeID, bytes32 hardwareProvider, uint256 duration);


/// @dev Not used for storage, just for returning data from view functions
/// @dev Not used for storage, just for returning data from view functions
struct Minipool {
struct Minipool {
int256 index;
int256 index;
address nodeID;
address nodeID;
uint256 status;
uint256 status;
uint256 duration;
uint256 duration;
uint256 delegationFee;
uint256 delegationFee;
address owner;
address owner;
address multisigAddr;
address multisigAddr;
uint256 avaxNodeOpAmt;
uint256 avaxNodeOpAmt;
uint256 avaxNodeOpInitialAmt;
uint256 avaxNodeOpInitialAmt;
uint256 avaxLiquidStakerAmt;
uint256 avaxLiquidStakerAmt;
bytes blsPubkeyAndSig;
// Submitted by the Rialto Oracle
// Submitted by the Rialto Oracle
bytes32 txID;
bytes32 txID;
uint256 creationTime;
uint256 creationTime;
uint256 initialStartTime;
uint256 initialStartTime;
uint256 startTime;
uint256 startTime;
uint256 endTime;
uint256 endTime;
uint256 avaxTotalRewardAmt;
uint256 avaxTotalRewardAmt;
bytes32 errorCode;
bytes32 errorCode;
// Calculated in recordStakingEnd
// Calculated in recordStakingEnd
uint256 ggpSlashAmt;
uint256 ggpSlashAmt;
uint256 avaxNodeOpRewardAmt;
uint256 avaxNodeOpRewardAmt;
uint256 avaxLiquidStakerRewardAmt;
uint256 avaxLiquidStakerRewardAmt;
bytes32 hardwareProvider;
}
}


uint256 public minStakingDuration;
uint256 public minStakingDuration;


constructor(Storage storageAddress) Base(storageAddress) {
constructor(Storage storageAddress) Base(storageAddress) {
version = 1;
version = 1;
}
}


/// @notice Payable function to receive funds from the vault
function receiveWithdrawalAVAX() external payable {}
function receiveWithdrawalAVAX() external payable {}


//
//
// GUARDS
// GUARDS
//
//


/// @notice Look up minipool owner by minipool index
/// @notice Look up minipool owner by minipool index
/// @param minipoolIndex A valid minipool index
/// @param minipoolIndex A valid minipool index
/// @return minipool owner or revert
/// @return minipool owner or revert
function onlyOwner(int256 minipoolIndex) private view returns (address) {
function onlyOwner(int256 minipoolIndex) private view returns (address) {
address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")));
address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")));
if (msg.sender != owner) {
if (msg.sender != owner) {
revert OnlyOwner();
revert OnlyOwner();
}
}
return owner;
return owner;
}
}


/// @notice Verifies the multisig trying to use the given node ID is valid
/// @notice Verifies the multisig trying to use the given node ID is valid
/// @dev Look up multisig index by minipool nodeID
/// @dev Look up multisig index by minipool nodeID
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @return minipool index or revert
/// @return minipool index or revert
function onlyValidMultisig(address nodeID) private view returns (int256) {
function onlyValidMultisig(address nodeID) private view returns (int256) {
int256 minipoolIndex = requireValidMinipool(nodeID);
int256 minipoolIndex = requireValidMinipool(nodeID);


address assignedMultisig = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".multisigAddr")));
address assignedMultisig = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".multisigAddr")));
if (msg.sender != assignedMultisig) {
if (msg.sender != assignedMultisig) {
revert InvalidMultisigAddress();
revert InvalidMultisigAddress();
}
}
return minipoolIndex;
return minipoolIndex;
}
}


/// @notice Look up minipool index by minipool nodeID
/// @notice Look up minipool index by minipool nodeID
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @return minipool index or revert
/// @return minipool index or revert
function requireValidMinipool(address nodeID) private view returns (int256) {
function requireValidMinipool(address nodeID) public view returns (int256) {
int256 minipoolIndex = getIndexOf(nodeID);
int256 minipoolIndex = getIndexOf(nodeID);
if (minipoolIndex == -1) {
if (minipoolIndex == -1) {
revert MinipoolNotFound();
revert MinipoolNotFound();
}
}


return minipoolIndex;
return minipoolIndex;
}
}


/// @notice Ensure a minipool is allowed to move to the "to" state
/// @notice Ensure a minipool is allowed to move to the "to" state
/// @param minipoolIndex A valid minipool index
/// @param minipoolIndex A valid minipool index
/// @param to New status
/// @param to New status
function requireValidStateTransition(int256 minipoolIndex, MinipoolStatus to) private view {
function requireValidStateTransition(int256 minipoolIndex, MinipoolStatus to) private view {
bytes32 statusKey = keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status"));
bytes32 statusKey = keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status"));
MinipoolStatus currentStatus = MinipoolStatus(getUint(statusKey));
MinipoolStatus currentStatus = MinipoolStatus(getUint(statusKey));
bool isValid;
bool isValid;


if (currentStatus == MinipoolStatus.Prelaunch) {
if (currentStatus == MinipoolStatus.Prelaunch) {
isValid = (to == MinipoolStatus.Launched || to == MinipoolStatus.Canceled);
isValid = (to == MinipoolStatus.Launched || to == MinipoolStatus.Canceled);
} else if (currentStatus == MinipoolStatus.Launched) {
} else if (currentStatus == MinipoolStatus.Launched) {
isValid = (to == MinipoolStatus.Staking || to == MinipoolStatus.Error);
isValid = (to == MinipoolStatus.Staking || to == MinipoolStatus.Error);
} else if (currentStatus == MinipoolStatus.Staking) {
} else if (currentStatus == MinipoolStatus.Staking) {
isValid = (to == MinipoolStatus.Withdrawable);
isValid = (to == MinipoolStatus.Withdrawable);
} else if (currentStatus == MinipoolStatus.Withdrawable || currentStatus == MinipoolStatus.Error) {
} else if (currentStatus == MinipoolStatus.Withdrawable || currentStatus == MinipoolStatus.Error) {
isValid = (to == MinipoolStatus.Finished);
isValid = (to == MinipoolStatus.Finished);
} else if (currentStatus == MinipoolStatus.Finished || currentStatus == MinipoolStatus.Canceled) {
} else if (currentStatus == MinipoolStatus.Finished || currentStatus == MinipoolStatus.Canceled) {
// Once a node is finished/canceled, if they re-validate they go back to beginning state
// Once a node is finished/canceled, if they re-validate they go back to beginning state
isValid = (to == MinipoolStatus.Prelaunch);
isValid = (to == MinipoolStatus.Prelaunch);
} else {
} else {
isValid = false;
isValid = false;
}
}


if (!isValid) {
if (!isValid) {
revert InvalidStateTransition();
revert InvalidStateTransition();
}
}
}
}


//
//
// OWNER FUNCTIONS
// OWNER FUNCTIONS
//
//


/// @notice Accept AVAX deposit from node operator to create a Minipool. Node Operator must be staking GGP. Open to public.
/// @notice Accept AVAX deposit from node operator to create a Minipool. Node Operator must be staking GGP. Open to public.
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @param duration Requested validation period in seconds
/// @param duration Requested validation period in seconds
/// @param delegationFee Percentage delegation fee in units of ether (2% is 20_000)
/// @param delegationFee Percentage delegation fee in units of ether (2% is 20_000)
/// @param avaxAssignmentRequest Amount of requested AVAX to be matched for this Minipool
/// @param avaxAssignmentRequest Amount of requested AVAX to be matched for this Minipool
function createMinipool(address nodeID, uint256 duration, uint256 delegationFee, uint256 avaxAssignmentRequest) external payable whenNotPaused {
function createMinipool(
address nodeID,
uint256 duration,
uint256 delegationFee,
uint256 avaxAssignmentRequest,
bytes calldata blsPubkeyAndSig,
bytes32 hardwareProvider
) public payable whenNotPaused {
this.createMinipoolOnBehalfOf{value: msg.value}(
msg.sender,
nodeID,
duration,
delegationFee,
avaxAssignmentRequest,
blsPubkeyAndSig,
hardwareProvider
);
}

/// @notice Accept AVAX deposit from node operator to create a Minipool. Node Operator must be staking GGP. Open to public.
/// @param owner C-chain address representing the minipool owner
/// @param nodeID 20-byte Avalanche node ID
/// @param duration Requested validation period in seconds
/// @param delegationFee Percentage delegation fee in units of ether (2% is 20_000)
/// @param avaxAssignmentRequest Amount of requested AVAX to be matched for this Minipool
function createMinipoolOnBehalfOf(
address owner,
address nodeID,
uint256 duration,
uint256 delegationFee,
uint256 avaxAssignmentRequest,
bytes calldata blsPubkeyAndSig,
bytes32 hardwareProvider
) external payable whenNotPaused {
if (nodeID == address(0)) {
if (nodeID == address(0)) {
revert InvalidNodeID();
revert InvalidNodeID();
}
}


ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
if (
if (
// Current rule is matched funds must be 1:1 nodeOp:LiqStaker
// Current rule is matched funds must be 1:1 nodeOp:LiqStaker
msg.value != avaxAssignmentRequest ||
msg.value != avaxAssignmentRequest ||
avaxAssignmentRequest > dao.getMinipoolMaxAVAXAssignment() ||
avaxAssignmentRequest > dao.getMinipoolMaxAVAXAssignment() ||
avaxAssignmentRequest < dao.getMinipoolMinAVAXAssignment()
avaxAssignmentRequest < dao.getMinipoolMinAVAXAssignment()
) {
) {
revert InvalidAVAXAssignmentRequest();
revert InvalidAVAXAssignmentRequest();
}
}


if (msg.value + avaxAssignmentRequest < dao.getMinipoolMinAVAXStakingAmt()) {
if (msg.value + avaxAssignmentRequest < dao.getMinipoolMinAVAXStakingAmt()) {
revert InsufficientAVAXForMinipoolCreation();
revert InsufficientAVAXForMinipoolCreation();
}
}


if (duration < dao.getMinipoolMinDuration() || duration > dao.getMinipoolMaxDuration()) {
if (duration < dao.getMinipoolMinDuration() || duration > dao.getMinipoolMaxDuration()) {
revert DurationOutOfBounds();
revert DurationOutOfBounds();
}
}


if (delegationFee < 20_000 || delegationFee > 1_000_000) {
if (delegationFee < 20_000 || delegationFee > 1_000_000) {
revert DelegationFeeOutOfBounds();
revert DelegationFeeOutOfBounds();
}
}


Staking staking = Staking(getContractAddress("Staking"));
Staking staking = Staking(getContractAddress("Staking"));
staking.increaseAVAXStake(msg.sender, msg.value);
staking.increaseAVAXStake(owner, msg.value);
staking.increaseAVAXAssigned(msg.sender, avaxAssignmentRequest);
staking.increaseAVAXAssigned(owner, avaxAssignmentRequest);


if (staking.getRewardsStartTime(msg.sender) == 0) {
if (staking.getRewardsStartTime(owner) == 0) {
staking.setRewardsStartTime(msg.sender, block.timestamp);
staking.setRewardsStartTime(owner, block.timestamp);
}
}


uint256 ratio = staking.getCollateralizationRatio(msg.sender);
if (staking.getCollateralizationRatio(owner) < dao.getMinCollateralizationRatio()) {
if (ratio < dao.getMinCollateralizationRatio()) {
revert InsufficientGGPCollateralization();
revert InsufficientGGPCollateralization();
}
}


// Get a Rialto multisig to assign for this minipool
// Get a Rialto multisig to assign for this minipool
MultisigManager multisigManager = MultisigManager(getContractAddress("MultisigManager"));
address multisig = MultisigManager(getContractAddress("MultisigManager")).requireNextActiveMultisig();
address multisig = multisigManager.requireNextActiveMultisig();


// Create or update a minipool record for nodeID
// Create or update a minipool record for nodeID
// If nodeID exists, only allow overwriting if node is finished or canceled
// If nodeID exists, only allow overwriting if node is finished or canceled
// (completed its validation period and all rewards paid and processing is complete)
// (completed its validation period and all rewards paid and processing is complete)
int256 minipoolIndex = getIndexOf(nodeID);
int256 minipoolIndex = getIndexOf(nodeID);
if (minipoolIndex != -1) {
if (minipoolIndex != -1) {
requireValidStateTransition(minipoolIndex, MinipoolStatus.Prelaunch);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Prelaunch);
resetMinipoolData(minipoolIndex);
resetMinipoolData(minipoolIndex);
// Also reset initialStartTime as we are starting a whole new validation
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")), 0);
} else {
} else {
minipoolIndex = int256(getUint(keccak256("minipool.count")));
minipoolIndex = int256(getUint(keccak256("minipool.count")));
// The minipoolIndex is stored 1 greater than actual value. The 1 is subtracted in getIndexOf()
// The minipoolIndex is stored 1 greater than actual value. The 1 is subtracted in getIndexOf()
setUint(keccak256(abi.encodePacked("minipool.index", nodeID)), uint256(minipoolIndex + 1));
setUint(keccak256(abi.encodePacked("minipool.index", nodeID)), uint256(minipoolIndex + 1));
setAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".nodeID")), nodeID);
setAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".nodeID")), nodeID);
addUint(keccak256("minipool.count"), 1);
addUint(keccak256("minipool.count"), 1);
}
}


// Save the attrs individually in the k/v store
// Save attributes to the k/v store
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Prelaunch));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Prelaunch));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".duration")), duration);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".duration")), duration);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".delegationFee")), delegationFee);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".delegationFee")), delegationFee);
setAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")), msg.sender);
setAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")), owner);
setAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".multisigAddr")), multisig);
setAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".multisigAddr")), multisig);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpInitialAmt")), msg.value);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpInitialAmt")), msg.value);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")), msg.value);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")), msg.value);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")), avaxAssignmentRequest);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")), avaxAssignmentRequest);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".creationTime")), block.timestamp);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".creationTime")), block.timestamp);
setBytes(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".blsPubkeyAndSig")), blsPubkeyAndSig);
setBytes32(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".hardwareProvider")), hardwareProvider);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")), 0);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".ggpSlashAmt")), 0);


emit MinipoolStatusChanged(nodeID, MinipoolStatus.Prelaunch);
emit MinipoolStatusChanged(nodeID, MinipoolStatus.Prelaunch);


Vault vault = Vault(getContractAddress("Vault"));
Vault vault = Vault(getContractAddress("Vault"));
vault.depositAVAX{value: msg.value}();
vault.depositAVAX{value: msg.value}();
}
}


/// @notice Owner of a minipool can cancel the (prelaunch) minipool
/// @notice Owner of a minipool can cancel the (prelaunch) minipool
/// @param nodeID 20-byte Avalanche node ID the Owner registered with
/// @param nodeID 20-byte Avalanche node ID the Owner registered with
function cancelMinipool(address nodeID) external nonReentrant {
function cancelMinipool(address nodeID) external nonReentrant {
ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
int256 index = requireValidMinipool(nodeID);
int256 index = requireValidMinipool(nodeID);
onlyOwner(index);
onlyOwner(index);
// make sure the minipool meets the wait period requirement
// Make sure the minipool meets the wait period requirement
uint256 creationTime = getUint(keccak256(abi.encodePacked("minipool.item", index, ".creationTime")));
uint256 creationTime = getUint(keccak256(abi.encodePacked("minipool.item", index, ".creationTime")));
if (block.timestamp - creationTime < dao.getMinipoolCancelMoratoriumSeconds()) {
if (block.timestamp - creationTime < dao.getMinipoolCancelMoratoriumSeconds()) {
revert CancellationTooEarly();
revert CancellationTooEarly();
}
}
_cancelMinipoolAndReturnFunds(nodeID, index);
_cancelMinipoolAndReturnFunds(nodeID, index);
}
}


function setBLSKeys(address nodeID, bytes calldata blsPubkeyAndSig) public {
int256 minipoolIndex = requireValidMinipool(nodeID);

ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
if (!dao.hasRole("Relauncher", msg.sender)) {
revert OnlyRole();
}
setBytes(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".blsPubkeyAndSig")), blsPubkeyAndSig);
emit BLSKeysAdded(nodeID, blsPubkeyAndSig);
}

/// @notice Withdraw function for a Node Operator to claim all AVAX funds they are due (original AVAX staked, plus any AVAX rewards)
/// @notice Withdraw function for a Node Operator to claim all AVAX funds they are due (original AVAX staked, plus any AVAX rewards)
/// @param nodeID 20-byte Avalanche node ID the Node Operator registered with
/// @param nodeID 20-byte Avalanche node ID the Node Operator registered with
function withdrawMinipoolFunds(address nodeID) external nonReentrant {
function withdrawMinipoolFunds(address nodeID) external {
int256 minipoolIndex = requireValidMinipool(nodeID);
int256 minipoolIndex = requireValidMinipool(nodeID);
address owner = onlyOwner(minipoolIndex);
address owner = onlyOwner(minipoolIndex);
uint256 totalAvaxAmt = _withdrawMinipoolFunds(nodeID, minipoolIndex, owner);
owner.safeTransferETH(totalAvaxAmt);
}

/// @notice Withdraw function for a Node Operator to claim all AVAX funds they are due (original AVAX staked, plus any AVAX rewards)
/// @dev Check that the owner is sending the transaction before calling this method
/// Check that the minipool index, owner, and nodeID all align
/// @param nodeID 20-byte Avalanche node ID the Node Operator registered with
/// @param minipoolIndex index representing the minipool
/// @param owner the c-chain address that owns the minipool
/// @return totalAvaxAmt The total amount of AVAX related to this minipool owed to the owner
function _withdrawMinipoolFunds(address nodeID, int256 minipoolIndex, address owner) internal nonReentrant returns (uint256 totalAvaxAmt) {
requireValidStateTransition(minipoolIndex, MinipoolStatus.Finished);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Finished);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Finished));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Finished));


uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")));
uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")));
uint256 avaxNodeOpRewardAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt")));
uint256 avaxNodeOpRewardAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt")));
uint256 totalAvaxAmt = avaxNodeOpAmt + avaxNodeOpRewardAmt;
totalAvaxAmt = avaxNodeOpAmt + avaxNodeOpRewardAmt;


Staking staking = Staking(getContractAddress("Staking"));
Staking staking = Staking(getContractAddress("Staking"));
staking.decreaseAVAXStake(owner, avaxNodeOpAmt);
staking.decreaseAVAXStake(owner, avaxNodeOpAmt);


Vault vault = Vault(getContractAddress("Vault"));
Vault vault = Vault(getContractAddress("Vault"));
vault.withdrawAVAX(totalAvaxAmt);
vault.withdrawAVAX(totalAvaxAmt);
owner.safeTransferETH(totalAvaxAmt);
emit MinipoolStatusChanged(nodeID, MinipoolStatus.Finished);
return totalAvaxAmt;
}

/// @notice Rewards are are transfered to the minipool owner and the minipool is re-launched with the original principle
/// @param nodeID 20-byte Avalanche node ID the Node Operator registered with
/// @param duration Requested validation period in seconds
function withdrawRewardsAndRelaunchMinipool(address nodeID, uint256 duration, bytes32 hardwareProvider) public whenNotPaused {
if (!ProtocolDAO(getContractAddress("ProtocolDAO")).hasRole("Relauncher", msg.sender)) {
revert OnlyRole();
}

int256 minipoolIndex = requireValidMinipool(nodeID);
address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")));
uint256 totalAvaxAmt = _withdrawMinipoolFunds(nodeID, minipoolIndex, owner);
uint256 minipoolBase = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpInitialAmt")));

this.createMinipoolOnBehalfOf{value: minipoolBase}(
owner,
nodeID,
duration,
20_000,
minipoolBase,
getBytes(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".blsPubkeyAndSig"))),
hardwareProvider
);
owner.safeTransferETH(totalAvaxAmt - minipoolBase);
}
}


//
//
// RIALTO FUNCTIONS
// RIALTO FUNCTIONS
//
//


/// @notice Verifies that the minipool related the the given node ID is able to a validator
/// @notice Verifies that the minipool related the the given node ID is able to a validator
/// @dev Rialto calls this to see if a claim would succeed. Does not change state.
/// @dev Rialto calls this to see if a claim would succeed. Does not change state.
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @return boolean representing if the minipool can become a validator
/// @return boolean representing if the minipool can become a validator
function canClaimAndInitiateStaking(address nodeID) external view returns (bool) {
function canClaimAndInitiateStaking(address nodeID) external view returns (bool) {
int256 minipoolIndex = onlyValidMultisig(nodeID);
int256 minipoolIndex = onlyValidMultisig(nodeID);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Launched);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Launched);


TokenggAVAX ggAVAX = TokenggAVAX(payable(getContractAddress("TokenggAVAX")));
TokenggAVAX ggAVAX = TokenggAVAX(payable(getContractAddress("TokenggAVAX")));
uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")));
uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")));
return avaxLiquidStakerAmt <= ggAVAX.amountAvailableForStaking();
return avaxLiquidStakerAmt <= ggAVAX.amountAvailableForStaking();
}
}


/// @notice Withdraws minipool's AVAX for staking on Avalanche
/// @notice Withdraws minipool's AVAX for staking on Avalanche
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @dev Rialto calls this to claim a minipool for staking and validation on the P-chain.
/// @dev Rialto calls this to claim a minipool for staking and validation on the P-chain.
function claimAndInitiateStaking(address nodeID) public {
function claimAndInitiateStaking(address nodeID) public {
_claimAndInitiateStaking(nodeID, false);
_claimAndInitiateStaking(nodeID, false);
}
}


/// @notice Withdraws minipool's AVAX for staking on Avalanche while that minipool is cycling
/// @notice Withdraws minipool's AVAX for staking on Avalanche while that minipool is cycling
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @dev Rialto calls this to claim a minipool for staking and validation on the P-chain.
/// @dev Rialto calls this to claim a minipool for staking and validation on the P-chain.
function claimAndInitiateStakingCycle(address nodeID) internal {
function claimAndInitiateStakingCycle(address nodeID) internal {
_claimAndInitiateStaking(nodeID, true);
_claimAndInitiateStaking(nodeID, true);
}
}


/// @notice Withdraw AVAX from the vault and ggAVAX to initiate staking and register the node as a validator
/// @notice Withdraw AVAX from the vault and ggAVAX to initiate staking and register the node as a validator
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @dev Rialto calls this to claim a minipool for staking and validation on the P-chain.
/// @dev Rialto calls this to claim a minipool for staking and validation on the P-chain.
function _claimAndInitiateStaking(address nodeID, bool isCycling) internal {
function _claimAndInitiateStaking(address nodeID, bool isCycling) internal {
int256 minipoolIndex = onlyValidMultisig(nodeID);
int256 minipoolIndex = onlyValidMultisig(nodeID);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Launched);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Launched);


uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")));
uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")));
uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")));
uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")));


// Transfer funds to this contract and then send to multisig
// Transfer funds to this contract and then send to multisig
TokenggAVAX ggAVAX = TokenggAVAX(payable(getContractAddress("TokenggAVAX")));
TokenggAVAX ggAVAX = TokenggAVAX(payable(getContractAddress("TokenggAVAX")));
if (!isCycling && (avaxLiquidStakerAmt > ggAVAX.amountAvailableForStaking())) {
if (!isCycling && (avaxLiquidStakerAmt > ggAVAX.amountAvailableForStaking())) {
revert WithdrawAmountTooLarge();
revert WithdrawAmountTooLarge();
}
}
ggAVAX.withdrawForStaking(avaxLiquidStakerAmt);
ggAVAX.withdrawForStaking(avaxLiquidStakerAmt);
addUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt);
addUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt);


setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Launched));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Launched));

emit MinipoolStatusChanged(nodeID, MinipoolStatus.Launched);
emit MinipoolStatusChanged(nodeID, MinipoolStatus.Launched);


// This should be called once per minipool start, not on every cycle
if (!isCycling) {
bytes32 hardwareProvider = getBytes32(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".hardwareProvider")));
uint256 duration = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".duration")));
emit MinipoolLaunched(nodeID, hardwareProvider, duration);
}

Vault vault = Vault(getContractAddress("Vault"));
Vault vault = Vault(getContractAddress("Vault"));
vault.withdrawAVAX(avaxNodeOpAmt);
vault.withdrawAVAX(avaxNodeOpAmt);


uint256 totalAvaxAmt = avaxNodeOpAmt + avaxLiquidStakerAmt;
uint256 totalAvaxAmt = avaxNodeOpAmt + avaxLiquidStakerAmt;
msg.sender.safeTransferETH(totalAvaxAmt);
msg.sender.safeTransferETH(totalAvaxAmt);
}
}


/// @notice Rialto calls this after successfully registering the minipool as a validator for Avalanche
/// @notice Rialto calls this after successfully registering the minipool as a validator for Avalanche
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @param txID The ID of the transaction that successfully registered the node with Avalanche to become a validator
/// @param txID The ID of the transaction that successfully registered the node with Avalanche to become a validator
/// @param startTime Time the node became a validator
/// @param startTime Time the node became a validator
function recordStakingStart(address nodeID, bytes32 txID, uint256 startTime) external {
function recordStakingStart(address nodeID, bytes32 txID, uint256 startTime) external {
int256 minipoolIndex = onlyValidMultisig(nodeID);
int256 minipoolIndex = onlyValidMultisig(nodeID);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Staking);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Staking);
if (startTime > block.timestamp) {
if (startTime > block.timestamp) {
revert InvalidStartTime();
revert InvalidStartTime();
}
}


setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Staking));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Staking));
setBytes32(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".txID")), txID);
setBytes32(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".txID")), txID);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".startTime")), startTime);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".startTime")), startTime);


// If this is the first of many cycles, set the initialStartTime
// If this is the first of many cycles, set the initialStartTime
uint256 initialStartTime = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")));
uint256 initialStartTime = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")));
if (initialStartTime == 0) {
if (initialStartTime == 0) {
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")), startTime);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")), startTime);
}
}


address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")));
address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")));


Staking staking = Staking(getContractAddress("Staking"));
Staking staking = Staking(getContractAddress("Staking"));
uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")));
uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")));


staking.increaseAVAXValidating(owner, avaxLiquidStakerAmt);
staking.increaseAVAXValidating(owner, avaxLiquidStakerAmt);


if (staking.getAVAXValidatingHighWater(owner) < staking.getAVAXValidating(owner)) {
if (staking.getAVAXValidatingHighWater(owner) < staking.getAVAXValidating(owner)) {
staking.setAVAXValidatingHighWater(owner, staking.getAVAXValidating(owner));
staking.setAVAXValidatingHighWater(owner, staking.getAVAXValidating(owner));
}
}


emit MinipoolStatusChanged(nodeID, MinipoolStatus.Staking);
emit MinipoolStatusChanged(nodeID, MinipoolStatus.Staking);
}
}


/// @notice Records the nodeID's validation period end
/// @notice Records the nodeID's validation period end
/// @param nodeID 20-byte Avalanche node ID
/// @param nodeID 20-byte Avalanche node ID
/// @param endTime The time the node ID stopped validating Avalanche
/// @param endTime The time the node ID stopped validating Avalanche
/// @param avaxTotalRewardAmt The rewards the node received from Avalanche for being a validator
/// @param avaxTotalRewardAmt The rewards the node received from Avalanche for being a validator
/// @dev Rialto will xfer back all staked avax + avax rewards. Also handles the slashing of node ops GGP bond.
/// @dev Rialto will xfer back all staked avax + avax rewards. Also handles the slashing of node ops GGP bond.
function recordStakingEnd(address nodeID, uint256 endTime, uint256 avaxTotalRewardAmt) public payable {
function recordStakingEnd(address nodeID, uint256 endTime, uint256 avaxTotalRewardAmt) public payable {
int256 minipoolIndex = onlyValidMultisig(nodeID);
int256 minipoolIndex = onlyValidMultisig(nodeID);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Withdrawable);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Withdrawable);


Minipool memory mp = getMinipool(minipoolIndex);
Minipool memory mp = getMinipool(minipoolIndex);
if (endTime <= mp.startTime || endTime > block.timestamp) {
if (endTime <= mp.startTime || endTime > block.timestamp) {
revert InvalidEndTime();
revert InvalidEndTime();
}
}


uint256 totalAvaxAmt = mp.avaxNodeOpAmt + mp.avaxLiquidStakerAmt;
uint256 totalAvaxAmt = mp.avaxNodeOpAmt + mp.avaxLiquidStakerAmt;
if (msg.value != totalAvaxAmt + avaxTotalRewardAmt) {
if (msg.value != totalAvaxAmt + avaxTotalRewardAmt) {
revert InvalidAmount();
revert InvalidAmount();
}
}


setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Withdrawable));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Withdrawable));
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".endTime")), endTime);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".endTime")), endTime);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxTotalRewardAmt")), avaxTotalRewardAmt);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxTotalRewardAmt")), avaxTotalRewardAmt);


// Calculate rewards splits (these will all be zero if no rewards were recvd)
// Calculate rewards splits (these will all be zero if no rewards were recvd)
// TODO Revisit this logic if we ever allow unequal matched funds
// NOTE: Commission fee amount fails to persist for Node Operators across cycling minipools.
// Currently, setting MinipoolNodeCommissionFeePct to 0 (as of 2/23/2024) avoids the issue,
// ensuring a 50/50 reward split. Revisit this logic if we want to reinstate a commission fee
uint256 avaxHalfRewards = avaxTotalRewardAmt / 2;
uint256 avaxHalfRewards = avaxTotalRewardAmt / 2;


// Node operators recv an additional commission fee
// Node operators recv an additional commission fee
ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
uint256 avaxLiquidStakerRewardAmt = avaxHalfRewards - avaxHalfRewards.mulWadDown(dao.getMinipoolNodeCommissionFeePct());
uint256 avaxLiquidStakerRewardAmt = avaxHalfRewards - avaxHalfRewards.mulWadDown(dao.getMinipoolNodeCommissionFeePct());
uint256 avaxNodeOpRewardAmt = avaxTotalRewardAmt - avaxLiquidStakerRewardAmt;
uint256 avaxNodeOpRewardAmt = avaxTotalRewardAmt - avaxLiquidStakerRewardAmt;


setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt")), avaxNodeOpRewardAmt);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt")), avaxNodeOpRewardAmt);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerRewardAmt")), avaxLiquidStakerRewardAmt);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerRewardAmt")), avaxLiquidStakerRewardAmt);


// No rewards means validation period failed, must slash node ops GGP.
// No rewards means validation period failed, must slash node ops GGP.
if (avaxTotalRewardAmt == 0) {
if (avaxTotalRewardAmt == 0) {
slash(minipoolIndex);
slas
}

// Send the nodeOps AVAX + rewards to vault so they can claim later
Vault vault = Vault(getContractAddress("Vault"));
vault.depositAVAX{value: mp.avaxNodeOpAmt + avaxNodeOpRewardAmt}();
// Return Liq stakers funds + rewards
TokenggAVAX ggAVAX = TokenggAVAX(payable(getContractAddress("TokenggAVAX")));
ggAVAX.depositFromStaking{value: mp.avaxLiquidStakerAmt + avaxLiquidStakerRewardAmt}(mp.avaxLiquidStakerAmt, avaxLiquidStakerRewardAmt);
subUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), mp.avaxLiquidStakerAmt);

Staking staking = Staking(getContractAddress("Staking"));
staking.decreaseAVAXAssigned(mp.owner, mp.avaxLiquidStakerAmt);
staking.decreaseAVAXValidating(mp.owner, mp.avaxLiquidStakerAmt);

emit MinipoolStatusChanged(nodeID, MinipoolStatus.Withdrawable);
}

/// @notice Records the nodeID's validation period end
/// @param nodeID 20-byte Avalanche node ID
/// @param endTime The time the node ID stopped validating Avalanche
/// @param avaxTotalRewardAmt The rewards the node received from Avalanche for being a validator
/// @dev Rialto will xfer back all staked avax + avax rewards. Also handles the slashing of node ops GGP bond.
/// @dev We call recordStakingEnd,recreateMinipool,claimAndInitiateStaking in one tx to prevent liq staker funds from being sniped
function recordStakingEndThenMaybeCycle(address nodeID, uint256 endTime, uint256 avaxTotalRewardAmt) external payable whenNotPaused {
int256 minipoolIndex = onlyValidMultisig(nodeID);

uint256 initialStartTime = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".initialStartTime")));
uint256 duration = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".duration")));

recordStakingEnd(nodeID, endTime, avaxTotalRewardAmt);
ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));

uint256 minipoolEnd = initialStartTime + duration;
uint256 minipoolEndWithTolerance = minipoolEnd + dao.getMinipoolCycleDelayTolerance();

uint256 nextCycleEnd = block.timestamp + dao.getMinipoolCycleDuration();

if (nextCycleEnd <= minipoolEndWithTolerance) {
recreateMinipool(nodeID);
claimAndInitiateStakingCycle(nodeID);
} else {
// if difference is less than a cycle, the minipool was meant to validate again
// set an errorCode the front-end can decode
if (nextCycleEnd - minipoolEnd < dao.getMinipoolCycleDuration()) {
bytes32 errorCode = "EC1";
setBytes32(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".errorCode")), errorCode);
}
}
}

/// @notice Re-stake a minipool, compounding all rewards recvd
/// @param nodeID 20-byte Avalanche node ID
function recreateMinipool(address nodeID) internal whenNotPaused {
int256 minipoolIndex = onlyValidMultisig(nodeID);
Minipool memory mp = getMinipool(minipoolIndex);
MinipoolStatus currentStatus = MinipoolStatus(mp.status);

if (currentStatus != MinipoolStatus.Withdrawable) {
revert InvalidStateTransition();
}

// Compound the avax plus rewards
// NOTE Assumes a 1:1 nodeOp:liqStaker funds ratio
uint256 compoundedAvaxAmt = mp.avaxNodeOpAmt + mp.avaxLiquidStakerRewardAmt;
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")), compoundedAvaxAmt);
setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")), compoundedAvaxAmt);

Staking staking = Staking(getContractAddress("Staking"));
// Only increase AVAX stake by rewards amount we are compounding
// since AVAX stake is only decreased by withdrawMinipool()
staking.increaseAVAXStake(mp.owner, mp.avaxLiquidStakerRewardAmt);
staking.increaseAVAXAssigned(mp.owner, compoundedAvaxAmt);

ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO"));
uint256 ratio = staking.getCollateralizationRatio(mp.owner);
if (ratio < dao.getMinCollateralizationRatio()) {
revert InsufficientGGPCollateralization();
}

resetMinipoolData(minipoolIndex);

setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Prelaunch));

emit MinipoolStatusChanged(nodeID, MinipoolStatus.Prelaunch);
}

/// @notice A staking error occurred while registering the node as a validator
/// @param nodeID 20-byte Avalanche node ID
/// @param errorCode The code that represents the reason for failure
/// @dev Rialto was unable to start the validation period, so cancel and refund all money
function recordStakingError(address nodeID, bytes32 errorCode) external payable {
int256 minipoolIndex = onlyValidMultisig(nodeID);
requireValidStateTransition(minipoolIndex, MinipoolStatus.Error);

address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner")));
uint256 avaxNodeOpAmt = getUint(keccak2