Skip to content

Commit

Permalink
Estimate staking fees (#281)
Browse files Browse the repository at this point in the history
Closes: #150

Add function that estimates the deposit fees for a given amount and
expose it
in staking module.

Returns the following fees for deposit operation:
- `tbtc` - total tBTC network minting fees including:
  - `treasuryFee` - the tBTC treasury fee taken from each deposit and
transferred to the treasury upon sweep proof submission. Is calculated
     based on the initial funding transaction amount,
- `optimisticMintingFee` - the tBTC optimistic minting fee, Is
calculated
     AFTER the treasury fee is cut,
- `depositTxMaxFee` - maximum amount of BTC transaction fee that can be
     incurred by each swept deposit being part of the given sweep
     transaction.
- `acre` - total Acre network staking fees including:
- `depositorFee` - the Acre network depositor fee taken from each
deposit
    and transferred to the treasury upon stake request finalization,
- `depositFee` - the stBTC deposit fee taken for each tBTC deposit into
the
     stBTC pool.
- `total` - summed up all staking fees. We decided to add a total field
because
the SDK should be responsible for summing up all fees. If we add a new
fee in
the future the consumers will have to update their code as well which is
not a
   developer-friendly approach.
  • Loading branch information
nkuba authored Apr 18, 2024
2 parents cde70d9 + 1a21936 commit 5256712
Show file tree
Hide file tree
Showing 9 changed files with 556 additions and 13 deletions.
46 changes: 46 additions & 0 deletions sdk/src/lib/contracts/bitcoin-depositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ export type DecodedExtraData = {
referral: number
}

/**
* Represents the tBTC network minting fees.
*/
type TBTCMintingFees = {
/**
* The tBTC treasury fee taken from each deposit and transferred to the
* treasury upon sweep proof submission. Is calculated based on the initial
* funding transaction amount.
*/
treasuryFee: bigint
/**
* The tBTC optimistic minting fee, Is calculated AFTER the treasury fee is
* cut.
*/
optimisticMintingFee: bigint
/**
* Maximum amount of BTC transaction fee that can be incurred by each swept
* deposit being part of the given sweep transaction.
*/
depositTxMaxFee: bigint
}

/**
* Represents the Acre protocol deposit fees.
*/
type AcreDepositFees = {
/**
* The Acre protocol depositor fee taken from each Bitcoin deposit and
* transferred to the treasury upon deposit request finalization.
*/
bitcoinDepositorFee: bigint
}

export type DepositFees = {
tbtc: TBTCMintingFees
acre: AcreDepositFees
}

/**
* Interface for communication with the BitcoinDepositor on-chain contract.
*/
Expand Down Expand Up @@ -36,6 +74,14 @@ export interface BitcoinDepositor extends DepositorProxy {
*/
decodeExtraData(extraData: string): DecodedExtraData

/**
* 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
* precision.
*/
calculateDepositFee(amountToDeposit: bigint): Promise<DepositFees>

/**
* @returns Minimum deposit amount.
*/
Expand Down
8 changes: 8 additions & 0 deletions sdk/src/lib/contracts/stbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@ export interface StBTC {
* @returns Maximum withdraw value.
*/
assetsBalanceOf(identifier: ChainIdentifier): Promise<bigint>

/**
* Calculates the deposit fee taken from each tBTC deposit to the stBTC pool
* which is then transferred to the treasury.
* @param amount Amount to deposit in 1e18 precision.
* @returns Deposit fee.
*/
calculateDepositFee(amount: bigint): Promise<bigint>
}
101 changes: 100 additions & 1 deletion sdk/src/lib/ethereum/bitcoin-depositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
isAddress,
solidityPacked,
zeroPadBytes,
Contract,
} from "ethers"
import {
ChainIdentifier,
DecodedExtraData,
BitcoinDepositor,
DepositReceipt,
DepositFees,
} from "../contracts"
import { BitcoinRawTxVectors } from "../bitcoin"
import { EthereumAddress } from "./address"
Expand All @@ -23,9 +25,23 @@ import {
EthersContractDeployment,
EthersContractWrapper,
} from "./contract"
import { Hex } from "../utils"
import { Hex, fromSatoshi } from "../utils"
import { EthereumNetwork } from "./network"

type TbtcDepositParameters = {
depositTreasuryFeeDivisor: bigint
depositTxMaxFee: bigint
}

type TbtcBridgeMintingParameters = TbtcDepositParameters & {
optimisticMintingFeeDivisor: bigint
}

type BitcoinDepositorCache = {
tbtcBridgeMintingParameters: TbtcBridgeMintingParameters | undefined
depositorFeeDivisor: bigint | undefined
}

/**
* Ethereum implementation of the BitcoinDepositor.
*/
Expand All @@ -36,6 +52,8 @@ class EthereumBitcoinDepositor
extends EthersContractWrapper<BitcoinDepositorTypechain>
implements BitcoinDepositor
{
#cache: BitcoinDepositorCache

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

Expand All @@ -49,6 +67,10 @@ class EthereumBitcoinDepositor
}

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

/**
Expand Down Expand Up @@ -138,6 +160,83 @@ class EthereumBitcoinDepositor
async minDepositAmount(): Promise<bigint> {
return this.instance.minDepositAmount()
}

/**
* @see {BitcoinDepositor#calculateDepositFee}
*/
async calculateDepositFee(amountToDeposit: bigint): Promise<DepositFees> {
const {
depositTreasuryFeeDivisor,
depositTxMaxFee,
optimisticMintingFeeDivisor,
} = await this.#getTbtcBridgeMintingParameters()

const treasuryFee =
depositTreasuryFeeDivisor > 0
? amountToDeposit / depositTreasuryFeeDivisor
: 0n

const amountSubTreasury = amountToDeposit - treasuryFee

const optimisticMintingFee =
optimisticMintingFeeDivisor > 0
? amountSubTreasury / optimisticMintingFeeDivisor
: 0n

const depositorFeeDivisor = await this.#depositorFeeDivisor()
// Compute depositor fee. The fee is calculated based on the initial funding
// transaction amount, before the tBTC protocol network fees were taken.
const depositorFee =
depositorFeeDivisor > 0n ? amountToDeposit / depositorFeeDivisor : 0n

return {
tbtc: {
treasuryFee,
optimisticMintingFee,
depositTxMaxFee: fromSatoshi(depositTxMaxFee),
},
acre: {
bitcoinDepositorFee: depositorFee,
},
}
}

// TODO: Consider exposing it from tBTC SDK.
async #getTbtcBridgeMintingParameters(): Promise<TbtcBridgeMintingParameters> {
if (this.#cache.tbtcBridgeMintingParameters) {
return this.#cache.tbtcBridgeMintingParameters
}

const bridgeAddress = await this.instance.bridge()
const bridge = new Contract(bridgeAddress, [
"function depositsParameters()",
])
const depositsParameters =
(await bridge.depositsParameters()) as TbtcDepositParameters

const vaultAddress = await this.getTbtcVaultChainIdentifier()
const vault = new Contract(`0x${vaultAddress.identifierHex}`, [
"function optimisticMintingFeeDivisor()",
])
const optimisticMintingFeeDivisor =
(await vault.optimisticMintingFeeDivisor()) as bigint

this.#cache.tbtcBridgeMintingParameters = {
...depositsParameters,
optimisticMintingFeeDivisor,
}
return this.#cache.tbtcBridgeMintingParameters
}

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

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

