Skip to content

Commit

Permalink
Calculate withdrawal fees in SDK (#495)
Browse files Browse the repository at this point in the history
Refs: #447
Depends on: #474

In this PR we implement withdrawal fee calculation in a way that matches
the stBTC contract, where fee is calculated based on the total tBTC
amount and rounded up.
  • Loading branch information
r-czajkowski committed Jul 4, 2024
2 parents 50fb2de + 88ba215 commit 7c22547
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 75 deletions.
7 changes: 5 additions & 2 deletions sdk/src/acre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ class Acre {
new GelatoTransactionSender(gelatoApiKey),
)

const contracts = getEthereumContracts(ethersProvider, ethereumNetwork)
const contracts = await getEthereumContracts(
ethersProvider,
ethereumNetwork,
)

const subgraph = new AcreSubgraphApi(
// TODO: Set correct url based on the network
Expand Down Expand Up @@ -116,7 +119,7 @@ class Acre {

const ethereumNetwork = Acre.resolveEthereumNetwork(this.#network)

const contracts = getEthereumContracts(signer, ethereumNetwork)
const contracts = await getEthereumContracts(signer, ethereumNetwork)

const tbtc = await Tbtc.initialize(
signer,
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/lib/contracts/bitcoin-depositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export interface BitcoinDepositor extends DepositorProxy {
/**
* Calculates the deposit fee based on the provided amount.
* @param amountToDeposit Amount to deposit in 1e18 token precision.
* @returns Deposit fees grouped by tBTC and Acre networks in 1e18 tBTC token
* @returns Deposit fees grouped by tBTC and Acre protocols in 1e18 tBTC token
* precision.
*/
calculateDepositFee(amountToDeposit: bigint): Promise<DepositFees>
Expand Down
12 changes: 12 additions & 0 deletions sdk/src/lib/contracts/bitcoin-redeemer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { ChainIdentifier } from "./chain-identifier"

export type WithdrawalFees = {
tbtc: { treasuryFee: bigint }
}

export interface BitcoinRedeemer {
/**
* @returns The chain-specific identifier of this contract.
*/
getChainIdentifier(): ChainIdentifier

/**
* Calculates the withdrawal fee based on the provided amount.
* @param amountToWithdraw Amount to withdraw in 1e18 token precision.
* @returns Withdrawal fees grouped by tBTC and Acre protocols in 1e18 tBTC token
* precision.
*/
calculateWithdrawalFee(amountToWithdraw: bigint): Promise<WithdrawalFees>
}
8 changes: 8 additions & 0 deletions sdk/src/lib/contracts/stbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export interface StBTC {
*/
calculateDepositFee(amount: bigint): Promise<bigint>

/**
* Calculates the withdrawal fee taken from each tBTC withdrawal from the stBTC
* pool which is then transferred to the treasury.
* @param amount Amount to withdraw in 1e18 precision.
* @returns Withdrawal fee.
*/
calculateWithdrawalFee(amount: bigint): Promise<bigint>

/**
* Encodes the transaction data for a transaction that calls the
* `approveAndCall` function. The `approveAndCall` function allows `spender`
Expand Down
53 changes: 32 additions & 21 deletions sdk/src/lib/ethereum/bitcoin-depositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
isAddress,
solidityPacked,
zeroPadBytes,
Contract,
} from "ethers"
import {
ChainIdentifier,
Expand All @@ -26,13 +25,12 @@ import {
} from "./contract"
import { Hex, fromSatoshi } from "../utils"
import { EthereumNetwork } from "./network"
import TbtcBridge from "./tbtc-bridge"
import TbtcVault from "./tbtc-vault"

type TbtcDepositParameters = {
type TbtcBridgeMintingParameters = {
depositTreasuryFeeDivisor: bigint
depositTxMaxFee: bigint
}

type TbtcBridgeMintingParameters = TbtcDepositParameters & {
optimisticMintingFeeDivisor: bigint
}

Expand All @@ -53,6 +51,10 @@ class EthereumBitcoinDepositor
{
#cache: BitcoinDepositorCache

#tbtcBridge: TbtcBridge | undefined

#tbtcVault: TbtcVault | undefined

constructor(config: EthersContractConfig, network: EthereumNetwork) {
let artifact: EthersContractDeployment

Expand All @@ -72,6 +74,17 @@ class EthereumBitcoinDepositor
}
}

setTbtcContracts({
tbtcBridge,
tbtcVault,
}: {
tbtcBridge: TbtcBridge
tbtcVault: TbtcVault
}): void {
this.#tbtcBridge = tbtcBridge
this.#tbtcVault = tbtcVault
}

/**
* @see {BitcoinDepositor#getChainIdentifier}
*/
Expand Down Expand Up @@ -182,25 +195,15 @@ class EthereumBitcoinDepositor
return this.#cache.tbtcBridgeMintingParameters
}

const bridgeAddress = await this.instance.bridge()
const bridge = new Contract(
bridgeAddress,
[
"function depositParameters() view returns (uint64 depositDustThreshold, uint64 depositTreasuryFeeDivisor, uint64 depositTxMaxFee, uint32 depositRevealAheadPeriod)",
],
this.instance.runner,
)
if (!this.#tbtcBridge || !this.#tbtcVault) {
throw new Error("tBTC contracts not set")
}

const { depositTreasuryFeeDivisor, depositTxMaxFee } =
(await bridge.depositParameters()) as TbtcDepositParameters
await this.#tbtcBridge.depositParameters()

const vaultAddress = await this.getTbtcVaultChainIdentifier()
const vault = new Contract(
`0x${vaultAddress.identifierHex}`,
["function optimisticMintingFeeDivisor() view returns (uint32)"],
this.instance.runner,
)
const optimisticMintingFeeDivisor =
(await vault.optimisticMintingFeeDivisor()) as bigint
await this.#tbtcVault.optimisticMintingFeeDivisor()

this.#cache.tbtcBridgeMintingParameters = {
depositTreasuryFeeDivisor,
Expand All @@ -219,6 +222,14 @@ class EthereumBitcoinDepositor

return this.#cache.depositorFeeDivisor
}

async getTbtcBridgeAddress(): Promise<string> {
return this.instance.bridge()
}

async getTbtcVaultAddress(): Promise<string> {
return this.instance.tbtcVault()
}
}

export { EthereumBitcoinDepositor, packRevealDepositParameters }
62 changes: 61 additions & 1 deletion sdk/src/lib/ethereum/bitcoin-redeemer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ import {
EthersContractDeployment,
EthersContractWrapper,
} from "./contract"
import { ChainIdentifier, BitcoinRedeemer } from "../contracts"
import { ChainIdentifier, BitcoinRedeemer, WithdrawalFees } from "../contracts"
import { EthereumNetwork } from "./network"
import TbtcBridge from "./tbtc-bridge"

type TbtcBridgeRedemptionParameters = {
redemptionTreasuryFeeDivisor: bigint
}

type BitcoinRedeemerCache = {
tbtcBridgeRedemptionParameters: TbtcBridgeRedemptionParameters | undefined
}

export default class EthereumBitcoinRedeemer
// @ts-expect-error TODO: Figure out why type generated by typechain does not
Expand All @@ -16,6 +25,10 @@ export default class EthereumBitcoinRedeemer
extends EthersContractWrapper<BitcoinRedeemerTypechain>
implements BitcoinRedeemer
{
#cache: BitcoinRedeemerCache

#tbtcBridge: TbtcBridge | undefined

constructor(config: EthersContractConfig, network: EthereumNetwork) {
let artifact: EthersContractDeployment

Expand All @@ -29,6 +42,13 @@ export default class EthereumBitcoinRedeemer
}

super(config, artifact)
this.#cache = {
tbtcBridgeRedemptionParameters: undefined,
}
}

setTbtcContracts({ tbtcBridge }: { tbtcBridge: TbtcBridge }): void {
this.#tbtcBridge = tbtcBridge
}

/**
Expand All @@ -37,4 +57,44 @@ export default class EthereumBitcoinRedeemer
getChainIdentifier(): ChainIdentifier {
return this.getAddress()
}

/**
* @see {BitcoinRedeemer#calculateWithdrawalFee}
*/
async calculateWithdrawalFee(
amountToWithdraw: bigint,
): Promise<WithdrawalFees> {
const { redemptionTreasuryFeeDivisor } =
await this.#getTbtcBridgeRedemptionParameters()

const treasuryFee =
redemptionTreasuryFeeDivisor > 0
? amountToWithdraw / redemptionTreasuryFeeDivisor
: 0n

return {
tbtc: {
treasuryFee,
},
}
}

// TODO: Consider exposing it from tBTC SDK.
async #getTbtcBridgeRedemptionParameters(): Promise<TbtcBridgeRedemptionParameters> {
if (this.#cache.tbtcBridgeRedemptionParameters) {
return this.#cache.tbtcBridgeRedemptionParameters
}

if (!this.#tbtcBridge) {
throw new Error("tBTC contracts not set")
}

const { redemptionTreasuryFeeDivisor } =
await this.#tbtcBridge.redemptionParameters()

this.#cache.tbtcBridgeRedemptionParameters = {
redemptionTreasuryFeeDivisor,
}
return this.#cache.tbtcBridgeRedemptionParameters
}
}
27 changes: 25 additions & 2 deletions sdk/src/lib/ethereum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,42 @@ import { EthereumBitcoinDepositor } from "./bitcoin-depositor"
import { EthereumNetwork } from "./network"
import { EthereumStBTC } from "./stbtc"
import EthereumBitcoinRedeemer from "./bitcoin-redeemer"
import TbtcBridge from "./tbtc-bridge"
import TbtcVault from "./tbtc-vault"

