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

update StakingContractMainnet to support vault tokens #11

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
43 changes: 43 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: test

on:
workflow_dispatch:
pull_request:
push:
branches:
- "main"

env:
FOUNDRY_PROFILE: ci

jobs:
foundry:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Run Forge build
run: |
forge --version
forge build --sizes
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: forge-test

- name: Forge style
run: |
forge fmt --check
49 changes: 49 additions & 0 deletions broadcast/StakingContract.s.sol/421614/run-1728531911.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions script/NftVaultManager.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.13;

import "forge-std/Script.sol";
import "../src/Vault/NftVaultManager.sol";

contract NftVaultManagerScript is Script {
function run() public {
vm.startBroadcast();

new NftVaultManager();

vm.stopBroadcast();
}
}
3 changes: 1 addition & 2 deletions script/StakingContract.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import "../src/Rewards/StakingContractMainnet.sol";

contract StakingContractScript is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
vm.startBroadcast();

new StakingContractMainnet();

Expand Down
1 change: 1 addition & 0 deletions sh/deployArbitrum.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ source .env

# To deploy and verify our contract
forge script script/MagicswapV2.s.sol:MagicswapV2Script --aws --rpc-url $ARBITRUM_RPC --broadcast --verify -vvvv
forge script script/StakingContract.s.sol:StakingContractScript --aws --rpc-url $ARBITRUM_RPC --broadcast --verify -vvvv
2 changes: 2 additions & 0 deletions sh/deployArbitrumSepolia.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ source .env

