Skip to content

Commit 7cb8dd5

Browse files
committed
add rewards distributor contracts
1 parent ff79324 commit 7cb8dd5

13 files changed

+914
-25
lines changed

contracts/Minter.sol

+6
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,10 @@ contract Minter is IMinter {
102102
veRate = _rate;
103103
emit VeRateChanged(_rate);
104104
}
105+
106+
/// @notice Withdraw fund
107+
function withdraw(address payable _recipcient, uint256 _amount) external {
108+
if (msg.sender != team) revert NotTeam();
109+
_recipcient.transfer(_amount);
110+
}
105111
}

contracts/RewardsDistributor.sol

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
5+
import {IRewardsDistributor} from "./interfaces/IRewardsDistributor.sol";
6+
import {IVotingEscrow} from "./interfaces/IVotingEscrow.sol";
7+
import {IMinter} from "./interfaces/IMinter.sol";
8+
9+
/*
10+
* @title Curve Fee Distribution modified for ve(3,3) emissions
11+
* @author Curve Finance, andrecronje
12+
* @author velodrome.finance, @figs999, @pegahcarter
13+
* @license MIT
14+
*/
15+
contract RewardsDistributor is IRewardsDistributor {
16+
/// @inheritdoc IRewardsDistributor
17+
uint256 public constant WEEK = 7 * 86400;
18+
19+
/// @inheritdoc IRewardsDistributor
20+
uint256 public startTime;
21+
/// @inheritdoc IRewardsDistributor
22+
mapping(uint256 => uint256) public timeCursorOf;
23+
24+
/// @inheritdoc IRewardsDistributor
25+
uint256 public lastTokenTime;
26+
uint256[1000000000000000] public tokensPerWeek;
27+
28+
/// @inheritdoc IRewardsDistributor
29+
IVotingEscrow public immutable ve;
30+
/// @inheritdoc IRewardsDistributor
31+
address public minter;
32+
/// @inheritdoc IRewardsDistributor
33+
uint256 public tokenLastBalance;
34+
35+
constructor(address _ve) {
36+
uint256 _t = (block.timestamp / WEEK) * WEEK;
37+
startTime = _t;
38+
lastTokenTime = _t;
39+
ve = IVotingEscrow(_ve);
40+
minter = msg.sender;
41+
}
42+
43+
receive() external payable {}
44+
45+
function _checkpointToken() internal {
46+
uint256 tokenBalance = address(this).balance;
47+
uint256 toDistribute = tokenBalance - tokenLastBalance;
48+
tokenLastBalance = tokenBalance;
49+
50+
uint256 t = lastTokenTime;
51+
uint256 sinceLast = block.timestamp - t;
52+
lastTokenTime = block.timestamp;
53+
uint256 thisWeek = (t / WEEK) * WEEK;
54+
uint256 nextWeek = 0;
55+
uint256 timestamp = block.timestamp;
56+
57+
for (uint256 i = 0; i < 20; i++) {
58+
nextWeek = thisWeek + WEEK;
59+
if (timestamp < nextWeek) {
60+
if (sinceLast == 0 && timestamp == t) {
61+
tokensPerWeek[thisWeek] += toDistribute;
62+
} else {
63+
tokensPerWeek[thisWeek] += (toDistribute * (timestamp - t)) / sinceLast;
64+
}
65+
break;
66+
} else {
67+
if (sinceLast == 0 && nextWeek == t) {
68+
tokensPerWeek[thisWeek] += toDistribute;
69+
} else {
70+
tokensPerWeek[thisWeek] += (toDistribute * (nextWeek - t)) / sinceLast;
71+
}
72+
}
73+
t = nextWeek;
74+
thisWeek = nextWeek;
75+
}
76+
emit CheckpointToken(timestamp, toDistribute);
77+
}
78+
79+
/// @inheritdoc IRewardsDistributor
80+
function checkpointToken() external {
81+
if (msg.sender != minter) revert NotMinter();
82+
_checkpointToken();
83+
}
84+
85+
function _claim(uint256 _tokenId, uint256 _lastTokenTime) internal returns (uint256) {
86+
(uint256 toDistribute, uint256 epochStart, uint256 weekCursor) = _claimable(_tokenId, _lastTokenTime);
87+
timeCursorOf[_tokenId] = weekCursor;
88+
if (toDistribute == 0) return 0;
89+
90+
emit Claimed(_tokenId, epochStart, weekCursor, toDistribute);
91+
return toDistribute;
92+
}
93+
94+
function _claimable(
95+
uint256 _tokenId,
96+
uint256 _lastTokenTime
97+
) internal view returns (uint256 toDistribute, uint256 weekCursorStart, uint256 weekCursor) {
98+
uint256 _startTime = startTime;
99+
weekCursor = timeCursorOf[_tokenId];
100+
weekCursorStart = weekCursor;
101+
102+
// case where token does not exist
103+
uint256 maxUserEpoch = ve.userPointEpoch(_tokenId);
104+
if (maxUserEpoch == 0) return (0, weekCursorStart, weekCursor);
105+
106+
// case where token exists but has never been claimed
107+
if (weekCursor == 0) {
108+
IVotingEscrow.UserPoint memory userPoint = ve.userPointHistory(_tokenId, 1);
109+
weekCursor = (userPoint.ts / WEEK) * WEEK;
110+
weekCursorStart = weekCursor;
111+
}
112+
if (weekCursor >= _lastTokenTime) return (0, weekCursorStart, weekCursor);
113+
if (weekCursor < _startTime) weekCursor = _startTime;
114+
115+
for (uint256 i = 0; i < 50; i++) {
116+
if (weekCursor >= _lastTokenTime) break;
117+
118+
uint256 balance = ve.balanceOfNFTAt(_tokenId, weekCursor + WEEK - 1);
119+
uint256 supply = ve.totalSupplyAt(weekCursor + WEEK - 1);
120+
supply = supply == 0 ? 1 : supply;
121+
toDistribute += (balance * tokensPerWeek[weekCursor]) / supply;
122+
weekCursor += WEEK;
123+
}
124+
}
125+
126+
/// @inheritdoc IRewardsDistributor
127+
function claimable(uint256 _tokenId) external view returns (uint256 claimable_) {
128+
uint256 _lastTokenTime = (lastTokenTime / WEEK) * WEEK;
129+
(claimable_, , ) = _claimable(_tokenId, _lastTokenTime);
130+
}
131+
132+
/// @inheritdoc IRewardsDistributor
133+
function claim(uint256 _tokenId) external returns (uint256) {
134+
if (IMinter(minter).activePeriod() < ((block.timestamp / WEEK) * WEEK)) revert UpdatePeriod();
135+
uint256 _timestamp = block.timestamp;
136+
uint256 _lastTokenTime = lastTokenTime;
137+
_lastTokenTime = (_lastTokenTime / WEEK) * WEEK;
138+
uint256 amount = _claim(_tokenId, _lastTokenTime);
139+
if (amount != 0) {
140+
IVotingEscrow.LockedBalance memory _locked = ve.locked(_tokenId);
141+
if ((_timestamp >= _locked.end && !_locked.isPermanent) || ve.lockedToken(_tokenId) != address(0)) {
142+
address _owner = ve.ownerOf(_tokenId);
143+
payable(_owner).transfer(amount);
144+
} else {
145+
ve.depositFor{value: amount}(_tokenId, amount);
146+
}
147+
tokenLastBalance -= amount;
148+
}
149+
return amount;
150+
}
151+
152+
/// @inheritdoc IRewardsDistributor
153+
function claimMany(uint256[] calldata _tokenIds) external returns (bool) {
154+
if (IMinter(minter).activePeriod() < ((block.timestamp / WEEK) * WEEK)) revert UpdatePeriod();
155+
uint256 _timestamp = block.timestamp;
156+
uint256 _lastTokenTime = lastTokenTime;
157+
_lastTokenTime = (_lastTokenTime / WEEK) * WEEK;
158+
uint256 total = 0;
159+
uint256 _length = _tokenIds.length;
160+
161+
for (uint256 i = 0; i < _length; i++) {
162+
uint256 _tokenId = _tokenIds[i];
163+
if (_tokenId == 0) break;
164+
uint256 amount = _claim(_tokenId, _lastTokenTime);
165+
if (amount != 0) {
166+
IVotingEscrow.LockedBalance memory _locked = ve.locked(_tokenId);
167+
if ((_timestamp >= _locked.end && !_locked.isPermanent) || ve.lockedToken(_tokenId) != address(0)) {
168+
address _owner = ve.ownerOf(_tokenId);
169+
payable(_owner).transfer(amount);
170+
} else {
171+
ve.depositFor{value: amount}(_tokenId, amount);
172+
}
173+
total += amount;
174+
}
175+
}
176+
if (total != 0) {
177+
tokenLastBalance -= total;
178+
}
179+
180+
return true;
181+
}
182+
183+
/// @inheritdoc IRewardsDistributor
184+
function setMinter(address _minter) external {
185+
if (msg.sender != minter) revert NotMinter();
186+
minter = _minter;
187+
}
188+
}

