1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309 |
73×
72×
72×
72×
31×
52×
6×
2×
60×
59×
59×
59×
57×
57×
57×
57×
57×
57×
57×
57×
57×
15×
2×
2×
19×
18×
18×
18×
16×
16×
16×
16×
16×
16×
16×
16×
16×
16×
16×
16×
2×
2×
2×
2×
9×
9×
8×
8×
8×
8×
6×
6×
6×
6×
6×
46×
20×
20×
43×
43×
2×
41×
92×
92×
90×
90×
9×
9×
8×
8×
8×
8×
8×
8×
8×
97×
97×
97×
97×
97×
97×
97×
97×
199×
199×
199×
199×
199×
84×
84×
84×
84×
84×
199×
| // SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./vendor/CometInterface.sol";
import "./WrappedERC20.sol";
import "./vendor/ICometRewards.sol";
import "./ICusdcV3Wrapper.sol";
import "./CometHelpers.sol";
/**
* @title CusdcV3Wrapper
* @notice Wrapper for cUSDCV3 / COMET that acts as a stable-balance ERC20, instead of rebasing
* token. {comet} will be used as the unit for the underlying token, and {wComet} will be used
* as the unit for wrapped tokens.
*/
contract CusdcV3Wrapper is ICusdcV3Wrapper, WrappedERC20, CometHelpers {
using SafeERC20 for IERC20;
/// From cUSDCv3, used in principal <> present calculations
uint256 public constant TRACKING_INDEX_SCALE = 1e15;
/// From cUSDCv3, scaling factor for USDC rewards
uint64 public constant RESCALE_FACTOR = 1e12;
CometInterface public immutable underlyingComet;
ICometRewards public immutable rewardsAddr;
IERC20 public immutable rewardERC20;
mapping(address => uint64) public baseTrackingIndex;
mapping(address => uint64) public baseTrackingAccrued;
mapping(address => uint256) public rewardsClaimed;
constructor(
address cusdcv3,
address rewardsAddr_,
address rewardERC20_
) WrappedERC20("Wrapped cUSDCv3", "wcUSDCv3") {
if (cusdcv3 == address(0)) revert ZeroAddress();
rewardsAddr = ICometRewards(rewardsAddr_);
rewardERC20 = IERC20(rewardERC20_);
underlyingComet = CometInterface(cusdcv3);
}
/// @return number of decimals
function decimals() public pure override returns (uint8) {
return 6;
}
/// @param amount {Comet} The amount of cUSDCv3 to deposit
function deposit(uint256 amount) external {
_deposit(msg.sender, msg.sender, msg.sender, amount);
}
/// @param dst The dst to deposit into
/// @param amount {Comet} The amount of cUSDCv3 to deposit
function depositTo(address dst, uint256 amount) external {
_deposit(msg.sender, msg.sender, dst, amount);
}
/// @param src The address to deposit from
/// @param dst The address to deposit to
/// @param amount {Comet} The amount of cUSDCv3 to deposit
function depositFrom(
address src,
address dst,
uint256 amount
) external {
_deposit(msg.sender, src, dst, amount);
}
/// Only called internally to run the deposit logic
/// Takes `amount` fo cUSDCv3 from `src` and deposits to `dst` account in the wrapper.
/// @param operator The address calling the contract (msg.sender)
/// @param src The address to deposit from
/// @param dst The address to deposit to
/// @param amount {Comet} The amount of cUSDCv3 to deposit
function _deposit(
address operator,
address src,
address dst,
uint256 amount
) internal {
if (!hasPermission(src, operator)) revert Unauthorized();
// {Comet}
uint256 srcBal = underlyingComet.balanceOf(src);
if (amount > srcBal) amount = srcBal;
if (amount == 0) revert BadAmount();
underlyingComet.accrueAccount(address(this));
underlyingComet.accrueAccount(src);
CometInterface.UserBasic memory wrappedBasic = underlyingComet.userBasic(address(this));
int104 wrapperPrePrinc = wrappedBasic.principal;
IERC20(address(underlyingComet)).safeTransferFrom(src, address(this), amount);
wrappedBasic = underlyingComet.userBasic(address(this));
int104 wrapperPostPrinc = wrappedBasic.principal;
accrueAccountRewards(dst);
// safe to cast because amount is positive
_mint(dst, uint104(wrapperPostPrinc - wrapperPrePrinc));
}
/// @param amount {Comet} The amount of cUSDCv3 to withdraw
function withdraw(uint256 amount) external {
_withdraw(msg.sender, msg.sender, msg.sender, amount);
}
/// @param dst The address to withdraw cUSDCv3 to
/// @param amount {Comet} The amount of cUSDCv3 to withdraw
function withdrawTo(address dst, uint256 amount) external {
_withdraw(msg.sender, msg.sender, dst, amount);
}
/// @param src The address to withdraw from
/// @param dst The address to withdraw cUSDCv3 to
/// @param amount {Comet} The amount of cUSDCv3 to withdraw
function withdrawFrom(
address src,
address dst,
uint256 amount
) external {
_withdraw(msg.sender, src, dst, amount);
}
/// Internally called to run the withdraw logic
/// Withdraws `amount` cUSDCv3 from `src` account in the wrapper and sends to `dst`
/// @dev Rounds conservatively so as not to over-withdraw from the wrapper
/// @param operator The address calling the contract (msg.sender)
/// @param src The address to withdraw from
/// @param dst The address to withdraw cUSDCv3 to
/// @param amount {Comet} The amount of cUSDCv3 to withdraw
function _withdraw(
address operator,
address src,
address dst,
uint256 amount
) internal {
if (!hasPermission(src, operator)) revert Unauthorized();
// {Comet}
uint256 srcBalUnderlying = underlyingBalanceOf(src);
if (srcBalUnderlying < amount) amount = srcBalUnderlying;
if (amount == 0) revert BadAmount();
underlyingComet.accrueAccount(address(this));
underlyingComet.accrueAccount(src);
uint256 srcBalPre = balanceOf(src);
CometInterface.UserBasic memory wrappedBasic = underlyingComet.userBasic(address(this));
int104 wrapperPrePrinc = wrappedBasic.principal;
// conservative rounding in favor of the wrapper
IERC20(address(underlyingComet)).safeTransfer(dst, (amount / 10) * 10);
wrappedBasic = underlyingComet.userBasic(address(this));
int104 wrapperPostPrinc = wrappedBasic.principal;
// safe to cast because principal can't go negative, wrapper is not borrowing
uint256 burnAmt = uint256(uint104(wrapperPrePrinc - wrapperPostPrinc));
// occasionally comet will withdraw 1-10 wei more than we asked for.
// this is ok because 9 times out of 10 we are rounding in favor of the wrapper.
// safe because we have already capped the comet withdraw amount to src underlying bal.
Iif (srcBalPre <= burnAmt) burnAmt = srcBalPre;
accrueAccountRewards(src);
_burn(src, safe104(burnAmt));
}
/// Internally called to run transfer logic.
/// Accrues rewards for `src` and `dst` before transferring value.
function _beforeTokenTransfer(
address src,
address dst,
uint256 amount
) internal virtual override {
underlyingComet.accrueAccount(address(this));
super._beforeTokenTransfer(src, dst, amount);
accrueAccountRewards(src);
accrueAccountRewards(dst);
}
/// @param src The account to claim from
/// @param dst The address to send claimed rewards to
function claimTo(address src, address dst) public {
address sender = msg.sender;
if (!hasPermission(src, sender)) revert Unauthorized();
accrueAccount(src);
uint256 claimed = rewardsClaimed[src];
uint256 accrued = baseTrackingAccrued[src] * RESCALE_FACTOR;
if (accrued > claimed) {
uint256 owed = accrued - claimed;
rewardsClaimed[src] = accrued;
rewardsAddr.claimTo(address(underlyingComet), address(this), address(this), true);
IERC20(rewardERC20).safeTransfer(dst, owed);
emit RewardClaimed(src, dst, address(rewardERC20), owed);
}
}
/// Accure the cUSDCv3 account of the wrapper
function accrue() public {
underlyingComet.accrueAccount(address(this));
}
/// @param account The address to accrue, first in cUSDCv3, then locally
function accrueAccount(address account) public {
underlyingComet.accrueAccount(address(this));
accrueAccountRewards(account);
}
/// Get the balance of cUSDCv3 that is represented by the `accounts` wrapper value.
/// @param account The address to calculate the cUSDCv3 balance of
/// @return {Comet} The cUSDCv3 balance that `account` holds in the wrapper
function underlyingBalanceOf(address account) public view returns (uint256) {
uint256 balance = balanceOf(account);
if (balance == 0) {
return 0;
}
return convertStaticToDynamic(safe104(balance));
}
/// @return The exchange rate {comet/wComet}
function exchangeRate() public view returns (uint256) {
(uint64 baseSupplyIndex, ) = getUpdatedSupplyIndicies();
return presentValueSupply(baseSupplyIndex, safe104(10**underlyingComet.decimals()));
}
/// @param amount The value of {wComet} to convert to {Comet}
/// @return {Comet} The amount of cUSDCv3 represented by `amount of {wComet}
function convertStaticToDynamic(uint104 amount) public view returns (uint256) {
(uint64 baseSupplyIndex, ) = getUpdatedSupplyIndicies();
return presentValueSupply(baseSupplyIndex, amount);
}
/// @param amount The value of {Comet} to convert to {wComet}
/// @return {wComet} The amount of wrapped token represented by `amount` of {Comet}
function convertDynamicToStatic(uint256 amount) public view returns (uint104) {
(uint64 baseSupplyIndex, ) = getUpdatedSupplyIndicies();
return principalValueSupply(baseSupplyIndex, amount);
}
/// @param account The address to view the owed rewards of
/// @return {reward} The amount of reward tokens owed to `account`
function getRewardOwed(address account) external view returns (uint256) {
(, uint64 trackingSupplyIndex) = getUpdatedSupplyIndicies();
uint256 indexDelta = uint256(trackingSupplyIndex - baseTrackingIndex[account]);
uint256 newBaseTrackingAccrued = baseTrackingAccrued[account] +
safe64((safe104(balanceOf(account)) * indexDelta) / TRACKING_INDEX_SCALE);
uint256 claimed = rewardsClaimed[account];
uint256 accrued = newBaseTrackingAccrued * RESCALE_FACTOR;
uint256 owed = accrued > claimed ? accrued - claimed : 0;
return owed;
}
/// Internally called to get saved indicies
/// @return baseSupplyIndex_ {1} The saved baseSupplyIndex
/// @return trackingSupplyIndex_ {1} The saved trackingSupplyIndex
function getSupplyIndices()
internal
view
returns (uint64 baseSupplyIndex_, uint64 trackingSupplyIndex_)
{
TotalsBasic memory totals = underlyingComet.totalsBasic();
baseSupplyIndex_ = totals.baseSupplyIndex;
trackingSupplyIndex_ = totals.trackingSupplyIndex;
}
/// Internally called to update the account indicies and accrued rewards for a given address
/// @param account The UserBasic struct for a target address
function accrueAccountRewards(address account) internal {
uint256 accountBal = balanceOf(account);
(, uint64 trackingSupplyIndex) = getSupplyIndices();
uint256 indexDelta = uint256(trackingSupplyIndex - baseTrackingIndex[account]);
baseTrackingAccrued[account] += safe64(
(safe104(accountBal) * indexDelta) / TRACKING_INDEX_SCALE
);
baseTrackingIndex[account] = trackingSupplyIndex;
}
/// Internally called to get the updated supply indicies
/// @return {1} The current baseSupplyIndex
/// @return {1} The current trackingSupplyIndex
function getUpdatedSupplyIndicies() internal view returns (uint64, uint64) {
TotalsBasic memory totals = underlyingComet.totalsBasic();
uint40 timeDelta = uint40(block.timestamp) - totals.lastAccrualTime;
uint64 baseSupplyIndex_ = totals.baseSupplyIndex;
uint64 trackingSupplyIndex_ = totals.trackingSupplyIndex;
if (timeDelta > 0) {
uint256 baseTrackingSupplySpeed = underlyingComet.baseTrackingSupplySpeed();
uint256 utilization = underlyingComet.getUtilization();
uint256 supplyRate = underlyingComet.getSupplyRate(utilization);
baseSupplyIndex_ += safe64(mulFactor(baseSupplyIndex_, supplyRate * timeDelta));
trackingSupplyIndex_ += safe64(
divBaseWei(baseTrackingSupplySpeed * timeDelta, totals.totalSupplyBase)
);
}
return (baseSupplyIndex_, trackingSupplyIndex_);
}
}
|