Skip to content
Closed
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
214 changes: 214 additions & 0 deletions contracts/contracts/ExampleStakingManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: MIT
// from https://solidity-by-example.org/defi/staking-rewards
pragma solidity ^0.8.24;

import "./interfaces/IWarpMessenger.sol";
import "./interfaces/IStakingManager.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract ExampleStakingManager is IStakingManager, Ownable {
IWarpMessenger public constant WARP_MESSENGER = IWarpMessenger(0x0200000000000000000000000000000000000005);
uint64 constant MAX_UINT64 = type(uint64).max;

IERC20 public immutable stakingToken;
IERC20 public immutable rewardsToken;

uint256 public minStakingDuration; // Minimum duration for staking in seconds.
uint256 public minStakingAmount; // Minimum amount for staking.
bool public stakingEnabled; // Whether or not contract has started staking operations
uint256 public rewardRate; // Reward rate per second.
// Total staked
uint256 public totalSupply;
// User address => staked amount
mapping(address => uint256) public balanceOf;
mapping(address => uint256) public unlockedBalanceOf;

// Validator Manager
// subnetID + nodeID (stakingID) => messageID (RegisterValidatorMessage hash)
mapping(bytes32 => bytes32) public activeValidators;
// messageID => Validator
mapping(bytes32 => Validator) public registeredValidatorMessages;

struct Validator {
bytes32 subnetID;
bytes32 nodeID;
uint64 weight;
uint256 startedAt;
uint256 redeemedAt;
uint64 nonce;
address rewardAddress;
}

constructor(address _stakingToken, address _rewardToken, uint256 _minStakingDuration, uint256 _minStakingAmount) {
stakingToken = IERC20(_stakingToken);
rewardsToken = IERC20(_rewardToken);
minStakingDuration = _minStakingDuration;
minStakingAmount = _minStakingAmount;
}

function registerValidator(
bytes32 subnetID,
bytes32 nodeID,
uint64 amount,
uint64 expiryTimestamp,
bytes memory signature
) external {
// Q: check if the subnetID is this subnet?
require(amount > minStakingAmount, "ExampleValidatorManager: amount must be greater than minStakingAmount");
require(stakingEnabled, "Staking period has not started");
// Q: does below check make sense?
require(expiryTimestamp > block.timestamp, "ExampleValidatorManager: expiry timestamp must be in the future");
require(nodeID != bytes32(0), "ExampleValidatorManager: nodeID must not be zero");
require(signature.length == 64, "ExampleValidatorManager: invalid signature length, must be 64");

bytes32 stakingIDHash = keccak256(abi.encode(subnetID, nodeID));
require(activeValidators[stakingIDHash] == bytes32(0), "ExampleValidatorManager: validator already exists");

RegisterValidatorMessage memory message = RegisterValidatorMessage({
subnetID: subnetID,
nodeID: nodeID,
weight: amount,
expiryTimestamp: expiryTimestamp,
signature: signature
});

bytes memory messageBytes = abi.encode(message);
bytes32 messageID = sha256(messageBytes);
// This requires the message ID on P-Chain to be same as this message ID.
require(registeredValidatorMessages[messageID].weight == 0, "ExampleValidatorManager: message already exists");

WARP_MESSENGER.sendWarpMessage(messageBytes);

stakingToken.transferFrom(msg.sender, address(this), amount);
balanceOf[msg.sender] += amount;
totalSupply += amount;

registeredValidatorMessages[messageID] = Validator(subnetID, nodeID, amount, 0, 0, 0, msg.sender);
}

function receiveRegisterValidator(uint32 messageIndex) external {
(WarpMessage memory warpMessage, bool success) = WARP_MESSENGER.getVerifiedWarpMessage(messageIndex);
require(success, "ExampleValidatorManager: invalid warp message");

// TODO: check if the sender is P-Chain
// require(warpMessage.sourceChainID == P_CHAIN_ID, "ExampleValidatorManager: invalid source chain ID");
// require(warpMessage.originSenderAddress == address(this), "ExampleValidatorManager: invalid origin sender address");

// Parse the payload of the message.
ValidatorRegisteredMessage memory registeredMessage = abi.decode(warpMessage.payload, (ValidatorRegisteredMessage));

bytes32 messageID = registeredMessage.messageID;
require(messageID != bytes32(0), "ExampleValidatorManager: invalid messageID");

// TODO: maybe we want to minimize errors here?
Validator memory pendingValidator = registeredValidatorMessages[messageID];
require(pendingValidator.weight != 0, "ExampleValidatorManager: pending message does not exist");

require(pendingValidator.startedAt == 0, "ExampleValidatorManager: register message already consumed");

bytes32 stakingID = keccak256(abi.encode(pendingValidator.subnetID, pendingValidator.nodeID));
require(activeValidators[stakingID] == bytes32(0), "ExampleValidatorManager: validator already exists");

activeValidators[stakingID] = messageID;
registeredValidatorMessages[messageID].startedAt = block.timestamp;
}