return this.#cache.depositorFeeDivisor
}
}

export { EthereumBitcoinDepositor, packRevealDepositParameters }
28 changes: 28 additions & 0 deletions sdk/src/lib/ethereum/stbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class EthereumStBTC
extends EthersContractWrapper<StBTCTypechain>
implements StBTC
{
readonly #BASIS_POINT_SCALE = BigInt(1e4)

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

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

Expand Down Expand Up @@ -44,6 +50,28 @@ class EthereumStBTC
assetsBalanceOf(identifier: ChainIdentifier): Promise<bigint> {
return this.instance.assetsBalanceOf(`0x${identifier.identifierHex}`)
}

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

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

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

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

return this.#cache.entryFeeBasisPoints
}
}

// eslint-disable-next-line import/prefer-default-export
Expand Down
47 changes: 45 additions & 2 deletions sdk/src/modules/staking/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { ChainIdentifier, TBTC } from "@keep-network/tbtc-v2.ts"
import { AcreContracts, DepositorProxy } from "../../lib/contracts"
import { AcreContracts, DepositorProxy, DepositFees } from "../../lib/contracts"
import { ChainEIP712Signer } from "../../lib/eip712-signer"
import { StakeInitialization } from "./stake-initialization"
import { toSatoshi } from "../../lib/utils"
import { fromSatoshi, toSatoshi } from "../../lib/utils"

