Skip to content

Implement tBTC Bridge fees reimbursement #942

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions solidity/contracts/BitcoinDepositor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol";

import {stBTC} from "./stBTC.sol";
import {FeesReimbursementPool} from "./FeesReimbursementPool.sol";

/// @title Bitcoin Depositor contract.
/// @notice The contract integrates Acre depositing with tBTC minting.
Expand Down Expand Up @@ -76,6 +77,16 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable {
/// `1/50 = 0.02 = 2%`.
uint64 public depositorFeeDivisor;

/// @notice Fees reimbursement pool.
FeesReimbursementPool public feesReimbursementPool;

/// @notice Minimum deposit amount threshold for tBTC Bridge fees reimbursement.
/// For deposits below this threshold, the fees will be reimbursed
/// from the fees reimbursement pool.
/// @dev If the threshold is set to 0, the fees reimbursement is disabled.
/// The threshold is in tBTC token precision.
uint256 public bridgeFeesReimbursementThreshold;

/// @notice Emitted when a deposit is initialized.
/// @dev Deposit details can be fetched from {{ Bridge.DepositRevealed }}
/// event emitted in the same transaction.
Expand Down Expand Up @@ -117,6 +128,13 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable {
/// @param depositorFeeDivisor New value of the depositor fee divisor.
event DepositorFeeDivisorUpdated(uint64 depositorFeeDivisor);

/// @notice Emitted when a tBTC Bridge fees reimbursement threshold is updated.
/// @param bridgeFeesReimbursementThreshold New value of the tBTC Bridge fees
/// reimbursement threshold.
event BridgeFeesReimbursementThresholdUpdated(
uint256 bridgeFeesReimbursementThreshold
);

/// Reverts if the tBTC Token address is zero.
error TbtcTokenZeroAddress();

Expand Down Expand Up @@ -177,6 +195,9 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable {

minDepositAmount = 0.015 * 1e18; // 0.015 BTC
depositorFeeDivisor = 1000; // 1/1000 == 10bps == 0.1% == 0.001

// Disable fees reimbursement by default.
bridgeFeesReimbursementThreshold = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we deploying a new BitcoinDepositor contract or upgrading the existing one? For this line it does not make a lot of difference as the default value is 0 but I want to understand the flow.

}

/// @notice This function allows depositing process initialization for a Bitcoin
Expand Down Expand Up @@ -258,6 +279,21 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable {
bytes32 extraData
) = _finalizeDeposit(depositKey);

if (
bridgeFeesReimbursementThreshold > 0 &&
initialAmount < bridgeFeesReimbursementThreshold
) {
uint256 tbtcBridgeFee = initialAmount - tbtcAmount;

if (tbtcBridgeFee > 0) {
uint256 reimbursedAmount = feesReimbursementPool.reimburse(
tbtcBridgeFee
);

tbtcAmount += reimbursedAmount;
}
}

// Compute depositor fee. The fee is calculated based on the initial funding
// transaction amount, before the tBTC protocol network fees were taken.
uint256 depositorFee = depositorFeeDivisor > 0
Expand Down Expand Up @@ -325,6 +361,19 @@ contract BitcoinDepositor is AbstractTBTCDepositor, Ownable2StepUpgradeable {
emit DepositorFeeDivisorUpdated(newDepositorFeeDivisor);
}

/// @notice Updates the tBTC Bridge fees reimbursement threshold.
/// @param newBridgeFeesReimbursementThreshold New value of the tBTC Bridge fees
/// reimbursement threshold.
function updateBridgeFeesReimbursementThreshold(
uint256 newBridgeFeesReimbursementThreshold
) external onlyOwner {
bridgeFeesReimbursementThreshold = newBridgeFeesReimbursementThreshold;

emit BridgeFeesReimbursementThresholdUpdated(
newBridgeFeesReimbursementThreshold
);
}

/// @notice Encodes deposit owner address and referral as extra data.
/// @dev Packs the data to bytes32: 20 bytes of deposit owner address and
/// 2 bytes of referral, 10 bytes of trailing zeros.
Expand Down
68 changes: 68 additions & 0 deletions solidity/contracts/FeesReimbursementPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.24;

import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title Fees Reimbursement Pool
/// @notice A contract that allows the Bitcoin Depositor to reimburse fees
/// for deposits.
contract FeesReimbursementPool is Ownable2StepUpgradeable {
using SafeERC20 for IERC20;

address public tbtcToken;
address public bitcoinDepositor;

/// @dev Caller is not the Bitcoin Depositor contract.
error CallerNotBitcoinDepositor();

/// @dev Attempted to reimburse zero amount.
error ZeroAmount();

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @notice Initialize the fees reimbursement pool.
/// @param _tbtcToken The tBTC token address.
/// @param _bitcoinDepositor The bitcoin depositor address.
function initialize(
address _tbtcToken,
address _bitcoinDepositor
) external initializer {
__Ownable2Step_init();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be good not calling it at all, given it does nothing, but if you want to make it more future-proof, this could make sense.

__Ownable_init(msg.sender);

tbtcToken = _tbtcToken;
bitcoinDepositor = _bitcoinDepositor;
}

/// @notice Reimburse the fees.
/// @param reimbursedAmount The amount to reimburse.
/// @return The amount reimbursed.
function reimburse(uint256 reimbursedAmount) external returns (uint256) {
if (msg.sender != bitcoinDepositor) revert CallerNotBitcoinDepositor();
if (reimbursedAmount == 0) revert ZeroAmount();

uint256 availableBalance = IERC20(tbtcToken).balanceOf(address(this));

if (availableBalance < reimbursedAmount) {
reimbursedAmount = availableBalance;
}

if (reimbursedAmount > 0) {
IERC20(tbtcToken).safeTransfer(msg.sender, reimbursedAmount);
}
Comment on lines +49 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there is some logic to calculate the reimbursed amount (I mean, in some cases the full amount cannot be covered), maybe we can create a view function so we can call it in SDK and use it in this function?


return reimbursedAmount;
}

/// @notice Withdraw the tokens from the pool.
/// @param to The address to withdraw to.
/// @param amount The amount to withdraw.
function withdraw(address to, uint256 amount) external onlyOwner {
IERC20(tbtcToken).safeTransfer(to, amount);
}
}
Loading