// TODO: review error messages

// TODO: add cooldown period for withdraw/redeem
function removeValidator(bytes32 subnetID, bytes32 nodeID) external {
bytes32 stakingID = keccak256(abi.encode(subnetID, nodeID));
bytes32 messageID = activeValidators[stakingID];
require(messageID != bytes32(0), "Validator not found");

Validator memory validator = registeredValidatorMessages[messageID];
require(validator.rewardAddress == msg.sender, "Only the validator can remove itself");

require(
block.timestamp >= validator.startedAt + minStakingDuration,
"Cannot remove validator before min staking duration"
);

require(validator.redeemedAt == 0, "Validator already redeemed");

SetSubnetValidatorWeightMessage memory message = SetSubnetValidatorWeightMessage(messageID, MAX_UINT64, 0);
bytes memory messageBytes = abi.encode(message);
WARP_MESSENGER.sendWarpMessage(messageBytes);
registeredValidatorMessages[messageID].redeemedAt = block.timestamp;
}

// todo: should we give a partial reward in case the validator got removed from P-chain (balance drained)?
function receiveRegisterMessageInvalid(uint32 messageIndex) external {
(WarpMessage memory warpMessage, bool success) = WARP_MESSENGER.getVerifiedWarpMessage(messageIndex);
require(success, "ExampleValidatorManager: invalid warp message");

InvalidValidatorRegisterMessage memory registeredMessage = abi.decode(
warpMessage.payload,
(InvalidValidatorRegisterMessage)
);

bytes32 messageID = registeredMessage.messageID;
require(messageID != bytes32(0), "ExampleValidatorManager: invalid messageID");

Validator memory pendingValidator = registeredValidatorMessages[messageID];
if (pendingValidator.weight == 0) {
return;
}
bytes32 stakingID = keccak256(abi.encode(pendingValidator.subnetID, pendingValidator.nodeID));
delete activeValidators[stakingID];
// TODO: do we need to check balanceOf[pendingValidator.rewardAddress] > weight?
uint256 stakedAmount = pendingValidator.weight;
// if redeemedAt is 0, this was not a graceful exit.
if (pendingValidator.redeemedAt != 0) {
// TODO: a reward here should be calculated and locked until
// a uptime report is available for the validator.
// The validator should only be able to redeem the reward after
// the uptime confirms the validator is eligible for the reward.
// uint256 reward = calculateReward(
// pendingValidator.weight,
// pendingValidator.startedAt,
// pendingValidator.redeemedAt
// );
// totalAmount += reward;
}
unlockedBalanceOf[pendingValidator.rewardAddress] += stakedAmount;
}

function increaseValidatorWeight(bytes32 subnetID, bytes32 nodeID, uint64 amount) external {}

function decreaseValidatorWeight(bytes32 subnetID, bytes32 nodeID, uint64 amount) external {}

function receiveValidatorRegistered(uint32 messageIndex) external {}

function receiveValidatorWeightChanged(uint32 messageIndex) external {}

function receiveUptimeMessage(uint32 messageIndex) external {}

function setSubnetValidatorManager(bytes32 subnetID, bytes32 chainID, address validatorManager) external {}

// TODO: add uptime tracking/rewards based on uptimes

// TODO: add partial withdraw + increase stake
// TODO: check weight usages in case == 0

// TODO: add delegation

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the tx flow for delegation? does it make sense to batch updating the delegations to a single SetSubnetValidatorWeightTx?


// Owner function to start the staking period.
function startStaking() external onlyOwner {
require(!stakingEnabled, "Staking has already started");
stakingEnabled = true;
}

// TODO: validation vs delegation should be weighted differently for rewards
// Function to calculate the user's reward.
// TODO: we probably would want a better reward calculation
function calculateReward(uint256 amount, uint256 startedAt, uint256 finishedAt) internal view returns (uint256) {
if (finishedAt <= startedAt || !stakingEnabled || finishedAt - startedAt < minStakingDuration) {
return 0;
}

uint256 stakingTime = finishedAt - startedAt;
return (stakingTime * rewardRate * amount) / (1 ether); // Assuming rewardRate is scaled appropriately

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we add the comment about the scale to the comment by the definition?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this reward calculation is even something we would use in production.

}
}
113 changes: 113 additions & 0 deletions contracts/contracts/interfaces/IStakingManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./IAllowList.sol";

