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

Fees #314

Merged
merged 10 commits into from
Apr 3, 2024
128 changes: 128 additions & 0 deletions core/contracts/lib/ERC4626Fees.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT

// Inspired by https://docs.openzeppelin.com/contracts/5.x/erc4626#fees

pragma solidity ^0.8.21;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

/// @dev ERC4626 vault with entry/exit fees expressed in https://en.wikipedia.org/wiki/Basis_point[basis point (bp)].
abstract contract ERC4626Fees is ERC4626Upgradeable {
using Math for uint256;

uint256 private constant _BASIS_POINT_SCALE = 1e4;

// === Overrides ===

/// @dev Preview taking an entry fee on deposit. See {IERC4626-previewDeposit}.
function previewDeposit(
uint256 assets
) public view virtual override returns (uint256) {
uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints());
return super.previewDeposit(assets - fee);
}

/// @dev Preview adding an entry fee on mint. See {IERC4626-previewMint}.
function previewMint(
uint256 shares
) public view virtual override returns (uint256) {
uint256 assets = super.previewMint(shares);
return assets + _feeOnRaw(assets, _entryFeeBasisPoints());
}

/// @dev Preview adding an exit fee on withdraw. See {IERC4626-previewWithdraw}.
function previewWithdraw(
uint256 assets
) public view virtual override returns (uint256) {
uint256 fee = _feeOnRaw(assets, _exitFeeBasisPoints());
return super.previewWithdraw(assets + fee);
}

/// @dev Preview taking an exit fee on redeem. See {IERC4626-previewRedeem}.
function previewRedeem(
uint256 shares
) public view virtual override returns (uint256) {
uint256 assets = super.previewRedeem(shares);
return assets - _feeOnTotal(assets, _exitFeeBasisPoints());
}

/// @dev Send entry fee to {_feeRecipient}. See {IERC4626-_deposit}.
function _deposit(
address caller,
address receiver,
uint256 assets,
uint256 shares
) internal virtual override {
uint256 fee = _feeOnTotal(assets, _entryFeeBasisPoints());
address recipient = _feeRecipient();

super._deposit(caller, receiver, assets, shares);

if (fee > 0 && recipient != address(this)) {
SafeERC20.safeTransfer(IERC20(asset()), recipient, fee);
}
}

/// @dev Send exit fee to {_exitFeeRecipient}. See {IERC4626-_deposit}.
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal virtual override {
uint256 fee = _feeOnRaw(assets, _exitFeeBasisPoints());
address recipient = _feeRecipient();

super._withdraw(caller, receiver, owner, assets, shares);

if (fee > 0 && recipient != address(this)) {
SafeERC20.safeTransfer(IERC20(asset()), recipient, fee);
}
}

// === Fee configuration ===

// slither-disable-next-line dead-code
function _entryFeeBasisPoints() internal view virtual returns (uint256);

// slither-disable-next-line dead-code
function _exitFeeBasisPoints() internal view virtual returns (uint256);

// slither-disable-next-line dead-code
function _feeRecipient() internal view virtual returns (address);

// === Fee operations ===

/// @dev Calculates the fees that should be added to an amount `assets`
/// that does not already include fees.
/// Used in {IERC4626-mint} and {IERC4626-withdraw} operations.
function _feeOnRaw(
uint256 assets,
uint256 feeBasisPoints
) private pure returns (uint256) {
return
assets.mulDiv(
feeBasisPoints,
_BASIS_POINT_SCALE,
Math.Rounding.Ceil
);
}

/// @dev Calculates the fee part of an amount `assets` that already includes fees.
/// Used in {IERC4626-deposit} and {IERC4626-redeem} operations.
function _feeOnTotal(
uint256 assets,
uint256 feeBasisPoints
) private pure returns (uint256) {
return
assets.mulDiv(
feeBasisPoints,
feeBasisPoints + _BASIS_POINT_SCALE,
Math.Rounding.Ceil
);
}
}
90 changes: 77 additions & 13 deletions core/contracts/stBTC.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.21;

import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";

import "./Dispatcher.sol";
import "./lib/ERC4626Fees.sol";

