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

Add SDRewardManager (ETHx Merkle Automation) #253

Merged
merged 10 commits into from
Nov 5, 2024
126 changes: 126 additions & 0 deletions contracts/SDRewardManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
pragma solidity 0.8.16;

import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import { IStaderConfig } from "./interfaces/IStaderConfig.sol";
import { ISocializingPool } from "./interfaces/ISocializingPool.sol";
import { UtilLib } from "./library/UtilLib.sol";

/**
* @title SDRewardManager
* @notice This contract is responsible to add SD rewards to the socializing pool
*/
contract SDRewardManager is Initializable {
using SafeERC20Upgradeable for IERC20Upgradeable;

struct SDRewardEntry {
uint256 cycleNumber;
uint256 amount;
bool approved;
}

///@notice Address of the Stader Config contract
IStaderConfig public staderConfig;

///@notice Cycle number of the last added entry
uint256 public lastEntryCycleNumber;

// Mapping of cycle numbers to reward entries
mapping(uint256 => SDRewardEntry) public rewardEntries;

// Event emitted when a new reward entry is created
event NewRewardEntry(uint256 indexed cycleNumber, uint256 amount);

// Event emitted when a reward entry is approved
event RewardEntryApproved(uint256 indexed cycleNumber, uint256 amount);

error AccessDenied(address account);
error EntryNotFound(uint256 cycleNumber);
error EntryAlreadyApproved(uint256 cycleNumber);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/**
* @notice Initializes the contract with a Stader configuration address
* @param _staderConfig Address of the StaderConfig contract
*/
function initialize(address _staderConfig) external initializer {
UtilLib.checkNonZeroAddress(_staderConfig);
staderConfig = IStaderConfig(_staderConfig);
}

/**
* @notice Adds a new reward entry for the current cycle (fetched from socializing pool)
* @param _amount The amount of SD to be rewarded
*/
function addRewardEntry(uint256 _amount) external {
if (!staderConfig.onlySDRewardEntryRole(msg.sender)) {
revert AccessDenied(msg.sender);
}
uint256 cycleNumber = getCurrentCycleNumber();
SDRewardEntry memory rewardEntry = rewardEntries[cycleNumber];

if (rewardEntry.approved) {
revert EntryAlreadyApproved(cycleNumber);
}

rewardEntry.cycleNumber = cycleNumber;
rewardEntry.amount = _amount;
lastEntryCycleNumber = cycleNumber;
rewardEntries[cycleNumber] = rewardEntry;

emit NewRewardEntry(cycleNumber, _amount);
}

/**
* @notice Approves a reward entry for the current cycle (fetched from socializing pool) and transfers the reward amount.
*/
function approveEntry() external {
if (!staderConfig.onlySDRewardApproverRole(msg.sender)) {
revert AccessDenied(msg.sender);
}

uint256 cycleNumber = getCurrentCycleNumber();

SDRewardEntry storage rewardEntry = rewardEntries[cycleNumber];

if (rewardEntry.cycleNumber == 0) {
revert EntryNotFound(cycleNumber);

Check warning on line 92 in contracts/SDRewardManager.sol

View check run for this annotation

Codecov / codecov/patch

contracts/SDRewardManager.sol#L92

Added line #L92 was not covered by tests
}

if (rewardEntry.approved) {
revert EntryAlreadyApproved(cycleNumber);
}

rewardEntry.approved = true;

if (rewardEntry.amount > 0) {
IERC20Upgradeable(staderConfig.getStaderToken()).safeTransferFrom(
blockgroot marked this conversation as resolved.
Show resolved Hide resolved
msg.sender,
staderConfig.getPermissionlessSocializingPool(),
rewardEntry.amount
);
emit RewardEntryApproved(cycleNumber, rewardEntry.amount);
}
}

/**
* @notice Returns the latest reward entry
* @return The latest SDRewardEntry struct for the most recent cycle
*/
function viewLatestEntry() external view returns (SDRewardEntry memory) {
return rewardEntries[lastEntryCycleNumber];
}

/**
* @notice Fetch the current cycle number from permissionless socializing pool
* @return Current cycle number
*/
function getCurrentCycleNumber() public view returns (uint256) {
return ISocializingPool(staderConfig.getPermissionlessSocializingPool()).getCurrentRewardsIndex();
}
}
10 changes: 10 additions & 0 deletions contracts/StaderConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@
//Roles
bytes32 public constant override MANAGER = keccak256("MANAGER");
bytes32 public constant override OPERATOR = keccak256("OPERATOR");
bytes32 public constant override ROLE_SD_REWARD_ENTRY = keccak256("ROLE_SD_REWARD_ENTRY");
galacticminter marked this conversation as resolved.
Show resolved Hide resolved
bytes32 public constant override ROLE_SD_REWARD_APPROVER = keccak256("ROLE_SD_REWARD_APPROVER");

bytes32 public constant SD = keccak256("SD");
bytes32 public constant ETHx = keccak256("ETHx");

Check warning on line 74 in contracts/StaderConfig.sol

View workflow job for this annotation

GitHub Actions / Run linters

Constant name must be in capitalized SNAKE_CASE

mapping(bytes32 => uint256) private constantsMap;
mapping(bytes32 => uint256) private variablesMap;
Expand Down Expand Up @@ -537,6 +539,14 @@
return hasRole(OPERATOR, account);
}

function onlySDRewardEntryRole(address account) external view override returns (bool) {
return hasRole(ROLE_SD_REWARD_ENTRY, account);
}

function onlySDRewardApproverRole(address account) external view override returns (bool) {
return hasRole(ROLE_SD_REWARD_APPROVER, account);
}

function verifyDepositAndWithdrawLimits() internal view {
if (
!(variablesMap[MIN_DEPOSIT_AMOUNT] != 0 &&
Expand Down
8 changes: 8 additions & 0 deletions contracts/interfaces/IStaderConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ interface IStaderConfig {

function OPERATOR() external view returns (bytes32);

function ROLE_SD_REWARD_ENTRY() external view returns (bytes32);

function ROLE_SD_REWARD_APPROVER() external view returns (bytes32);

// Constants
function getStakedEthPerNode() external view returns (uint256);

Expand Down Expand Up @@ -171,4 +175,8 @@ interface IStaderConfig {
function onlyManagerRole(address account) external view returns (bool);

function onlyOperatorRole(address account) external view returns (bool);

function onlySDRewardEntryRole(address account) external view returns (bool);

function onlySDRewardApproverRole(address account) external view returns (bool);
}
12 changes: 12 additions & 0 deletions scripts/deploy/SDRewardManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ethers, upgrades } from 'hardhat'

async function main() {
const [owner] = await ethers.getSigners()
const staderConfigAddr = process.env.STADER_CONFIG ?? ''

const sdRewardManagerFactory = await ethers.getContractFactory('SDRewardManager')
const sdRewardManager = await upgrades.deployProxy(sdRewardManagerFactory, [staderConfigAddr])
console.log('SDRewardManager deployed to: ', sdRewardManager.address)
}

main()
Loading
Loading