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

implement reward cap and commission cap #402

Merged
merged 7 commits into from
Apr 26, 2024
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
57 changes: 47 additions & 10 deletions contracts/RewardsDistributor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ contract RewardsDistributor is IRewardsDistributor, Initializable, OwnableUpgrad
/// @notice Reward information: runner => RewardInfo
mapping(address => RewardInfo) private info;

/// @notice perMill, <max commission> = <selfStake> * <maxCommissionFactor>
/// 0: disabled
uint256 public maxCommissionFactor;

/// @notice perMill, <max pool reward> = <totalStake> * <maxRewardFactor>
/// 0: disabled
uint256 public maxRewardFactor;

/// @dev ### EVENTS
/// @notice Emitted when rewards are distributed for the earliest pending distributed Era.
event DistributeRewards(
Expand All @@ -98,6 +106,8 @@ contract RewardsDistributor is IRewardsDistributor, Initializable, OwnableUpgrad
event InstantRewards(address indexed runner, uint256 indexed eraIdx, uint256 token);
/// @notice Emitted when rewards arrive via increaseAgreementRewards()
event AgreementRewards(address indexed runner, uint256 agreementId, uint256 token);
/// @notice Emitted when rewards return to treasury due to exceed reward cap
event ReturnRewards(address indexed runner, uint256 rewards, uint256 commission);

modifier onlyRewardsStaking() {
require(msg.sender == settings.getContractAddress(SQContracts.RewardsStaking), 'G014');
Expand All @@ -119,6 +129,14 @@ contract RewardsDistributor is IRewardsDistributor, Initializable, OwnableUpgrad
settings = _settings;
}

function setMaxCommissionFactor(uint256 _maxCommissionFactor) external onlyOwner {
maxCommissionFactor = _maxCommissionFactor;
}

function setMaxRewardFactor(uint256 _maxRewardFactor) external onlyOwner {
maxRewardFactor = _maxRewardFactor;
}

/**
* @notice Initialize the runner first last claim era.
* Only RewardsStaking can call.
Expand Down Expand Up @@ -347,36 +365,55 @@ contract RewardsDistributor is IRewardsDistributor, Initializable, OwnableUpgrad
delete rewardInfo.eraRewardRemoveTable[rewardInfo.lastClaimEra];
if (rewardInfo.eraReward != 0) {
uint256 totalStake = rewardsStaking.getTotalStakingAmount(runner);
uint256 selfStake = rewardsStaking.getDelegationAmount(runner, runner);
require(totalStake > 0, 'RD006');

uint256 commissionRate = IIndexerRegistry(
settings.getContractAddress(SQContracts.IndexerRegistry)
).getCommissionRate(runner);
uint256 commission = MathUtil.mulDiv(commissionRate, rewardInfo.eraReward, PER_MILL);

info[runner].accSQTPerStake += MathUtil.mulDiv(
rewardInfo.eraReward - commission,
uint256 commission = commissionRate.mulDiv(rewardInfo.eraReward, PER_MILL);

// 1. total reward can not greater than maxRewardFactor * totalStake
// 2. commission can not greater than maxCommissionFactor * selfStake
uint256 cappedReward = maxRewardFactor > 0
? MathUtil.min(rewardInfo.eraReward, totalStake.mulDiv(maxRewardFactor, PER_MILL))
: rewardInfo.eraReward;
uint256 cappedCommission = maxCommissionFactor > 0
? MathUtil.min(commission, selfStake.mulDiv(maxCommissionFactor, PER_MILL))
: commission;
info[runner].accSQTPerStake += cappedReward.sub(cappedCommission).mulDiv(
PER_TRILL,
totalStake
);
if (commission > 0) {
IERC20 SQToken = IERC20(settings.getContractAddress(SQContracts.SQToken));
if (cappedCommission > 0) {
// add commission to unbonding request
IERC20(settings.getContractAddress(SQContracts.SQToken)).safeTransfer(
SQToken.safeTransfer(
settings.getContractAddress(SQContracts.Staking),
commission
cappedCommission
);
IStaking(settings.getContractAddress(SQContracts.Staking)).unbondCommission(
runner,
commission
cappedCommission
);
}

emit DistributeRewards(
runner,
rewardInfo.lastClaimEra,
rewardInfo.eraReward,
commission
MathUtil.max(cappedReward, cappedCommission),
cappedCommission
);
if (rewardInfo.eraReward - cappedReward > 0 || commission - cappedCommission > 0) {
uint256 rewardsReturn;
rewardsReturn +=
(rewardInfo.eraReward - commission) -
(cappedReward.sub(cappedCommission));
rewardsReturn += commission - cappedCommission;
address treasury = ISettings(settings).getContractAddress(SQContracts.Treasury);
SQToken.safeTransfer(treasury, rewardsReturn);
emit ReturnRewards(runner, rewardsReturn, commission - cappedCommission);
}
}
return rewardInfo.lastClaimEra;
}
Expand Down
9 changes: 9 additions & 0 deletions contracts/StakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@ contract StakingManager is IStakingManager, Initializable, OwnableUpgradeable {
return StakingUtil.currentStaking(sm, _currentEra);
}

function getDelegationAmount(address _source, address _runner) public view returns (uint256) {
uint256 eraNumber = IEraManager(settings.getContractAddress(SQContracts.EraManager))
.eraNumber();
Staking staking = Staking(settings.getContractAddress(SQContracts.Staking));
(uint256 era, uint256 valueAt, uint256 valueAfter) = staking.delegation(_source, _runner);
StakingAmount memory sm = StakingAmount(era, valueAt, valueAfter);
return StakingUtil.currentStaking(sm, eraNumber);
}

function getTotalStakingAmount(address _runner) public view override returns (uint256) {
uint256 eraNumber = IEraManager(settings.getContractAddress(SQContracts.EraManager))
.eraNumber();
Expand Down
77 changes: 77 additions & 0 deletions publish/ABI/RewardsDistributor.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,31 @@
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "runner",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rewards",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "commission",
"type": "uint256"
}
],
"name": "ReturnRewards",
"type": "event"
},
{
"anonymous": false,
"inputs": [
Expand Down Expand Up @@ -404,6 +429,32 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "maxCommissionFactor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxRewardFactor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
Expand Down Expand Up @@ -460,6 +511,32 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_maxCommissionFactor",
"type": "uint256"
}
],
"name": "setMaxCommissionFactor",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_maxRewardFactor",
"type": "uint256"
}
],
"name": "setMaxRewardFactor",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
24 changes: 24 additions & 0 deletions publish/ABI/StakingManager.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,30 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_source",
"type": "address"
},
{
"internalType": "address",
"name": "_runner",
"type": "address"
}
],
"name": "getDelegationAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
Expand Down
94 changes: 87 additions & 7 deletions test/RewardsDistributer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
StakingManager,
} from '../src';
import { DEPLOYMENT_ID, METADATA_HASH, VERSION } from './constants';
import { acceptPlan, etherParse, startNewEra, time, timeTravel } from './helper';
import { acceptPlan, addInstantRewards, etherParse, eventFrom, startNewEra, time, timeTravel } from './helper';
import { deployContracts } from './setup';

describe('RewardsDistributor Contract', () => {
Expand Down Expand Up @@ -72,10 +72,10 @@ describe('RewardsDistributor Contract', () => {
await token.connect(root).transfer(delegator.address, etherParse('10'));
await token.connect(root).transfer(delegator2.address, etherParse('10'));
await token.connect(root).transfer(consumer.address, etherParse('10'));
await token.connect(consumer).increaseAllowance(planManager.address, etherParse('10'));
await token.connect(delegator).increaseAllowance(staking.address, etherParse('10'));
await token.connect(delegator2).increaseAllowance(staking.address, etherParse('10'));
await token.connect(root).increaseAllowance(rewardsDistributor.address, etherParse('10'));
await token.connect(consumer).increaseAllowance(planManager.address, etherParse('10000'));
await token.connect(delegator).increaseAllowance(staking.address, etherParse('10000'));
await token.connect(delegator2).increaseAllowance(staking.address, etherParse('10000'));
await token.connect(root).increaseAllowance(rewardsDistributor.address, etherParse('10000'));

//setup era period be 5 days
await eraManager.connect(root).updateEraPeriod(time.duration.days(5).toString());
Expand Down Expand Up @@ -131,6 +131,86 @@ describe('RewardsDistributor Contract', () => {
});
});

describe('Capped Rewards', async () => {
beforeEach(async () => {
await token.connect(root).transfer(runner.address, etherParse('10000'));
await token.connect(root).transfer(delegator.address, etherParse('10000'));
await token.connect(root).transfer(consumer.address, etherParse('10000'));
await stakingManager.connect(delegator).delegate(runner.address, etherParse(9000));
await startNewEra(eraManager);
await rewardsHelper.connect(runner).indexerCatchup(runner.address);
});
it('receive capped rewards', async () => {
// self stake 1000 SQT
// delegation: 9000 SQT
// commission rate: 5%
// reward cap: 10%
// commission cap: 10%
// arrival rewards: 1500 SQT
await rewardsDistributor.setMaxCommissionFactor(5e4);
await rewardsDistributor.setMaxRewardFactor(1e5);
const totalStake = await stakingManager.getTotalStakingAmount(runner.address);
expect(totalStake).to.eq(etherParse(10000));
const selfStake = await stakingManager.getDelegationAmount(runner.address, runner.address);
expect(selfStake).to.eq(etherParse(1000));
const arrivalReward = etherParse(1500);
const era = await eraManager.eraNumber();
await addInstantRewards(token, rewardsDistributor, consumer, runner.address, era, arrivalReward);
await startNewEra(eraManager);
const tx = await rewardsDistributor.connect(runner).collectAndDistributeRewards(runner.address);
const distributedRewards = await eventFrom(
tx,
rewardsDistributor,
'DistributeRewards(address,uint256,uint256,uint256)'
);
expect(distributedRewards.rewards).to.eq(etherParse(1000));
expect(distributedRewards.commission).to.eq(etherParse(50));
const returnRewards = await eventFrom(tx, rewardsDistributor, 'ReturnRewards(address,uint256,uint256)');
expect(returnRewards.rewards).to.eq(etherParse(500));
expect(returnRewards.commission).to.eq(etherParse(100));
});
it('rewards after capped may become zero', async () => {
// self stake 9000 SQT
// delegation: 9000 SQT
// commission rate: 30%
// reward cap: 10%
// commission cap: 25%
// arrival rewards: 10000 SQT
// commission: 3000 SQT
// capped commission: 2250 SQT
// capped reward: 1800 SQT
await rewardsDistributor.setMaxCommissionFactor(2.5e5);
await rewardsDistributor.setMaxRewardFactor(1e5);
await token.connect(runner).increaseAllowance(staking.address, etherParse(8000));
await stakingManager.connect(runner).stake(runner.address, etherParse(8000));
await indexerRegistry.connect(runner).setCommissionRate(3e5);
await startNewEra(eraManager);
await rewardsHelper.connect(runner).indexerCatchup(runner.address);
await startNewEra(eraManager);
await rewardsHelper.connect(runner).indexerCatchup(runner.address);

const totalStake = await stakingManager.getTotalStakingAmount(runner.address);
expect(totalStake).to.eq(etherParse(18000));
const selfStake = await stakingManager.getDelegationAmount(runner.address, runner.address);
expect(selfStake).to.eq(etherParse(9000));
const arrivalReward = etherParse(10000);
const era = await eraManager.eraNumber();
await addInstantRewards(token, rewardsDistributor, consumer, runner.address, era, arrivalReward);
await startNewEra(eraManager);
const tx = await rewardsDistributor.connect(runner).collectAndDistributeRewards(runner.address);
const distributedRewards = await eventFrom(
tx,
rewardsDistributor,
'DistributeRewards(address,uint256,uint256,uint256)'
);
expect(distributedRewards.rewards).to.eq(etherParse(2250)); // exclude commission
expect(distributedRewards.commission).to.eq(etherParse(2250));
const returnRewards = await eventFrom(tx, rewardsDistributor, 'ReturnRewards(address,uint256,uint256)');
expect(returnRewards.rewards).to.eq(etherParse(7750));
expect(returnRewards.commission).to.eq(etherParse(750));
});
});

describe('distribute and claim rewards', async () => {
beforeEach(async () => {
//a 30 days agreement with 400 rewards come in at Era2
Expand All @@ -144,14 +224,14 @@ describe('RewardsDistributor Contract', () => {
expect((await rewardsHelper.getRewardsAddTable(runner.address, 2, 1))[0]).to.be.eq(etherParse('0'));
expect((await rewardsHelper.getRewardsRemoveTable(runner.address, 2, 1))[0]).to.be.eq(etherParse('0'));
await rewardsDistributor.connect(runner).claim(runner.address);

rewards = (await token.balanceOf(runner.address)).div(1e14);
//move to Era 4
await startNewEra(eraManager);
await rewardsDistributor.collectAndDistributeRewards(runner.address);
expect((await rewardsHelper.getRewardsAddTable(runner.address, 3, 1))[0]).to.be.eq(etherParse('0'));
expect((await rewardsHelper.getRewardsRemoveTable(runner.address, 3, 1))[0]).to.be.eq(etherParse('0'));
await rewardsDistributor.connect(runner).claim(runner.address);

rewards = (await token.balanceOf(runner.address)).div(1e14);
//move to Era 5
await startNewEra(eraManager);
await rewardsDistributor.collectAndDistributeRewards(runner.address);
Expand Down
Loading