# To deploy and verify our contract
forge script script/MagicswapV2.s.sol:MagicswapV2Script --aws --rpc-url $ARBITRUM_SEPOLIA_RPC --broadcast --verify -vvvv
forge script script/StakingContract.s.sol:StakingContractScript --aws --rpc-url $ARBITRUM_SEPOLIA_RPC --broadcast --verify -vvvv
forge script script/NftVaultManager.s.sol:NftVaultManagerScript --aws --rpc-url $ARBITRUM_SEPOLIA_RPC --broadcast --verify -vvvv
122 changes: 99 additions & 23 deletions src/Rewards/StakingContractMainnet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ contract StakingContractMainnet is ReentrancyGuard {
address token; // 2nd slot
address rewardToken; // 3rd slot
uint32 endTime; // 3rd slot
bool isRewardRounded; // 3rd slot
uint256 rewardPerLiquidity; // 4th slot
uint32 lastRewardTime; // 5th slot
uint112 rewardRemaining; // 5th slot
Expand Down Expand Up @@ -65,7 +66,8 @@ contract StakingContractMainnet is ReentrancyGuard {
uint256 id,
uint256 amount,
uint256 startTime,
uint256 endTime
uint256 endTime,
bool isRewardRounded
);
event IncentiveUpdated(uint256 indexed id, int256 changeAmount, uint256 newStartTime, uint256 newEndTime);
event Stake(address indexed token, address indexed user, uint256 amount);
Expand All @@ -74,11 +76,14 @@ contract StakingContractMainnet is ReentrancyGuard {
event Unsubscribe(uint256 indexed id, address indexed user);
event Claim(uint256 indexed id, address indexed user, uint256 amount);

function createIncentive(address token, address rewardToken, uint112 rewardAmount, uint32 startTime, uint32 endTime)
external
nonReentrant
returns (uint256 incentiveId)
{
function createIncentive(
address token,
address rewardToken,
uint112 rewardAmount,
uint32 startTime,
uint32 endTime,
bool isRewardRounded
) external nonReentrant returns (uint256 incentiveId) {
alecananian marked this conversation as resolved.
Show resolved Hide resolved
if (rewardAmount <= 0) revert InvalidInput();

if (startTime < block.timestamp) startTime = uint32(block.timestamp);
Expand All @@ -99,13 +104,16 @@ contract StakingContractMainnet is ReentrancyGuard {
rewardToken: rewardToken,
lastRewardTime: startTime,
endTime: endTime,
isRewardRounded: isRewardRounded,
rewardRemaining: rewardAmount,
liquidityStaked: 0,
// Initial value of rewardPerLiquidity can be arbitrarily set to a non-zero value.
rewardPerLiquidity: type(uint256).max / 2
});

emit IncentiveCreated(token, rewardToken, msg.sender, incentiveId, rewardAmount, startTime, endTime);
emit IncentiveCreated(
token, rewardToken, msg.sender, incentiveId, rewardAmount, startTime, endTime, isRewardRounded
);
}

function updateIncentive(uint256 incentiveId, int112 changeAmount, uint32 newStartTime, uint32 newEndTime)
Expand All @@ -119,18 +127,24 @@ contract StakingContractMainnet is ReentrancyGuard {
_accrueRewards(incentive);

if (newStartTime != 0) {
if (newStartTime < block.timestamp) newStartTime = uint32(block.timestamp);
if (newStartTime < block.timestamp) {
newStartTime = uint32(block.timestamp);
}

incentive.lastRewardTime = newStartTime;
}

if (newEndTime != 0) {
if (newEndTime < block.timestamp) newEndTime = uint32(block.timestamp);
if (newEndTime < block.timestamp) {
newEndTime = uint32(block.timestamp);
}

incentive.endTime = newEndTime;
}

if (incentive.lastRewardTime >= incentive.endTime) revert InvalidTimeFrame();
if (incentive.lastRewardTime >= incentive.endTime) {
revert InvalidTimeFrame();
}

if (changeAmount > 0) {
incentive.rewardRemaining += uint112(changeAmount);
Expand All @@ -139,7 +153,9 @@ contract StakingContractMainnet is ReentrancyGuard {
} else if (changeAmount < 0) {
uint112 transferOut = uint112(-changeAmount);

if (transferOut > incentive.rewardRemaining) transferOut = incentive.rewardRemaining;
if (transferOut > incentive.rewardRemaining) {
transferOut = incentive.rewardRemaining;
}

unchecked {
incentive.rewardRemaining -= transferOut;
Expand Down Expand Up @@ -229,14 +245,28 @@ contract StakingContractMainnet is ReentrancyGuard {
emit Unstake(token, msg.sender, amount);
}

function subscribeToIncentives(uint256[] memory incentiveIds) external {
uint256 n = incentiveIds.length;

for (uint256 i = 0; i < n; i = _increment(i)) {
subscribeToIncentive(incentiveIds[i]);
}
}

function subscribeToIncentive(uint256 incentiveId) public nonReentrant {
if (incentiveId > incentiveCount || incentiveId <= 0) revert InvalidInput();
if (incentiveId > incentiveCount || incentiveId <= 0) {
revert InvalidInput();
}

if (rewardPerLiquidityLast[msg.sender][incentiveId] != 0) revert AlreadySubscribed();
if (rewardPerLiquidityLast[msg.sender][incentiveId] != 0) {
revert AlreadySubscribed();
}

Incentive storage incentive = incentives[incentiveId];

if (userStakes[msg.sender][incentive.token].liquidity <= 0) revert NotStaked();
if (userStakes[msg.sender][incentive.token].liquidity <= 0) {
revert NotStaked();
}

_accrueRewards(incentive);

Expand All @@ -262,14 +292,18 @@ contract StakingContractMainnet is ReentrancyGuard {

uint256 incentiveId = userStake.subscribedIncentiveIds.getUint24ValueAt(incentiveIndex);

if (rewardPerLiquidityLast[msg.sender][incentiveId] == 0) revert AlreadyUnsubscribed();
if (rewardPerLiquidityLast[msg.sender][incentiveId] == 0) {
revert AlreadyUnsubscribed();
}

Incentive storage incentive = incentives[incentiveId];

_accrueRewards(incentive);

/// In case there is a token specific issue we can ignore rewards.
if (!ignoreRewards) _claimReward(incentive, incentiveId, userStake.liquidity);
if (!ignoreRewards) {
_claimReward(incentive, incentiveId, userStake.liquidity);
}

rewardPerLiquidityLast[msg.sender][incentiveId] = 0;

Expand All @@ -281,7 +315,9 @@ contract StakingContractMainnet is ReentrancyGuard {
}

function accrueRewards(uint256 incentiveId) external nonReentrant {
if (incentiveId > incentiveCount || incentiveId <= 0) revert InvalidInput();
if (incentiveId > incentiveCount || incentiveId <= 0) {
revert InvalidInput();
}

_accrueRewards(incentives[incentiveId]);
}
Expand All @@ -292,7 +328,9 @@ contract StakingContractMainnet is ReentrancyGuard {
rewards = new uint256[](n);

for (uint256 i = 0; i < n; i = _increment(i)) {
if (incentiveIds[i] > incentiveCount || incentiveIds[i] <= 0) revert InvalidInput();
if (incentiveIds[i] > incentiveCount || incentiveIds[i] <= 0) {
revert InvalidInput();
}

Incentive storage incentive = incentives[incentiveIds[i]];

Expand All @@ -302,6 +340,26 @@ contract StakingContractMainnet is ReentrancyGuard {
}
}

/// @dev Claims rewards for all incentives in the list, skipping reward rounding.
function claimAllRewards(uint256[] calldata incentiveIds)
external
nonReentrant
returns (uint256[] memory rewards)
{
uint256 n = incentiveIds.length;
rewards = new uint256[](n);
for (uint256 i = 0; i < n; i = _increment(i)) {
if (incentiveIds[i] > incentiveCount || incentiveIds[i] <= 0) {
revert InvalidInput();
}

Incentive storage incentive = incentives[incentiveIds[i]];
_accrueRewards(incentive);
rewards[i] =
_claimReward(incentive, incentiveIds[i], userStakes[msg.sender][incentive.token].liquidity, true);
}
}

function _accrueRewards(Incentive storage incentive) internal {
uint256 lastRewardTime = incentive.lastRewardTime;

Expand All @@ -315,10 +373,10 @@ contract StakingContractMainnet is ReentrancyGuard {

uint256 passedTime = maxTime - lastRewardTime;

uint256 reward = uint256(incentive.rewardRemaining) * passedTime / totalTime;
uint256 reward = (uint256(incentive.rewardRemaining) * passedTime) / totalTime;

// Increments of less than type(uint224).max - overflow is unrealistic.
incentive.rewardPerLiquidity += reward * type(uint112).max / incentive.liquidityStaked;
incentive.rewardPerLiquidity += (reward * type(uint112).max) / incentive.liquidityStaked;

incentive.rewardRemaining -= uint112(reward);

Expand All @@ -332,13 +390,31 @@ contract StakingContractMainnet is ReentrancyGuard {
function _claimReward(Incentive storage incentive, uint256 incentiveId, uint112 usersLiquidity)
internal
returns (uint256 reward)
{
return _claimReward(incentive, incentiveId, usersLiquidity, false);
}

function _claimReward(Incentive storage incentive, uint256 incentiveId, uint112 usersLiquidity, bool skipRounding)
internal
returns (uint256 reward)
{
reward = _calculateReward(incentive, incentiveId, usersLiquidity);

rewardPerLiquidityLast[msg.sender][incentiveId] = incentive.rewardPerLiquidity;
uint256 rewardDelta;
// Check if the reward should be rounded
if (!skipRounding && incentive.isRewardRounded) {
uint8 decimals = ERC20(incentive.rewardToken).decimals();
uint256 roundedReward = (reward / 10 ** decimals) * 10 ** decimals;
// Delta of rewards to be left claimable for the user in the future
rewardDelta = reward - roundedReward;
reward = roundedReward;
}

ERC20(incentive.rewardToken).safeTransfer(msg.sender, reward);
// Calculate the reward per liquidity delta based on actual rewards given
uint256 rewardPerLiquidityDelta = (rewardDelta * type(uint112).max) / usersLiquidity;
rewardPerLiquidityLast[msg.sender][incentiveId] = incentive.rewardPerLiquidity - rewardPerLiquidityDelta;

ERC20(incentive.rewardToken).safeTransfer(msg.sender, reward);
emit Claim(incentiveId, msg.sender, reward);
}

Expand All @@ -349,7 +425,7 @@ contract StakingContractMainnet is ReentrancyGuard {
{
reward = _calculateReward(incentive, incentiveId, usersLiquidity);

uint256 rewardPerLiquidityDelta = reward * type(uint112).max / newLiquidity;
uint256 rewardPerLiquidityDelta = (reward * type(uint112).max) / newLiquidity;

rewardPerLiquidityLast[msg.sender][incentiveId] = incentive.rewardPerLiquidity - rewardPerLiquidityDelta;
}
Expand Down
43 changes: 41 additions & 2 deletions src/Rewards/test/StakingContractMainnet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import "./TestSetup.sol";

contract CreateIncentiveTest is TestSetup {
function testCreateIncentive(uint112 amount, uint32 startTime, uint32 endTime) public {
_createIncentive(address(tokenA), address(tokenB), amount, startTime, endTime);
_createIncentive(address(tokenA), address(tokenB), amount, startTime, endTime, false);
}

function testFailCreateIncentiveInvalidRewardToken(uint32 startTime, uint32 endTime) public {
_createIncentive(address(tokenA), zeroAddress, 1, startTime, endTime);
_createIncentive(address(tokenA), zeroAddress, 1, startTime, endTime, false);
}

function testUpdateIncentive(
Expand Down Expand Up @@ -147,6 +147,45 @@ contract CreateIncentiveTest is TestSetup {
assertEqInexact(reward0 + reward1 + soloReward, totalReward, 10);
}

function testClaimRoundedRewards() public {
uint112 amount = testIncentiveAmount;
uint256 duration = testIncentiveDuration;
uint256 incentiveId = _createIncentive(
address(tokenA), address(tokenB), amount, uint32(block.timestamp), uint32(block.timestamp + duration), true
);
uint256[] memory incentiveIds = new uint256[](1);
incentiveIds[0] = incentiveId;
StakingContractMainnet.Incentive memory incentive = _getIncentive(incentiveId);

// 2 users stake and subscribe
_stake(address(tokenA), 1, johnDoe, true);
_stake(address(tokenA), 1, janeDoe, true);
_subscribeToIncentive(incentiveId, johnDoe);
_subscribeToIncentive(incentiveId, janeDoe);

// 1/30 the time has passed
vm.warp(incentive.lastRewardTime + 86400);

// Each user got 1/60 of the total reward amount
(,, uint256 johnDoeReward) = _calculateReward(incentiveId, johnDoe);
(,, uint256 janeDoeReward) = _calculateReward(incentiveId, janeDoe);
assertEq(johnDoeReward, 16666666666666666666);
assertEq(janeDoeReward, 16666666666666666666);

// 1 user claims
vm.prank(johnDoe);
uint256[] memory johnDoeClaimed = stakingContract.claimRewards(incentiveIds);
assertEq(johnDoeClaimed[0], 16000000000000000000);

// User still has some rewards pending
(,, johnDoeReward) = _calculateReward(incentiveId, johnDoe);
assertEq(johnDoeReward, 666666666666666666);

// Other user still has the same reward
(,, janeDoeReward) = _calculateReward(incentiveId, janeDoe);
assertEq(janeDoeReward, 16666666666666666666);
}

function testUnstakeSaveRewards() public {
_stake(address(tokenA), 1, johnDoe, true);
_subscribeToIncentive(ongoingIncentive, johnDoe);
Expand Down
Loading
Loading