export * from "./bitcoin-depositor"
export * from "./address"
export { EthereumContractRunner }

function getEthereumContracts(
async function initializeTbtcContracts(
runner: EthereumContractRunner,
bitcoinDepositor: EthereumBitcoinDepositor,
): Promise<{
tbtcBridge: TbtcBridge
tbtcVault: TbtcVault
}> {
const tbtcBridgeAddress = await bitcoinDepositor.getTbtcBridgeAddress()
const tbtcVaultAddress = await bitcoinDepositor.getTbtcVaultAddress()

const tbtcBridge = new TbtcBridge(runner, tbtcBridgeAddress)
const tbtcVault = new TbtcVault(runner, tbtcVaultAddress)

return { tbtcBridge, tbtcVault }
}

async function getEthereumContracts(
runner: EthereumContractRunner,
network: EthereumNetwork,
): AcreContracts {
): Promise<AcreContracts> {
const bitcoinDepositor = new EthereumBitcoinDepositor({ runner }, network)
const stBTC = new EthereumStBTC({ runner }, network)
const bitcoinRedeemer = new EthereumBitcoinRedeemer({ runner }, network)

const tbtcContracts = await initializeTbtcContracts(runner, bitcoinDepositor)

bitcoinDepositor.setTbtcContracts(tbtcContracts)
bitcoinRedeemer.setTbtcContracts(tbtcContracts)

return { bitcoinDepositor, stBTC, bitcoinRedeemer }
}

Expand Down
47 changes: 42 additions & 5 deletions sdk/src/lib/ethereum/stbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class EthereumStBTC

#cache: {
entryFeeBasisPoints?: bigint
} = { entryFeeBasisPoints: undefined }
exitFeeBasisPoints?: bigint
} = { entryFeeBasisPoints: undefined, exitFeeBasisPoints: undefined }

