Skip to content

Commit

Permalink
Merge pull request #32 from onflow/add-fees
Browse files Browse the repository at this point in the history
Add fees
  • Loading branch information
sisyphusSmiling authored Apr 16, 2024
2 parents 9b6dc21 + 19de0f5 commit 3a8d955
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 46 deletions.
16 changes: 8 additions & 8 deletions cadence/contracts/bridge/FlowEVMBridge.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
/// Emitted any time a new asset type is onboarded to the bridge
access(all)
event Onboarded(type: Type, cadenceContractAddress: Address, evmContractAddress: String)
/// Denotes a defining contract was deployed to the bridge accountcode
/// Denotes a defining contract was deployed to the bridge account
access(all)
event BridgeDefiningContractDeployed(
contractName: String,
Expand Down Expand Up @@ -213,7 +213,7 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
// Lock the NFT & calculate the storage used by the NFT
let storageUsed = FlowEVMBridgeNFTEscrow.lockNFT(<-token)
// Calculate the bridge fee on current rates
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(used: storageUsed, includeBase: true)
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: storageUsed)
assert(
feeProvider.isAvailableToWithdraw(amount: feeAmount),
message: "Fee provider does not have balance to cover the bridge fee of ".concat(feeAmount.toString())
Expand Down Expand Up @@ -308,13 +308,13 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
protectedTransferCall: fun (): EVM.Result
): @{NonFungibleToken.NFT} {
pre {
feeProvider.isAvailableToWithdraw(amount: FlowEVMBridgeUtils.calculateBridgeFee(used: 0, includeBase: true)):
feeProvider.isAvailableToWithdraw(amount: FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)):
"Insufficient fee paid"
!type.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported"
self.typeRequiresOnboarding(type) == false: "NFT must first be onboarded"
}
// Withdraw from feeProvider and deposit to self
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(used: 0, includeBase: true)
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
let feeVault <-feeProvider.withdraw(amount: feeAmount) as! @FlowToken.Vault
FlowEVMBridgeUtils.deposit(<-feeVault)

Expand Down Expand Up @@ -401,10 +401,10 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
// Lock the FT balance & calculate the extra used by the FT if any
let storageUsed = FlowEVMBridgeTokenEscrow.lockTokens(<-vault)
// Calculate the bridge fee on current rates
feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(used: storageUsed, includeBase: true)
feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: storageUsed)
} else {
Burner.burn(<-vault)
feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(used: 0, includeBase: true)
feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
}

// Withdraw from feeProvider and deposit to self
Expand Down Expand Up @@ -469,14 +469,14 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
protectedTransferCall: fun (): EVM.Result
): @{FungibleToken.Vault} {
pre {
feeProvider.isAvailableToWithdraw(amount: FlowEVMBridgeUtils.calculateBridgeFee(used: 0, includeBase: true)):
feeProvider.isAvailableToWithdraw(amount: FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)):
"Insufficient fee paid"
!type.isSubtype(of: Type<@{NonFungibleToken.Collection}>()): "Mixed asset types are not yet supported"
!type.isInstance(Type<@FlowToken.Vault>()): "Must use the CadenceOwnedAccount interface to bridge $FLOW from EVM"
self.typeRequiresOnboarding(type) == false: "NFT must first be onboarded"
}
// Withdraw from feeProvider and deposit to self
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(used: 0, includeBase: true)
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
let feeVault <-feeProvider.withdraw(amount: feeAmount) as! @FlowToken.Vault
FlowEVMBridgeUtils.deposit(<-feeVault)