/**
* Represents all total deposit fees grouped by network.
*/
export type DepositFee = {
tbtc: bigint
acre: bigint
total: bigint
}

/**
* Module exposing features related to the staking.
Expand Down Expand Up @@ -80,6 +89,40 @@ class StakingModule {
return this.#contracts.stBTC.assetsBalanceOf(identifier)
}

/**
* Estimates the deposit fee based on the provided amount.
* @param amount Amount to deposit in satoshi.
* @returns Deposit fee grouped by tBTC and Acre networks in 1e8 satoshi
* precision and total deposit fee value.
*/
async estimateDepositFee(amount: bigint): Promise<DepositFee> {
const amountInTokenPrecision = fromSatoshi(amount)

const { acre: acreFees, tbtc: tbtcFees } =
await this.#contracts.bitcoinDepositor.calculateDepositFee(
amountInTokenPrecision,
)
const depositFee = await this.#contracts.stBTC.calculateDepositFee(
amountInTokenPrecision,
)

const sumFeesByProtocol = <
T extends DepositFees["tbtc"] | DepositFees["acre"],
>(
fees: T,
) => Object.values(fees).reduce((reducer, fee) => reducer + fee, 0n)

const tbtc = toSatoshi(sumFeesByProtocol(tbtcFees))

const acre = toSatoshi(sumFeesByProtocol(acreFees)) + toSatoshi(depositFee)

return {
tbtc,
acre,
total: tbtc + acre,
}
}

/**
* @returns Minimum deposit amount in 1e8 satoshi precision.
*/
Expand Down
51 changes: 51 additions & 0 deletions sdk/test/lib/ethereum/stbtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe("stbtc", () => {
const mockedContractInstance = {
balanceOf: jest.fn(),
assetsBalanceOf: jest.fn(),
entryFeeBasisPoints: jest.fn(),
}

beforeAll(() => {
Expand Down Expand Up @@ -70,4 +71,54 @@ describe("stbtc", () => {
expect(result).toEqual(expectedResult)
})
})

describe("depositFee", () => {
// 0.1 in 1e18 precision
const amount = 100000000000000000n
const mockedEntryFeeBasisPointsValue = 1n
// (amount * basisPoints) / (basisPoints / 1e4)
const expectedResult = 9999000099990n

let result: bigint

describe("when the entry fee basis points value is not yet cached", () => {
beforeAll(async () => {
mockedContractInstance.entryFeeBasisPoints.mockResolvedValue(
mockedEntryFeeBasisPointsValue,
)

result = await stbtc.calculateDepositFee(amount)
})

it("should get the entry fee basis points from contract", () => {
expect(mockedContractInstance.entryFeeBasisPoints).toHaveBeenCalled()
})

it("should calculate the deposit fee correctly", () => {
expect(result).toEqual(expectedResult)
})
})

describe("the entry fee basis points value is cached", () => {
beforeAll(async () => {
mockedContractInstance.entryFeeBasisPoints.mockResolvedValue(
mockedEntryFeeBasisPointsValue,
)

await stbtc.calculateDepositFee(amount)

result = await stbtc.calculateDepositFee(amount)
})

it("should get the entry fee basis points from cache", () => {
expect(
mockedContractInstance.entryFeeBasisPoints,
).toHaveBeenCalledTimes(1)
})

it("should calculate the deposit fee correctly", () => {
expect(result).toEqual(expectedResult)
})
})
})
})
Loading

0 comments on commit 5256712

Please sign in to comment.