interface IStakingManager {
// RegisterValidatorMessage is the message that is sent by the staking manager contract to the P-Chain as a warp message
// to register a validator on a subnet.
// A messageID can be calculated by hashing (sha256) the RegisterValidatorMessage.
struct RegisterValidatorMessage {
bytes32 subnetID;
bytes32 nodeID;
uint64 weight;
uint64 expiryTimestamp;
bytes signature;
}

// ValidatorRegisteredMessage is the message that is sentto the staking manager contract as a warp message
// that confirms the registration of a validator on P-chain. The messageID is the sha256 of the RegisterValidatorMessage.
struct ValidatorRegisteredMessage {
// The messageID is the sha256 of the related RegisterValidatorMessage.
bytes32 messageID;
}

// InvalidValidatorRegisterMessage is the message that is sent to the staking manager contract as a warp message
// that indicates that the registration of a validator on P-chain was invalid or will forever be invalid (validation finished).
struct InvalidValidatorRegisterMessage {
// The messageID is the sha256 of the related RegisterValidatorMessage.
bytes32 messageID;
}

// SetSubnetValidatorWeightMessage is the message that is sent by the staking manager contract to the P-Chain as a warp message
// to set the weight of a validator on a subnet.
struct SetSubnetValidatorWeightMessage {
// The messageID is the sha256 of the related RegisterValidatorMessage.
bytes32 messageID;
uint64 nonce;
uint64 weight;
}

// ValidatorWeightChangedMessage is the message that is sent to the staking manager contract as a warp message
// that confirms the change of weight of a validator on P-chain.
struct ValidatorWeightChangedMessage {
// The messageID is the sha256 of the related RegisterValidatorMessage.
bytes32 messageID;
uint64 nonce;
uint64 weight;
}

// UptimeMessage is the warp message that can be received by the staking manager contract to evaluate the uptime of a validator.
// This can be used to calculate the reward for a validator.
struct UptimeMessage {
// The messageID is the sha256 of the related RegisterValidatorMessage.
bytes32 messageID;
uint256 from;
uint256 to;
uint256 uptime;
}

// SetSubnetValidatorManager is the warp message that can be sent by the staking manager contract to the P-Chain
// to change the validator manager of a subnet.
// Note: this can only be done if the existing validator manager is the contract that is sending the message.
// The first validator manager must be registered manually (not via a contract) on the P-chain.
struct SetSubnetValidatorManager {
bytes32 subnetID;
bytes32 chainID;
address validatorManager;
}

// registerValidator creates and sends a warp message for P-Chain to register a validator on a subnet.
// A successfull transaction will not imply that the validator is registered. See receiveValidatorRegistered.
// The contract sending the message must be a registered staking contract on the P-chain via SetSubnetValidatorManagerTx.
function registerValidator(
bytes32 subnetID,
bytes32 nodeID,
uint64 amount,
uint64 expiryTimestamp,
bytes memory signature
) external;

// removeValidator creates and sends a warp message for P-Chain to remove a validator from a subnet.
// A successfull transaction will not imply that the validator is removed. See receiveRegisterMessageInvalid.
function removeValidator(bytes32 subnetID, bytes32 nodeID) external;

// increaseValidatorWeight creates and sends a warp message for P-Chain to increase the weight of a validator on a subnet.
// A successfull transaction will not imply that the weight is increased. See receiveValidatorWeightChanged.
function increaseValidatorWeight(bytes32 subnetID, bytes32 nodeID, uint64 amount) external;

// decreaseValidatorWeight creates and sends a warp message for P-Chain to decrease the weight of a validator on a subnet.
// A successfull transaction will not imply that the weight is decreased. See receiveValidatorWeightChanged.
function decreaseValidatorWeight(bytes32 subnetID, bytes32 nodeID, uint64 amount) external;

// receiveValidatorRegistered is called with a verified warp messageIndex to confirm the registration of a validator on P-chain.
// The warp payload corresponds to messageIndex must be a ValidatorRegisteredMessage.
function receiveValidatorRegistered(uint32 messageIndex) external;

// receiveValidatorWeightChanged is called with a verified warp messageIndex to confirm the change of weight of a validator on P-chain.
// The warp payload corresponds to messageIndex must be a ValidatorWeightChangedMessage.
function receiveValidatorWeightChanged(uint32 messageIndex) external;

// receiveRegisterMessageInvalid is called with a verified warp messageIndex to confirm that the registration of a validator on P-chain was invalid
// or will forever be invalid (validation finished).
function receiveRegisterMessageInvalid(uint32 messageIndex) external;

// receiveUptimeMessage is called with a verified warp messageIndex to report the confirmed uptime of a validator between two timestamps.
// The warp payload corresponds to messageIndex must be a UptimeMessage.
function receiveUptimeMessage(uint32 messageIndex) external;

// setSubnetValidatorManager creates and sends a warp message for P-Chain to change the validator manager of a subnet.
// This can only be done if the existing validator manager is the contract that is sending the message.
function setSubnetValidatorManager(bytes32 subnetID, bytes32 chainID, address validatorManager) external;

// TODO: add delegation
}
Loading