constructor(config: EthersContractConfig, network: EthereumNetwork) {
let artifact: EthersContractDeployment
Expand Down Expand Up @@ -72,10 +73,16 @@ class EthereumStBTC
async calculateDepositFee(amount: bigint): Promise<bigint> {
const entryFeeBasisPoints = await this.#getEntryFeeBasisPoints()

return (
(amount * entryFeeBasisPoints) /
(entryFeeBasisPoints + this.#BASIS_POINT_SCALE)
)
return this.#feeOnTotal(amount, entryFeeBasisPoints)
}

/**
* @see {StBTC#calculateDepositFee}
*/
async calculateWithdrawalFee(amount: bigint): Promise<bigint> {
const exitFeeBasisPoints = await this.#getExitFeeBasisPoints()

return this.#feeOnTotal(amount, exitFeeBasisPoints)
}

async #getEntryFeeBasisPoints(): Promise<bigint> {
Expand All @@ -88,6 +95,16 @@ class EthereumStBTC
return this.#cache.entryFeeBasisPoints
}

async #getExitFeeBasisPoints(): Promise<bigint> {
if (this.#cache.exitFeeBasisPoints) {
return this.#cache.exitFeeBasisPoints
}

this.#cache.exitFeeBasisPoints = await this.instance.exitFeeBasisPoints()

return this.#cache.exitFeeBasisPoints
}

/**
* @see {StBTC#encodeApproveAndCallFunctionData}
*/
Expand Down Expand Up @@ -118,6 +135,26 @@ class EthereumStBTC
convertToShares(amount: bigint): Promise<bigint> {
return this.instance.convertToShares(amount)
}

/**
* Calculates the fee when it's included in the amount.
* One is added to the result if there is a remainder to match the stBTC
* contract calculations rounding.
* @param amount Amount in tBTC
* @param feeBasisPoints Fee basis points applied to calculate the fee.
* @returns The fee part of an amount that already includes fees.
*/
#feeOnTotal(amount: bigint, feeBasisPoints: bigint) {
const result =
(amount * feeBasisPoints) / (feeBasisPoints + this.#BASIS_POINT_SCALE)
if (
(amount * feeBasisPoints) % (feeBasisPoints + this.#BASIS_POINT_SCALE) >
0
) {
return result + 1n
}
return result
}
}

// eslint-disable-next-line import/prefer-default-export
Expand Down
Loading

0 comments on commit 7c22547

Please sign in to comment.