-
Notifications
You must be signed in to change notification settings - Fork 2
/
stBTC.sol
552 lines (470 loc) · 21.7 KB
/
stBTC.sol
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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.24;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@thesis-co/solidity-contracts/contracts/token/IReceiveApproval.sol";
import "./PausableOwnable.sol";
import "./lib/ERC4626Fees.sol";
import "./interfaces/IDispatcher.sol";
import {ZeroAddress} from "./utils/Errors.sol";
/// @title stBTC
/// @notice This contract implements the ERC-4626 tokenized vault standard. By
/// staking tBTC, users acquire a liquid staking token called stBTC,
/// commonly referred to as "shares".
/// Users have the flexibility to redeem stBTC, enabling them to
/// withdraw their deposited tBTC along with the accrued yield.
/// @dev ERC-4626 is a standard to optimize and unify the technical parameters
/// of yield-bearing vaults. This contract facilitates the minting and
/// burning of shares (stBTC), which are represented as standard ERC20
/// tokens, providing a seamless exchange with tBTC tokens.
// slither-disable-next-line missing-inheritance
contract stBTC is ERC4626Fees, PausableOwnable {
using SafeERC20 for IERC20;
/// Dispatcher contract that routes tBTC from stBTC to a given allocation
/// contract and back.
IDispatcher public dispatcher;
/// Address of the treasury wallet, where fees should be transferred to.
address public treasury;
/// Minimum amount for a single deposit operation. The value should be set
/// low enough so the deposits routed through Bitcoin Depositor contract won't
/// be rejected. It means that minimumDepositAmount should be lower than
/// tBTC protocol's depositDustThreshold reduced by all the minting fees taken
/// before depositing in the Acre contract.
uint256 public minimumDepositAmount;
/// Entry fee basis points applied to entry fee calculation.
uint256 public entryFeeBasisPoints;
/// Exit fee basis points applied to exit fee calculation.
uint256 public exitFeeBasisPoints;
/// @notice Returns the maximum amount of the underlying asset for which the
/// shares can be minted without the coverage in deposited assets.
mapping(address => uint256) public allowedDebt;
/// @notice Returns the current debt of the debtor.
mapping(address => uint256) public currentDebt;
/// @notice Total amount of debt across all debtors.
/// @dev This is the total amount of assets for which shares have been minted
/// without the coverage in deposited assets. The value is used to
/// adjust the total assets held by the vault.
uint256 public totalDebt;
/// Emitted when the treasury wallet address is updated.
/// @param oldTreasury Address of the old treasury wallet.
/// @param newTreasury Address of the new treasury wallet.
event TreasuryUpdated(address oldTreasury, address newTreasury);
/// Emitted when deposit parameters are updated.
/// @param minimumDepositAmount New value of the minimum deposit amount.
event MinimumDepositAmountUpdated(uint256 minimumDepositAmount);
/// Emitted when the dispatcher contract is updated.
/// @param oldDispatcher Address of the old dispatcher contract.
/// @param newDispatcher Address of the new dispatcher contract.
event DispatcherUpdated(address oldDispatcher, address newDispatcher);
/// Emitted when the entry fee basis points are updated.
/// @param entryFeeBasisPoints New value of the fee basis points.
event EntryFeeBasisPointsUpdated(uint256 entryFeeBasisPoints);
/// Emitted when the exit fee basis points are updated.
/// @param exitFeeBasisPoints New value of the fee basis points.
event ExitFeeBasisPointsUpdated(uint256 exitFeeBasisPoints);
/// Emitted when the maximum debt allowance of the debtor is updated.
/// @param debtor Address of the debtor.
/// @param newAllowance Maximum debt allowance of the debtor.
event DebtAllowanceUpdated(address indexed debtor, uint256 newAllowance);
/// Emitted when debt is minted.
/// @param debtor Address of the debtor.
/// @param currentDebt Current debt of the debtor.
/// @param assets Amount of assets for which debt will be taken.
/// @param shares Amount of shares minted.
event DebtMinted(
address indexed debtor,
uint256 currentDebt,
uint256 assets,
uint256 shares
);
/// Emitted when debt is repaid.
/// @param debtor Address of the debtor.
/// @param currentDebt Current debt of the debtor.
/// @param assets Amount of assets repaying the debt.
/// @param shares Amount of shares burned.
event DebtRepaid(
address indexed debtor,
uint256 currentDebt,
uint256 assets,
uint256 shares
);
/// Reverts if the amount is less than the minimum deposit amount.
/// @param amount Amount to check.
/// @param min Minimum amount to check 'amount' against.
error LessThanMinDeposit(uint256 amount, uint256 min);
/// Reverts if the address is disallowed.
error DisallowedAddress();
/// Reverts if the fee basis points exceed the maximum value.
error ExceedsMaxFeeBasisPoints();
/// Reverts if the treasury address is the same.
error SameTreasury();
/// Reverts if the dispatcher address is the same.
error SameDispatcher();
/// @notice Emitted when the debt allowance of a debtor is insufficient.
/// @dev Used in the debt minting function.
/// @param debtor Address of the debtor.
/// @param allowance Maximum debt allowance of the debtor.
/// @param needed Requested amount of debt of the debtor.
error InsufficientDebtAllowance(
address debtor,
uint256 allowance,
uint256 needed
);
/// @notice Emitted when the debt of the debtor is insufficient - the debtor
/// tries to repay more than they borrowed.
/// @dev Used in the debt repayment function.
/// @param debtor Address of the debtor.
/// @param debt Current debt of the debtor.
/// @param needed Requested amount of assets repaying the debt.
error ExcessiveDebtRepayment(address debtor, uint256 debt, uint256 needed);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(IERC20 asset, address _treasury) public initializer {
__ERC4626_init(asset);
__ERC20_init("Acre Staked Bitcoin", "stBTC");
__PausableOwnable_init(msg.sender, msg.sender);
__ERC4626NonFungibleWithdrawals_init();
if (address(_treasury) == address(0)) {
revert ZeroAddress();
}
treasury = _treasury;
minimumDepositAmount = 0.001 * 1e18; // 0.001 tBTC
entryFeeBasisPoints = 0;
exitFeeBasisPoints = 25; // 25 bps == 0.25%
}
/// @notice Updates treasury wallet address.
/// @param newTreasury New treasury wallet address.
function updateTreasury(address newTreasury) external onlyOwner {
if (newTreasury == address(0)) {
revert ZeroAddress();
}
if (newTreasury == address(this)) {
revert DisallowedAddress();
}
if (newTreasury == treasury) {
revert SameTreasury();
}
emit TreasuryUpdated(treasury, newTreasury);
treasury = newTreasury;
}
/// @notice Updates minimum deposit amount.
/// @param newMinimumDepositAmount New value of the minimum deposit amount. It
/// is the minimum amount for a single deposit operation.
function updateMinimumDepositAmount(
uint256 newMinimumDepositAmount
) external onlyOwner {
minimumDepositAmount = newMinimumDepositAmount;
emit MinimumDepositAmountUpdated(newMinimumDepositAmount);
}
/// @notice Updates the dispatcher contract and gives it an unlimited
/// allowance to transfer deposited tBTC.
/// @param newDispatcher Address of the new dispatcher contract.
function updateDispatcher(IDispatcher newDispatcher) external onlyOwner {
if (address(newDispatcher) == address(0)) {
revert ZeroAddress();
}
if (address(newDispatcher) == address(dispatcher)) {
revert SameDispatcher();
}
address oldDispatcher = address(dispatcher);
emit DispatcherUpdated(oldDispatcher, address(newDispatcher));
dispatcher = newDispatcher;
if (oldDispatcher != address(0)) {
// Setting allowance to zero for the old dispatcher
IERC20(asset()).forceApprove(oldDispatcher, 0);
}
// Setting allowance to max for the new dispatcher
IERC20(asset()).forceApprove(address(dispatcher), type(uint256).max);
}
/// @notice Update the entry fee basis points.
/// @param newEntryFeeBasisPoints New value of the fee basis points.
function updateEntryFeeBasisPoints(
uint256 newEntryFeeBasisPoints
) external onlyOwner {
if (newEntryFeeBasisPoints > _BASIS_POINT_SCALE) {
revert ExceedsMaxFeeBasisPoints();
}
entryFeeBasisPoints = newEntryFeeBasisPoints;
emit EntryFeeBasisPointsUpdated(newEntryFeeBasisPoints);
}
/// @notice Update the exit fee basis points.
/// @param newExitFeeBasisPoints New value of the fee basis points.
function updateExitFeeBasisPoints(
uint256 newExitFeeBasisPoints
) external onlyOwner {
if (newExitFeeBasisPoints > _BASIS_POINT_SCALE) {
revert ExceedsMaxFeeBasisPoints();
}
exitFeeBasisPoints = newExitFeeBasisPoints;
emit ExitFeeBasisPointsUpdated(newExitFeeBasisPoints);
}
/// @notice Calls `receiveApproval` function on spender previously approving
/// the spender to withdraw from the caller multiple times, up to
/// the `value` amount. If this function is called again, it
/// overwrites the current allowance with `value`. Reverts if the
/// approval reverted or if `receiveApproval` call on the spender
/// reverted.
/// @dev If the `value` is set to `type(uint256).max` then
/// `transferFrom` and `burnFrom` will not reduce an allowance.
/// @param spender The address which will spend the funds.
/// @param value The amount of tokens to be spent.
/// @param extraData Additional data.
/// @return True if both approval and `receiveApproval` calls succeeded.
function approveAndCall(
address spender,
uint256 value,
bytes memory extraData
) external returns (bool) {
if (approve(spender, value)) {
IReceiveApproval(spender).receiveApproval(
msg.sender,
value,
address(this),
extraData
);
return true;
}
return false;
}
/// @notice Disables non-fungible withdrawals.
function disableNonFungibleWithdrawals() external onlyOwner {
_disableNonFungibleWithdrawals();
}
/// @notice Sets the maximum debt allowance of the debtor.
/// @dev The current debt value is intentionally not checked to allow the
/// governance reduce the debt allowance in case the depositor
/// becomes risky or malicious.
/// @param debtor Address of the debtor.
/// @param newAllowance Maximum debt allowance of the debtor.
function updateDebtAllowance(
address debtor,
uint256 newAllowance
) external onlyOwner {
emit DebtAllowanceUpdated(debtor, newAllowance);
allowedDebt[debtor] = newAllowance;
}
/// @notice Mints the requested amount of shares and registers a debt in
/// asset corresponding to the minted amount of shares.
/// @dev The debt is calculated based on the current conversion
/// rate from the shares to assets.
/// @param shares The amount of shares to mint.
/// @param receiver The receiver of the shares.
/// @return assets The debt amount in asset taken for the shares minted.
function mintDebt(
uint256 shares,
address receiver
) public whenNotPaused returns (uint256 assets) {
assets = convertToAssets(shares);
// Increase the debt of the debtor.
currentDebt[msg.sender] += assets;
// Check the maximum debt allowance of the debtor.
if (currentDebt[msg.sender] > allowedDebt[msg.sender]) {
revert InsufficientDebtAllowance(
msg.sender,
allowedDebt[msg.sender],
currentDebt[msg.sender]
);
}
emit DebtMinted(msg.sender, currentDebt[msg.sender], assets, shares);
// Increase the total debt.
totalDebt += assets;
// Mint the shares to the receiver.
super._mint(receiver, shares);
return shares;
}
/// @dev This function proxies `mintDebt` call and provides compatibility
/// with Mezo IReceiptToken interface.
function mintReceipt(address to, uint256 amount) external {
mintDebt(amount, to);
}
/// @notice Repay the asset debt, fully of partially with the provided shares.
/// @dev The debt to be repaid is calculated based on the current conversion
/// rate from the shares to assets.
/// @dev The debtor has to approve the transfer of the shares. To determine
/// the asset debt that is going to be repaid, the caller can use
/// the `previewRepayDebt` function.
/// @param shares The amount of shares to return.
/// @return assets The amount of debt in asset paid off.
function repayDebt(
uint256 shares
) public whenNotPaused returns (uint256 assets) {
assets = convertToAssets(shares);
// Check the current debt of the debtor.
if (currentDebt[msg.sender] < assets) {
revert ExcessiveDebtRepayment(
msg.sender,
currentDebt[msg.sender],
assets
);
}
// Decrease the debt of the debtor.
currentDebt[msg.sender] -= assets;
emit DebtRepaid(msg.sender, currentDebt[msg.sender], assets, shares);
// Decrease the total debt.
totalDebt -= assets;
// Burn the shares from the debtor.
super._burn(msg.sender, shares);
return shares;
}
/// @notice This function proxies `repayDebt` call and provides
/// compatibility with Mezo IReceiptToken interface.
function burnReceipt(uint256 amount) external {
repayDebt(amount);
}
/// @notice Mints shares to receiver by depositing exactly amount of
/// tBTC tokens.
/// @dev Takes into account a deposit parameter, minimum deposit amount,
/// which determines the minimum amount for a single deposit operation.
/// The amount of the assets has to be pre-approved in the tBTC
/// contract.
/// @param assets Approved amount of tBTC tokens to deposit. This includes
/// treasury fees for staking tBTC.
/// @param receiver The address to which the shares will be minted.
/// @return Minted shares adjusted for the fees taken by the treasury.
function deposit(
uint256 assets,
address receiver
) public override returns (uint256) {
if (assets < minimumDepositAmount) {
revert LessThanMinDeposit(assets, minimumDepositAmount);
}
return super.deposit(assets, receiver);
}
/// @notice Mints shares to receiver by depositing tBTC tokens.
/// @dev Takes into account a deposit parameter, minimum deposit amount,
/// which determines the minimum amount for a single deposit operation.
/// The amount of the assets has to be pre-approved in the tBTC
/// contract.
/// The msg.sender is required to grant approval for the transfer of a
/// certain amount of tBTC, and in addition, approval for the associated
/// fee. Specifically, the total amount to be approved (amountToApprove)
/// should be equal to the sum of the deposited amount and the fee.
/// To determine the total assets amount necessary for approval
/// corresponding to a given share amount, use the `previewMint` function.
/// @param shares Amount of shares to mint.
/// @param receiver The address to which the shares will be minted.
/// @return assets Used assets to mint shares.
function mint(
uint256 shares,
address receiver
) public override returns (uint256 assets) {
if ((assets = super.mint(shares, receiver)) < minimumDepositAmount) {
revert LessThanMinDeposit(assets, minimumDepositAmount);
}
}
/// @notice Withdraws assets from the vault and transfers them to the
/// receiver.
/// @dev Withdraw unallocated assets first and and if not enough, then pull
/// the assets from the dispatcher.
/// @param assets Amount of assets to withdraw.
/// @param receiver The address to which the assets will be transferred.
/// @param owner The address of the owner of the shares.
function withdraw(
uint256 assets,
address receiver,
address owner
) public override returns (uint256) {
uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this));
// If there is not enough assets in stBTC to cover user withdrawals and
// withdrawal fees then pull the assets from the dispatcher.
uint256 assetsWithFees = assets + _feeOnRaw(assets, exitFeeBasisPoints);
if (assetsWithFees > currentAssetsBalance) {
dispatcher.withdraw(assetsWithFees - currentAssetsBalance);
}
return super.withdraw(assets, receiver, owner);
}
/// @notice Redeems shares for assets and transfers them to the receiver.
/// @dev Redeem unallocated assets first and and if not enough, then pull
/// the assets from the dispatcher.
/// @param shares Amount of shares to redeem.
/// @param receiver The address to which the assets will be transferred.
/// @param owner The address of the owner of the shares.
function redeem(
uint256 shares,
address receiver,
address owner
) public override returns (uint256) {
uint256 assets = convertToAssets(shares);
uint256 currentAssetsBalance = IERC20(asset()).balanceOf(address(this));
if (assets > currentAssetsBalance) {
dispatcher.withdraw(assets - currentAssetsBalance);
}
return super.redeem(shares, receiver, owner);
}
/// @notice Returns the total amount of assets held by the vault across all
/// allocations and this contract.
/// @dev The value contains virtual assets reflecting the debt minted by the
/// debtors. The debt is not backed by the deposited assets, and it is
/// used to adjust the total assets held by the vault, to allow shares
/// and assets conversion calculations.
function totalAssets() public view override returns (uint256) {
return
IERC20(asset()).balanceOf(address(this)) +
dispatcher.totalAssets() +
totalDebt;
}
/// @dev Returns the maximum amount of the underlying asset that can be
/// deposited into the Vault for the receiver, through a deposit call.
/// If the Vault is paused, returns 0.
function maxDeposit(address) public view override returns (uint256) {
if (paused()) {
return 0;
}
return type(uint256).max;
}
/// @dev Returns the maximum amount of the Vault shares that can be minted
/// for the receiver, through a mint call.
/// If the Vault is paused, returns 0.
function maxMint(address) public view override returns (uint256) {
if (paused()) {
return 0;
}
return type(uint256).max;
}
/// @dev Returns the maximum amount of the underlying asset that can be
/// withdrawn from the owner balance in the Vault, through a withdraw call.
/// If the Vault is paused, returns 0.
function maxWithdraw(address owner) public view override returns (uint256) {
if (paused()) {
return 0;
}
return super.maxWithdraw(owner);
}
/// @dev Returns the maximum amount of Vault shares that can be redeemed from
/// the owner balance in the Vault, through a redeem call.
/// If the Vault is paused, returns 0.
function maxRedeem(address owner) public view override returns (uint256) {
if (paused()) {
return 0;
}
return super.maxRedeem(owner);
}
/// @notice Returns the number of assets that corresponds to the amount of
/// shares held by the specified account.
/// @dev This function is used to convert shares to assets position for
/// the given account. It does not take fees into account.
/// @param account The owner of the shares.
/// @return The amount of assets.
function assetsBalanceOf(address account) public view returns (uint256) {
return convertToAssets(balanceOf(account));
}
/// @notice Previews the amount of assets that will be burned for the given
/// amount of repaid shares.
function previewRepayDebt(uint256 shares) public view returns (uint256) {
return convertToAssets(shares);
}
/// @return Returns entry fee basis point used in deposits.
function _entryFeeBasisPoints() internal view override returns (uint256) {
return entryFeeBasisPoints;
}
/// @return Returns exit fee basis point used in withdrawals.
function _exitFeeBasisPoints() internal view override returns (uint256) {
return exitFeeBasisPoints;
}
/// @notice Returns the address of the treasury wallet, where fees should be
/// transferred to.
function _feeRecipient() internal view override returns (address) {
return treasury;
}
}