Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated
Binary file added .yarn/install-state.gz
Binary file not shown.
942 changes: 942 additions & 0 deletions .yarn/releases/yarn-4.9.2.cjs

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
compressionLevel: mixed

enableGlobalCache: true

nodeLinker: node-modules

npmRegistryServer: "https://registry.npmjs.org"

preferReuse: true

supportedArchitectures:
cpu:
- x64
- arm64
libc:
- glibc
- musl
os:
- darwin
- linux

yarnPath: .yarn/releases/yarn-4.9.2.cjs
21 changes: 15 additions & 6 deletions cannonfile.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
name = "fixed-staking-rewards"
version = "1.0.0"
version = "1.0.1"
description = "Fixed staking rewards contract for ERC20 tokens"

[var.main]
owner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
rewardsToken = "0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9"
stakingToken = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
rewardsTokenFeed = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
owner = "0xEb3107117FEAd7de89Cd14D463D340A2E6917769"
rewardsToken = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F"
stakingToken = "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51"
rewardsTokenFeed = "0xDC3EA94CD0AC27d9A86C180091e7f78C683d3699"
rewardYieldForYear = "<%= parseEther('0.5') %>"

[deploy.FixedStakingRewards]
artifact = "FixedStakingRewards"
args = [
"<%= settings.owner %>",
"<%= settings.rewardsToken %>",
"<%= settings.rewardsToken %>",
"<%= settings.stakingToken %>",
"<%= settings.rewardsTokenFeed %>",
]

[invoke.FixedStakingRewards_setRewardYieldForYear]
target = ["FixedStakingRewards"]
fromCall.func = "owner"
func = "setRewardYieldForYear"
args = [
"<%= settings.rewardYieldForYear %>"
]
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "fixed-staking-rewards",
"version": "0.0.0",
"main": "index.js",
"repository": "synthetixio/fixed-staking-rewards",
"author": "Synthetix",
"license": "MIT",
"private": true,
"scripts": {
"start": "cannon build --keep-alive --dry-run --impersonate-all --anvil.port 8545 --chain-id 1 --rpc-url https://mainnet.infura.io/v3/$INFURA_API_KEY",
"build": "cannon build --dry-run --impersonate-all --anvil.port 8545 --chain-id 1 --rpc-url https://mainnet.infura.io/v3/$INFURA_API_KEY",
"lint": "forge fmt",
"test": "forge test -vvv"
},
"devDependencies": {
"@usecannon/cli": "2.24.2"
},
"packageManager": "yarn@4.9.2"
}
47 changes: 22 additions & 25 deletions src/FixedStakingRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,28 @@ error InvalidPriceFeed(uint256 updateTime, int256 currentRewardTokenRate);

