Skip to content

Commit 96d7ef1

Browse files
coolhillfyang1024brkomir
authored
add staking contracts (#76)
Add sQuartz: Deposit Quartz for sQuartz and over time claim performance fees from Sandclock yield strategies. Additionally receive bonus multiplier points (inspired by GMX reward mechanism) at 100% APR which can be compounded in order to claim a bigger portion of the staking rewards. Redeem sQuartz for Quartz, which will proportionally burn some of the earned multiplier points for that account. The first 30 days of a deposit special rules apply: no additional deposits allowed and in order to withdraw/transfer a fee needs to be paid (starts at 10% of the amount and linearly decreases to zero over 30 days). --------- Co-authored-by: Fei Yang <[email protected]> Co-authored-by: brkomir <[email protected]>
1 parent f0545a4 commit 96d7ef1

7 files changed

+1523
-0
lines changed

script/DeployStakingSepolia.s.sol

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: AGPL-3.0
2+
pragma solidity ^0.8.13;
3+
4+
import {CREATE3Script} from "./base/CREATE3Script.sol";
5+
import {RewardTracker} from "../src/staking/RewardTracker.sol";
6+
import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol";
7+
8+
contract DeployScript is CREATE3Script {
9+
bytes32 public constant DISTRIBUTOR = keccak256("DISTRIBUTOR");
10+
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
11+
12+
constructor() CREATE3Script(vm.envString("VERSION")) {}
13+
14+
function run() external returns (RewardTracker sQuartz) {
15+
uint256 deployerPrivateKey = uint256(vm.envBytes32("PRIVATE_KEY"));
16+
17+
MockERC20 quartz;
18+
address WETH = 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9; // weth on sepolia
19+
20+
vm.startBroadcast(deployerPrivateKey);
21+
22+
quartz = new MockERC20("Mock Quartz", "QUARTZ", 18);
23+
sQuartz = new RewardTracker(address(msg.sender), address(quartz), "Staked Quartz", "sQuartz", WETH, 30 days);
24+
25+
vm.stopBroadcast();
26+
}
27+
}

src/staking/BonusTracker.sol

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: AGPL-3.0
2+
pragma solidity ^0.8.10;
3+
4+
import {ERC20} from "solmate/tokens/ERC20.sol";
5+
import {ERC4626} from "solmate/mixins/ERC4626.sol";
6+
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
7+
import {ReentrancyGuard} from "openzeppelin-contracts/security/ReentrancyGuard.sol";
8+
9+
abstract contract BonusTracker is ERC4626, ReentrancyGuard {
10+
using FixedPointMathLib for uint256;
11+
12+
event BonusPaid(address indexed user, uint256 bonus);
13+
event BonusBurned(address indexed user, uint256 bonus);
14+
15+
uint256 internal constant PRECISION = 1e30;
16+
17+
/// @notice The last Unix timestamp (in seconds) when bonusPerTokenStored was updated
18+
uint64 public lastBonusUpdateTime;
19+
20+
/// @notice The last stored bonusPerToken value
21+
uint256 public bonusPerTokenStored;
22+
23+
/// @notice The total bonus amount currently held by users
24+
uint256 public totalBonus;
25+
26+
/// @notice The bonusPerToken value when an account last compounded/withdrew bonus
27+
mapping(address => uint256) public userBonusPerTokenPaid;
28+
29+
/// @notice The number of multiplier points compounded for an account
30+
mapping(address => uint256) public multiplierPointsOf;
31+
32+
/// @notice The bonusOf() value when an account last staked/withdrew bonus
33+
mapping(address => uint256) public bonus;
34+
35+
constructor(ERC20 _asset, string memory _name, string memory _symbol) ERC4626(_asset, _name, _symbol) {
36+
lastBonusUpdateTime = uint64(block.timestamp);
37+
}
38+
39+
/// @notice Claim bonus
40+
function boost() external nonReentrant returns (uint256 _bonus) {
41+
_updateReward(msg.sender);
42+
_updateBonus(msg.sender);
43+
_bonus = bonus[msg.sender];
44+
45+
if (_bonus > 0) {
46+
bonus[msg.sender] = 0;
47+
multiplierPointsOf[msg.sender] += _bonus;
48+
totalBonus += _bonus;
49+
emit BonusPaid(msg.sender, _bonus);
50+
}
51+
}
52+
53+
/// @notice The latest time at which stakers are earning bonus.
54+
function lastTimeBonusApplicable() public view returns (uint64) {
55+
return uint64(block.timestamp);
56+
}
57+
58+
/// @notice The amount of bonus tokens each staked token has earned so far
59+
function bonusPerToken() external view returns (uint256) {
60+
return _bonusPerToken(lastTimeBonusApplicable());
61+
}
62+
63+
/// @notice The amount of bonus tokens an account has accrued so far.
64+
function bonusOf(address _account) external view returns (uint256) {
65+
return _earnedBonus(_account, balanceOf[_account], _bonusPerToken(lastTimeBonusApplicable()), bonus[_account]);
66+
}
67+
68+
function _earnedBonus(address account, uint256 accountBalance, uint256 bonusPerToken_, uint256 accountBonus)
69+
internal
70+
view
71+
returns (uint256)
72+
{
73+
return accountBalance.mulDivDown(bonusPerToken_ - userBonusPerTokenPaid[account], PRECISION) + accountBonus;
74+
}
75+
76+
function _bonusPerToken(uint256 lastTimeBonusApplicable_) internal view returns (uint256) {
77+
return bonusPerTokenStored + (lastTimeBonusApplicable_ - lastBonusUpdateTime).mulDivDown(PRECISION, 365 days);
78+
}
79+
80+
function _updateBonus(address _account) internal {
81+
// storage loads
82+
uint256 accountBalance = balanceOf[_account];
83+
uint64 lastTimeBonusApplicable_ = lastTimeBonusApplicable();
84+
uint256 bonusPerToken_ = _bonusPerToken(lastTimeBonusApplicable_);
85+
86+
// accrue bonus
87+
bonusPerTokenStored = bonusPerToken_;
88+
lastBonusUpdateTime = lastTimeBonusApplicable_;
89+
bonus[_account] = _earnedBonus(_account, accountBalance, bonusPerToken_, bonus[_account]);
90+
userBonusPerTokenPaid[_account] = bonusPerToken_;
91+
}
92+
93+
function _updateReward(address) internal virtual {}
94+
}

src/staking/DebtTracker.sol

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: AGPL-3.0
2+
pragma solidity ^0.8.10;
3+
4+
import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
5+
6+
abstract contract DebtTracker {
7+
using FixedPointMathLib for uint256;
8+
9+
event DebtAdded(address indexed user, uint256 debt, uint64 timestamp);
10+
event DebtPaid(address indexed user, uint256 debt);
11+
12+
error UserHasDebt();
13+
14+
/// @notice The initial debt for an account
15+
mapping(address => uint256) public debtOf;
16+
17+
/// @notice The debt start time for a user
18+
mapping(address => uint64) public debtStartTimeFor;
19+
20+
/// @notice The treasury address that recieves any paid debt
21+
address public treasury;
22+
23+
/// @notice The amount of current debt left for an account
24+
function debtFor(address _account) external view returns (uint256) {
25+
return _debtFor(_account);
26+
}
27+
28+
function _updateDebt(address _account) internal {
29+
// storage loads
30+
uint64 now_ = uint64(block.timestamp);
31+
uint256 startTime_ = debtStartTimeFor[_account];
32+
33+
// if 30 days has passed: eliminate debt
34+
if (now_ - startTime_ >= 30 days) {
35+
debtOf[_account] = 0;
36+
}
37+
}
38+
39+
function _debtFor(address _account) internal view returns (uint256) {
40+
// storage loads
41+
uint64 now_ = uint64(block.timestamp);
42+
uint256 startTime = debtStartTimeFor[_account];
43+
uint256 delta = now_ - startTime;
44+
uint256 debt = debtOf[_account];
45+
46+
// if 30 days passed: no debt
47+
if (delta >= 30 days) return 0;
48+
49+
// otherwise debt decreases linearly to 0 over 30 days
50+
return (debt - debt.mulDivDown(delta, 30 days));
51+
}
52+
}

0 commit comments

Comments
 (0)