Skip to content

Commit

Permalink
Add SDRewardManager (ETHx Merkle Automation) (#253)
Browse files Browse the repository at this point in the history
* feat: add sd reward manager contract

* test: add tests and scripts

* fix: permissions

* chore: allow manager to grant and revoke permissions

* Revert "chore: allow manager to grant and revoke permissions"

This reverts commit b82cdf2.

* fix: call internal function and allow manager to grant and revoke permissions

* feat: allow overriding last non approved entry

* refactor: test file name

* refactor: use legacy mechanism to check access control

* feat: fetch cycle number from pool
  • Loading branch information
blockgroot authored Nov 5, 2024
1 parent deef735 commit 781525e
Show file tree
Hide file tree
Showing 5 changed files with 412 additions and 0 deletions.
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);
}

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

rewardEntry.approved = true;

if (rewardEntry.amount > 0) {
IERC20Upgradeable(staderConfig.getStaderToken()).safeTransferFrom(
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,6 +67,8 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable {
//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");
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
Expand Down Expand Up @@ -537,6 +539,14 @@ contract StaderConfig is IStaderConfig, AccessControlUpgradeable {
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

0 comments on commit 781525e

Please sign in to comment.