/// @title stBTC
/// @notice This contract implements the ERC-4626 tokenized vault standard. By
Expand All @@ -18,7 +18,7 @@ import "./Dispatcher.sol";
/// 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.
contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
contract stBTC is ERC4626Fees, Ownable2StepUpgradeable {
using SafeERC20 for IERC20;

/// Dispatcher contract that routes tBTC from stBTC to a given vault and back.
Expand All @@ -37,6 +37,12 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
/// Maximum total amount of tBTC token held by Acre protocol.
uint256 public maximumTotalAssets;

/// Entry fee basis points applied to entry fee calculation.
uint256 public entryFeeBasisPoints;

/// Exit fee basis points applied to exit fee calculation.
uint256 public exitFeeBasisPoints;

/// Emitted when the treasury wallet address is updated.
/// @param treasury New treasury wallet address.
event TreasuryUpdated(address treasury);
Expand All @@ -54,6 +60,14 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
/// @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);

/// Reverts if the amount is less than the minimum deposit amount.
/// @param amount Amount to check.
/// @param min Minimum amount to check 'amount' against.
Expand Down Expand Up @@ -84,6 +98,8 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
// TODO: Revisit the exact values closer to the launch.
minimumDepositAmount = 0.001 * 1e18; // 0.001 tBTC
maximumTotalAssets = 25 * 1e18; // 25 tBTC
entryFeeBasisPoints = 0; // TODO: tbd
exitFeeBasisPoints = 0; // TODO: tbd
}

/// @notice Updates treasury wallet address.
Expand Down Expand Up @@ -151,15 +167,40 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
IERC20(asset()).forceApprove(address(dispatcher), type(uint256).max);
}

// TODO: Implement a governed upgrade process that initiates an update and
// then finalizes it after a delay.
/// @notice Update the entry fee basis points.
/// @param newEntryFeeBasisPoints New value of the fee basis points.
function updateEntryFeeBasisPoints(
uint256 newEntryFeeBasisPoints
) external onlyOwner {
entryFeeBasisPoints = newEntryFeeBasisPoints;

emit EntryFeeBasisPointsUpdated(newEntryFeeBasisPoints);
}

// TODO: Implement a governed upgrade process that initiates an update and
// then finalizes it after a delay.
/// @notice Update the exit fee basis points.
/// @param newExitFeeBasisPoints New value of the fee basis points.
function updateExitFeeBasisPoints(
uint256 newExitFeeBasisPoints
) external onlyOwner {
exitFeeBasisPoints = newExitFeeBasisPoints;

emit ExitFeeBasisPointsUpdated(newExitFeeBasisPoints);
}

/// @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.
/// @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.
/// @return Minted shares adjusted for the fees taken by the treasury.
function deposit(
uint256 assets,
address receiver
Expand All @@ -176,11 +217,15 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
/// 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 tBTC transfer.
/// 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
Expand All @@ -202,22 +247,25 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
/// deposited into the vault for the receiver through a deposit
/// call. It takes into account the deposit parameter, maximum total
/// assets, which determines the total amount of tBTC token held by
/// Acre protocol.
/// @dev When the remaining amount of unused limit is less than the minimum
/// deposit amount, this function returns 0.
/// Acre. This function always returns available limit for deposits,
/// but the fee is not taken into account. As a result of this, there
/// always will be some dust left. If the dust is lower than the
/// minimum deposit amount, this function will return 0.
/// @return The maximum amount of tBTC token that can be deposited into
/// Acre protocol for the receiver.
function maxDeposit(address) public view override returns (uint256) {
if (maximumTotalAssets == type(uint256).max) {
return type(uint256).max;
}

uint256 _totalAssets = totalAssets();
uint256 currentTotalAssets = totalAssets();
if (currentTotalAssets >= maximumTotalAssets) return 0;

return
_totalAssets >= maximumTotalAssets
? 0
: maximumTotalAssets - _totalAssets;
// Max amount left for next deposits. If it is lower than the minimum
// deposit amount, return 0.
uint256 unusedLimit = maximumTotalAssets - currentTotalAssets;

return minimumDepositAmount > unusedLimit ? 0 : unusedLimit;
}

/// @notice Returns the maximum amount of the vault shares that can be
Expand All @@ -239,4 +287,20 @@ contract stBTC is ERC4626Upgradeable, Ownable2StepUpgradeable {
function depositParameters() public view returns (uint256, uint256) {
return (minimumDepositAmount, maximumTotalAssets);
}

/// @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;
}
}
6 changes: 6 additions & 0 deletions core/contracts/test/upgrades/stBTCV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ contract stBTCV2 is ERC4626Upgradeable, Ownable2StepUpgradeable {
/// Maximum total amount of tBTC token held by Acre protocol.
uint256 public maximumTotalAssets;

/// Entry fee basis points applied to entry fee calculation.
uint256 public entryFeeBasisPoints;

/// Exit fee basis points applied to exit fee calculation.
uint256 public exitFeeBasisPoints;

// TEST: New variable.
uint256 public newVariable;

Expand Down
Loading
Loading