contracts/interfaces/IRewardsDistributor.sol

-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ interface IRewardsDistributor {
2626
/// @notice Interface of VotingEscrow.sol
2727
function ve() external view returns (IVotingEscrow);
2828

29-
/// @notice Address of token used for distributions (AERO)
30-
function token() external view returns (address);
31-
3229
/// @notice Address of Minter.sol
3330
/// Authorized caller of checkpointToken()
3431
function minter() external view returns (address);

src/types/contracts/Minter.ts

+19
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface MinterInterface extends Interface {
4141
| "veRate"
4242
| "voter"
4343
| "weekly"
44+
| "withdraw"
4445
): FunctionFragment;
4546

4647
getEvent(
@@ -93,6 +94,10 @@ export interface MinterInterface extends Interface {
9394
encodeFunctionData(functionFragment: "veRate", values?: undefined): string;
9495
encodeFunctionData(functionFragment: "voter", values?: undefined): string;
9596
encodeFunctionData(functionFragment: "weekly", values?: undefined): string;
97+
encodeFunctionData(
98+
functionFragment: "withdraw",
99+
values: [AddressLike, BigNumberish]
100+
): string;
96101

97102
decodeFunctionResult(functionFragment: "WEEK", data: BytesLike): Result;
98103
decodeFunctionResult(functionFragment: "acceptTeam", data: BytesLike): Result;
@@ -127,6 +132,7 @@ export interface MinterInterface extends Interface {
127132
decodeFunctionResult(functionFragment: "veRate", data: BytesLike): Result;
128133
decodeFunctionResult(functionFragment: "voter", data: BytesLike): Result;
129134
decodeFunctionResult(functionFragment: "weekly", data: BytesLike): Result;
135+
decodeFunctionResult(functionFragment: "withdraw", data: BytesLike): Result;
130136
}
131137

132138
export namespace AcceptTeamEvent {
@@ -259,6 +265,12 @@ export interface Minter extends BaseContract {
259265

260266
weekly: TypedContractMethod<[], [bigint], "view">;
261267

268+
withdraw: TypedContractMethod<
269+
[_recipcient: AddressLike, _amount: BigNumberish],
270+
[void],
271+
"nonpayable"
272+
>;
273+
262274
getFunction<T extends ContractMethod = ContractMethod>(
263275
key: string | FunctionFragment
264276
): T;
@@ -306,6 +318,13 @@ export interface Minter extends BaseContract {
306318
getFunction(
307319
nameOrSignature: "weekly"
308320
): TypedContractMethod<[], [bigint], "view">;
321+
getFunction(
322+
nameOrSignature: "withdraw"
323+
): TypedContractMethod<
324+
[_recipcient: AddressLike, _amount: BigNumberish],
325+
[void],
326+
"nonpayable"
327+
>;
309328

310329
getEvent(
311330
key: "AcceptTeam"

0 commit comments

Comments
 (0)