Expand Down
21 changes: 1 addition & 20 deletions cadence/contracts/bridge/FlowEVMBridgeConfig.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ contract FlowEVMBridgeConfig {
/// Flat rate fee for all bridge requests
access(all)
var baseFee: UFix64
/// Fee rate per storage unit consumed by bridged assets
access(all)
var storageRate: UFix64
/// Default ERC20.decimals() value
access(all)
let defaultDecimals: UInt8
Expand All @@ -42,10 +39,6 @@ contract FlowEVMBridgeConfig {
///
access(all)
event BridgeFeeUpdated(old: UFix64, new: UFix64, isOnboarding: Bool)
/// Emitted whenever baseFee or storageRate is updated
///
access(all)
event StorageRateUpdated(old: UFix64, new: UFix64)

/*************
Getters
Expand Down Expand Up @@ -83,6 +76,7 @@ contract FlowEVMBridgeConfig {
/// @param new: UFix64 - new onboarding fee
///
/// @emits BridgeFeeUpdated with the old and new rates and isOnboarding set to true
///
access(all)
fun updateOnboardingFee(_ new: UFix64) {
emit BridgeFeeUpdated(old: FlowEVMBridgeConfig.onboardFee, new: new, isOnboarding: true)
Expand All @@ -100,24 +94,11 @@ contract FlowEVMBridgeConfig {
emit BridgeFeeUpdated(old: FlowEVMBridgeConfig.baseFee, new: new, isOnboarding: false)
FlowEVMBridgeConfig.baseFee = new
}

/// Updates the storage rate
///
/// @param new: UFix64 - new storage rate
///
/// @emits StorageRateUpdated with the old and new rates
///
access(all)
fun updateStorageRate(_ new: UFix64) {
emit StorageRateUpdated(old: FlowEVMBridgeConfig.baseFee, new: new)
FlowEVMBridgeConfig.baseFee = new
}
}

init() {
self.onboardFee = 0.0
self.baseFee = 0.0
self.storageRate = 0.0
self.defaultDecimals = 18
self.typeToEVMAddress = {
Type<@FlowToken.Vault>(): EVM.EVMAddress(
Expand Down
16 changes: 7 additions & 9 deletions cadence/contracts/bridge/FlowEVMBridgeUtils.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "FungibleToken"
import "MetadataViews"
import "ViewResolver"
import "FlowToken"
import "FlowStorageFees"

import "EVM"

Expand Down Expand Up @@ -74,22 +75,19 @@ contract FlowEVMBridgeUtils {
])
}

/// Validates the Vault used to pay the bridging fee
/// NOTE: Currently fees are calculated at a flat base fee, but may be dynamically calculated based on storage
/// used by escrowed assets in the future
/// Calculates the fee bridge fee based on the given storage usage. If includeBase is true, the base fee is included
/// in the resulting calculation.
///
/// @param used: The amount of storage used by the asset
/// @param includeBase: Whether to include the base fee in the calculation
///
/// @return The calculated fee amount
///
access(all)
view fun calculateBridgeFee(used: UInt64, includeBase: Bool): UFix64 {
// TODO: Include storage-based fee calculation
// let x = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(used) * FlowEVMBridgeConfig.storageRate
let y = includeBase ? FlowEVMBridgeConfig.baseFee : 0.0
// return x + y
return y
view fun calculateBridgeFee(bytes used: UInt64): UFix64 {
let megabytesUsed = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(used)
let storageFee = FlowStorageFees.storageCapacityToFlow(megabytesUsed)
return storageFee + FlowEVMBridgeConfig.baseFee
}

/// Returns whether the given type is allowed to be bridged as defined by the BridgePermissions contract interface.
Expand Down
6 changes: 3 additions & 3 deletions cadence/contracts/standards/EVM.cdc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Crypto
import NonFungibleToken from 0x0000000000000001
import FungibleToken from 0x0000000000000002
import FlowToken from 0x0000000000000003
import "NonFungibleToken"
import "FungibleToken"
import "FlowToken"

access(all)
contract EVM {
Expand Down
192 changes: 192 additions & 0 deletions cadence/contracts/standards/FlowStorageFees.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* The FlowStorageFees smart contract
*
* An account's storage capacity determines up to how much storage on chain it can use.
* A storage capacity is calculated by multiplying the amount of reserved flow with `StorageFee.storageMegaBytesPerReservedFLOW`
* The minimum amount of flow tokens reserved for storage capacity is `FlowStorageFees.minimumStorageReservation` this is paid during account creation, by the creator.
*
* At the end of all transactions, any account that had any value changed in their storage
* has their storage capacity checked against their storage used and their main flow token vault against the minimum reservation.
* If any account fails this check the transaction wil fail.
*
* An account moving/deleting its `FlowToken.Vault` resource will result
* in the transaction failing because the account will have no storage capacity.
*
*/

import FungibleToken from "FungibleToken"
import FlowToken from "FlowToken"

access(all) contract FlowStorageFees {

// Emitted when the amount of storage capacity an account has per reserved Flow token changes
access(all) event StorageMegaBytesPerReservedFLOWChanged(_ storageMegaBytesPerReservedFLOW: UFix64)

// Emitted when the minimum amount of Flow tokens that an account needs to have reserved for storage capacity changes.
access(all) event MinimumStorageReservationChanged(_ minimumStorageReservation: UFix64)

// Defines how much storage capacity every account has per reserved Flow token.
// definition is written per unit of flow instead of the inverse,
// so there is no loss of precision calculating storage from flow,
// but there is loss of precision when calculating flow per storage.
access(all) var storageMegaBytesPerReservedFLOW: UFix64

// Defines the minimum amount of Flow tokens that every account needs to have reserved for storage capacity.
// If an account has less then this amount reserved by the end of any transaction it participated in, the transaction will fail.
access(all) var minimumStorageReservation: UFix64

// An administrator resource that can change the parameters of the FlowStorageFees smart contract.
access(all) resource Administrator {

// Changes the amount of storage capacity an account has per accounts' reserved storage FLOW.
access(all) fun setStorageMegaBytesPerReservedFLOW(_ storageMegaBytesPerReservedFLOW: UFix64) {
if FlowStorageFees.storageMegaBytesPerReservedFLOW == storageMegaBytesPerReservedFLOW {
return
}
FlowStorageFees.storageMegaBytesPerReservedFLOW = storageMegaBytesPerReservedFLOW
emit StorageMegaBytesPerReservedFLOWChanged(storageMegaBytesPerReservedFLOW)
}

// Changes the minimum amount of FLOW an account has to have reserved.
access(all) fun setMinimumStorageReservation(_ minimumStorageReservation: UFix64) {
if FlowStorageFees.minimumStorageReservation == minimumStorageReservation {
return
}
FlowStorageFees.minimumStorageReservation = minimumStorageReservation
emit MinimumStorageReservationChanged(minimumStorageReservation)
}

access(contract) init(){}
}

/// calculateAccountCapacity returns the storage capacity of an account
///
/// Returns megabytes
/// If the account has no default balance it is counted as a balance of 0.0 FLOW
access(all) fun calculateAccountCapacity(_ accountAddress: Address): UFix64 {
var balance = 0.0
let acct = getAccount(accountAddress)

if let balanceRef = acct.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) {
balance = balanceRef.balance
}

return self.accountBalanceToAccountStorageCapacity(balance)
}

/// calculateAccountsCapacity returns the storage capacity of a batch of accounts
access(all) fun calculateAccountsCapacity(_ accountAddresses: [Address]): [UFix64] {
let capacities: [UFix64] = []
for accountAddress in accountAddresses {
let capacity = self.calculateAccountCapacity(accountAddress)
capacities.append(capacity)
}
return capacities
}

// getAccountsCapacityForTransactionStorageCheck returns the storage capacity of a batch of accounts
// This is used to check if a transaction will fail because of any account being over the storage capacity
// The payer is an exception as its storage capacity is derived from its balance minus the maximum possible transaction fees
// (transaction fees if the execution effort is at the execution efort limit, a.k.a.: computation limit, a.k.a.: gas limit)
access(all) fun getAccountsCapacityForTransactionStorageCheck(accountAddresses: [Address], payer: Address, maxTxFees: UFix64): [UFix64] {
let capacities: [UFix64] = []
for accountAddress in accountAddresses {
var balance = 0.0
let acct = getAccount(accountAddress)

if let balanceRef = acct.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) {
if accountAddress == payer {
// if the account is the payer, deduct the maximum possible transaction fees from the balance
balance = balanceRef.balance.saturatingSubtract(maxTxFees)
} else {
balance = balanceRef.balance
}
}

capacities.append(self.accountBalanceToAccountStorageCapacity(balance))
}
return capacities
}

// accountBalanceToAccountStorageCapacity returns the storage capacity
// an account would have with given the flow balance of the account.
access(all) view fun accountBalanceToAccountStorageCapacity(_ balance: UFix64): UFix64 {
// get address token balance
if balance < self.minimumStorageReservation {
// if < then minimum return 0
return 0.0
}

// return balance multiplied with megabytes per flow
return self.flowToStorageCapacity(balance)
}

// Amount in Flow tokens
// Returns megabytes
access(all) view fun flowToStorageCapacity(_ amount: UFix64): UFix64 {
return amount.saturatingMultiply(FlowStorageFees.storageMegaBytesPerReservedFLOW)
}

// Amount in megabytes
// Returns Flow tokens
access(all) view fun storageCapacityToFlow(_ amount: UFix64): UFix64 {
if FlowStorageFees.storageMegaBytesPerReservedFLOW == 0.0 {
return 0.0
}
// possible loss of precision
// putting the result back into `flowToStorageCapacity` might not yield the same result
return amount / FlowStorageFees.storageMegaBytesPerReservedFLOW
}

// converts storage used from UInt64 Bytes to UFix64 Megabytes.
access(all) view fun convertUInt64StorageBytesToUFix64Megabytes(_ storage: UInt64): UFix64 {
// safe convert UInt64 to UFix64 (without overflow)
let f = UFix64(storage % 100000000) * 0.00000001 + UFix64(storage / 100000000)
// decimal point correction. Megabytes to bytes have a conversion of 10^-6 while UFix64 minimum value is 10^-8
let storageMb = f.saturatingMultiply(100.0)
return storageMb
}

/// Gets "available" balance of an account
///
/// The available balance of an account is its default token balance minus what is reserved for storage.
/// If the account has no default balance it is counted as a balance of 0.0 FLOW
access(all) fun defaultTokenAvailableBalance(_ accountAddress: Address): UFix64 {
//get balance of account
let acct = getAccount(accountAddress)
var balance = 0.0

if let balanceRef = acct.capabilities.borrow<&FlowToken.Vault>(/public/flowTokenBalance) {
balance = balanceRef.balance
}

// get how much should be reserved for storage
var reserved = self.defaultTokenReservedBalance(accountAddress)

return balance.saturatingSubtract(reserved)
}

/// Gets "reserved" balance of an account
///
/// The reserved balance of an account is its storage used multiplied by the storage cost per flow token.
/// The reserved balance is at least the minimum storage reservation.
access(all) view fun defaultTokenReservedBalance(_ accountAddress: Address): UFix64 {
let acct = getAccount(accountAddress)
var reserved = self.storageCapacityToFlow(self.convertUInt64StorageBytesToUFix64Megabytes(acct.storage.used))
// at least self.minimumStorageReservation should be reserved
if reserved < self.minimumStorageReservation {
reserved = self.minimumStorageReservation
}

return reserved
}

init() {
self.storageMegaBytesPerReservedFLOW = 1.0 // 1 Mb per 1 Flow token
self.minimumStorageReservation = 0.0 // or 0 kb of minimum storage reservation
let admin <- create Administrator()
self.account.storage.save(<-admin, to: /storage/storageFeesAdmin)
}
}

12 changes: 12 additions & 0 deletions cadence/scripts/bridge/calculate_bridge_fee.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import "FlowEVMBridgeUtils"

/// Returns the calculated fee based on the number of bytes used to escrow an asset plus the base fee.
///
/// @param bytes: The number of bytes used to escrow an asset.
///
/// @return The calculated fee to be paid in FlowToken
///
access(all)
fun main(bytes used: UInt64): UFix64 {
return FlowEVMBridgeUtils.calculateBridgeFee(bytes: used)
}
5 changes: 5 additions & 0 deletions cadence/scripts/config/get_base_fee.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "FlowEVMBridgeConfig"

access(all) fun main(): UFix64 {
return FlowEVMBridgeConfig.baseFee
}
5 changes: 5 additions & 0 deletions cadence/scripts/config/get_onboard_fee.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "FlowEVMBridgeConfig"

access(all) fun main(): UFix64 {
return FlowEVMBridgeConfig.onboardFee
}
Loading

0 comments on commit 3a8d955

Please sign in to comment.