contract FixedStakingRewards is IStakingRewards, ERC20, ReentrancyGuard, Ownable {
using SafeERC20 for IERC20;

/* ========== STATE VARIABLES ========== */

IERC20 immutable public rewardsToken;
IERC20 immutable public stakingToken;
IChainlinkAggregator immutable public rewardsTokenRateAggregator;
IERC20 public immutable rewardsToken;
IERC20 public immutable stakingToken;
IChainlinkAggregator public immutable rewardsTokenRateAggregator;
uint256 public immutable rewardsTokenRateDecimals;
uint256 public targetRewardApy = 0;
uint256 public rewardRate = 0;
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
uint256 public rewardsAvailableDate;


mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;

/* ========== CONSTRUCTOR ========== */

constructor(
address _owner,
address _rewardsToken,
address _stakingToken,
address _rewardsTokenRateAggregator
) ERC20("FixedStakingRewards", "FSR") Ownable(_owner) {
constructor(address _owner, address _rewardsToken, address _stakingToken, address _rewardsTokenRateAggregator)
ERC20("FixedStakingRewards", "FSR")
Ownable(_owner)
{
rewardsToken = IERC20(_rewardsToken);
stakingToken = IERC20(_stakingToken);
rewardsTokenRateAggregator = IChainlinkAggregator(_rewardsTokenRateAggregator);
Expand All @@ -59,16 +56,14 @@ contract FixedStakingRewards is IStakingRewards, ERC20, ReentrancyGuard, Ownable
if (totalSupply() == 0) {
return rewardPerTokenStored;
}
return
rewardPerTokenStored +
(block.timestamp - lastUpdateTime) * rewardRate;
return rewardPerTokenStored + (block.timestamp - lastUpdateTime) * rewardRate;
}

function earned(address account) public override view returns (uint256) {
function earned(address account) public view override returns (uint256) {
return (balanceOf(account) * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18 + rewards[account];
}

function getRewardForDuration() public override view returns (uint256) {
function getRewardForDuration() public view override returns (uint256) {
return rewardRate * 14 days;
}

Expand All @@ -81,10 +76,7 @@ contract FixedStakingRewards is IStakingRewards, ERC20, ReentrancyGuard, Ownable

uint256 requiredRewards = (totalSupply() + amount) * getRewardForDuration() / 1e18;
if (requiredRewards > rewardsToken.balanceOf(address(this))) {
revert NotEnoughRewards(
rewardsToken.balanceOf(address(this)),
requiredRewards
);
revert NotEnoughRewards(rewardsToken.balanceOf(address(this)), requiredRewards);
}

_mint(msg.sender, amount);
Expand All @@ -93,7 +85,9 @@ contract FixedStakingRewards is IStakingRewards, ERC20, ReentrancyGuard, Ownable
}

function withdraw(uint256 amount) public override nonReentrant updateReward(msg.sender) {
if (block.timestamp < rewardsAvailableDate) revert RewardsNotAvailableYet(block.timestamp, rewardsAvailableDate);
if (block.timestamp < rewardsAvailableDate) {
revert RewardsNotAvailableYet(block.timestamp, rewardsAvailableDate);
}
if (amount == 0) revert CannotWithdrawZero();

_rebalance();
Expand All @@ -104,7 +98,9 @@ contract FixedStakingRewards is IStakingRewards, ERC20, ReentrancyGuard, Ownable
}

function getReward() public override nonReentrant updateReward(msg.sender) {
if (block.timestamp < rewardsAvailableDate) revert RewardsNotAvailableYet(block.timestamp, rewardsAvailableDate);
if (block.timestamp < rewardsAvailableDate) {
revert RewardsNotAvailableYet(block.timestamp, rewardsAvailableDate);
}
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
Expand Down Expand Up @@ -157,11 +153,12 @@ contract FixedStakingRewards is IStakingRewards, ERC20, ReentrancyGuard, Ownable

/* ========== INTERNAL FUNCTIONS ========== */
function _rebalance() internal {
(, int256 currentRewardTokenRate, , uint256 updateTime, ) = rewardsTokenRateAggregator.latestRoundData();
if (currentRewardTokenRate == 0 || updateTime < block.timestamp - 1 hours) {
(, int256 currentRewardTokenRate,, uint256 updateTime,) = rewardsTokenRateAggregator.latestRoundData();
if (currentRewardTokenRate == 0 || updateTime < block.timestamp - 1 days - 1 hours) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit 25 hours here probably better way to write it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it is quite readable and matches the report better, unless there is a technical downside - would probably be nicer this way?

revert InvalidPriceFeed(updateTime, currentRewardTokenRate);
}
rewardRate = targetRewardApy * 1e18 / (uint256(currentRewardTokenRate) * 10 ** (18 - rewardsTokenRateDecimals)) / 365 days;
rewardRate = targetRewardApy * 1e18 / (uint256(currentRewardTokenRate) * 10 ** (18 - rewardsTokenRateDecimals))
/ 365 days;
}

/* ========== MODIFIERS ========== */
Expand Down
18 changes: 3 additions & 15 deletions src/interfaces/IChainlinkAggregator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,10 @@ interface IChainlinkAggregator {
function getRoundData(uint80 _roundId)
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
}
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
3 changes: 2 additions & 1 deletion src/interfaces/IStakingRewards.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;

import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

// https://docs.synthetix.io/contracts/source/interfaces/istakingrewards
Expand All @@ -23,4 +24,4 @@ interface IStakingRewards {
function stake(uint256 amount) external;

function withdraw(uint256 amount) external;
}
}
Loading