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

Update fee estimate #133

Merged
merged 12 commits into from
Oct 29, 2024
175 changes: 136 additions & 39 deletions cadence/tests/flow_evm_bridge_tests.cdc

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import "FungibleToken"
import "NonFungibleToken"
import "ViewResolver"
import "MetadataViews"
import "FlowToken"

import "ScopedFTProviders"

import "EVM"

import "FlowEVMBridge"
import "FlowEVMBridgeConfig"
import "FlowEVMBridgeUtils"

/// This transaction bridges NFTs from EVM to Cadence assuming the NFT has already been onboarded to the FlowEVMBridge
/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method
/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress)
///
/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier
/// @param ids: The ERC721 ids of the NFTs to bridge to Cadence from EVM
///
transaction(nftIdentifier: String, ids: [UInt256]) {

let nftType: Type
let collection: &{NonFungibleToken.Collection}
let scopedProvider: @ScopedFTProviders.ScopedFTProvider
let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount

prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {
/* --- Reference the signer's CadenceOwnedAccount --- */
//
// Borrow a reference to the signer's COA
self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)
?? panic("Could not borrow COA signer's account at path /storage/evm")

/* --- Construct the NFT type --- */
//
// Construct the NFT type from the provided identifier
self.nftType = CompositeType(nftIdentifier)
?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))
// Parse the NFT identifier into its components
let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType)
?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))
let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType)
?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))

/* --- Reference the signer's NFT Collection --- */
//
// Borrow a reference to the NFT collection, configuring if necessary
let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)
?? panic("Could not borrow ViewResolver from NFT contract with name "
.concat(nftContractName).concat(" and address ")
.concat(nftContractAddress.toString()))
let collectionData = viewResolver.resolveContractView(
resourceType: self.nftType,
viewType: Type<MetadataViews.NFTCollectionData>()
) as! MetadataViews.NFTCollectionData?
?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier))
if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil {
signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath)
signer.capabilities.unpublish(collectionData.publicPath)
let collectionCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath)
signer.capabilities.publish(collectionCap, at: collectionData.publicPath)
}
self.collection = signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath)
?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path "
.concat(collectionData.storagePath.toString()))

/* --- Configure a ScopedFTProvider --- */
//
// Set a cap on the withdrawable bridge fee
var approxFee = FlowEVMBridgeUtils.calculateBridgeFee(
bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction
) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length))
// Issue and store bridge-dedicated Provider Capability in storage if necessary
if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {
let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
/storage/flowTokenVault
)
signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
}
// Copy the stored Provider capability and create a ScopedFTProvider
let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
from: FlowEVMBridgeConfig.providerCapabilityStoragePath
) ?? panic("Invalid FungibleToken Provider Capability found in storage at path "
.concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString()))
let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)
self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(
provider: providerCapCopy,
filters: [ providerFilter ],
expiration: getCurrentBlock().timestamp + 1.0
)
}

execute {
// Iterate over the provided ids
for id in ids {
// Execute the bridge
let nft: @{NonFungibleToken.NFT} <- self.coa.withdrawNFT(
type: self.nftType,
id: id,
feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
)
// Ensure the bridged nft is the correct type
assert(
nft.getType() == self.nftType,
message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier)
.concat(", received: ").concat(nft.getType().identifier)
)
// Deposit the bridged NFT into the signer's collection
self.collection.deposit(token: <-nft)
}
// Destroy the ScopedFTProvider
destroy self.scopedProvider
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import "FungibleToken"
import "NonFungibleToken"
import "ViewResolver"
import "MetadataViews"

import "ScopedFTProviders"

import "EVM"

import "FlowEVMBridge"
import "FlowEVMBridgeConfig"
import "FlowEVMBridgeUtils"

/// This transaction bridges NFTs from EVM to Cadence assuming the NFT has already been onboarded to the FlowEVMBridge.
/// Also know that the recipient Flow account must have a Receiver capable of receiving the this bridged NFT accessible
/// via published Capability at the token's standard path.
/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method
/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress)
///
/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier
/// @param ids: The ERC721 ids of the NFTs to bridge to Cadence from EVM
/// @param recipient: The Flow account address to receive the bridged NFT
///
transaction(nftIdentifier: String, ids: [UInt256], recipient: Address) {

let nftType: Type
let receiver: &{NonFungibleToken.Receiver}
let scopedProvider: @ScopedFTProviders.ScopedFTProvider
let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount

prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {
/* --- Reference the signer's CadenceOwnedAccount --- */
//
// Borrow a reference to the signer's COA
self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)
?? panic("Could not borrow COA signer's account at path /storage/evm")

/* --- Construct the NFT type --- */
//
// Construct the NFT type from the provided identifier
self.nftType = CompositeType(nftIdentifier)
?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))
// Parse the NFT identifier into its components
let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType)
?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))
let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType)
?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))

/* --- Reference the recipient's NFT Receiver --- */
//
// Borrow a reference to the NFT collection, configuring if necessary
let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)
?? panic("Could not borrow ViewResolver from NFT contract with name "
.concat(nftContractName).concat(" and address ")
.concat(nftContractAddress.toString()))
let collectionData = viewResolver.resolveContractView(
resourceType: self.nftType,
viewType: Type<MetadataViews.NFTCollectionData>()
) as! MetadataViews.NFTCollectionData?
?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier))
// Configure the signer's account for this NFT
if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil {
signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath)
signer.capabilities.unpublish(collectionData.publicPath)
let collectionCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath)
signer.capabilities.publish(collectionCap, at: collectionData.publicPath)
}
self.receiver = getAccount(recipient).capabilities.borrow<&{NonFungibleToken.Receiver}>(collectionData.publicPath)
?? panic("Could not borrow NonFungibleToken Receiver from recipient's public capability path")

/* --- Configure a ScopedFTProvider --- */
//
// Set a cap on the withdrawable bridge fee
var approxFee = FlowEVMBridgeUtils.calculateBridgeFee(
bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction
) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length))
// Issue and store bridge-dedicated Provider Capability in storage if necessary
if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {
let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
/storage/flowTokenVault
)
signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
}
// Copy the stored Provider capability and create a ScopedFTProvider
let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
from: FlowEVMBridgeConfig.providerCapabilityStoragePath
) ?? panic("Invalid FungibleToken Provider Capability found in storage at path "
.concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString()))
let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)
self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(
provider: providerCapCopy,
filters: [ providerFilter ],
expiration: getCurrentBlock().timestamp + 1.0
)
}

execute {
// Iterate over the provided ids
for id in ids {
// Execute the bridge
let nft: @{NonFungibleToken.NFT} <- self.coa.withdrawNFT(
type: self.nftType,
id: id,
feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
)
// Ensure the bridged nft is the correct type
assert(
nft.getType() == self.nftType,
message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier)
.concat(", received: ").concat(nft.getType().identifier)
)
// Deposit the bridged NFT into the signer's collection
self.receiver.deposit(token: <-nft)
}
// Destroy the ScopedFTProvider
destroy self.scopedProvider
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import "FungibleToken"
import "NonFungibleToken"
import "ViewResolver"
import "MetadataViews"
import "FlowToken"

import "ScopedFTProviders"

import "EVM"

import "FlowEVMBridge"
import "FlowEVMBridgeConfig"
import "FlowEVMBridgeUtils"

/// Bridges an NFT from the signer's collection in Cadence to the provided recipient in FlowEVM
///
/// NOTE: This transaction also onboards the NFT to the bridge if necessary which may incur additional fees
/// than bridging an asset that has already been onboarded.
///
/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier
/// @param id: The Cadence NFT.id of the NFT to bridge to EVM
/// @param recipient: The hex-encoded EVM address to receive the NFT
///
transaction(nftIdentifier: String, ids: [UInt64], recipient: String) {

let nftType: Type
let collection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}
let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount
let requiresOnboarding: Bool
let scopedProvider: @ScopedFTProviders.ScopedFTProvider

prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {
/* --- Reference the signer's CadenceOwnedAccount --- */
//
// Borrow a reference to the signer's COA
self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)
?? panic("Could not borrow COA signer's account at path /storage/evm")

/* --- Construct the NFT type --- */
//
// Construct the NFT type from the provided identifier
self.nftType = CompositeType(nftIdentifier)
?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))
// Parse the NFT identifier into its components
let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType)
?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))
let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType)
?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))

/* --- Retrieve the NFT --- */
//
// Borrow a reference to the NFT collection, configuring if necessary
let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)
?? panic("Could not borrow ViewResolver from NFT contract with name "
.concat(nftContractName).concat(" and address ")
.concat(nftContractAddress.toString()))
let collectionData = viewResolver.resolveContractView(
resourceType: self.nftType,
viewType: Type<MetadataViews.NFTCollectionData>()
) as! MetadataViews.NFTCollectionData?
?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier))
self.collection = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(
from: collectionData.storagePath
) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path "
.concat(collectionData.storagePath.toString()))

// Withdraw the requested NFT & set a cap on the withdrawable bridge fee
var approxFee = FlowEVMBridgeUtils.calculateBridgeFee(
bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction
) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length))
// Determine if the NFT requires onboarding - this impacts the fee required
self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nftType)
?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier))
// Add the onboarding fee if onboarding is necessary
if self.requiresOnboarding {
approxFee = approxFee + FlowEVMBridgeConfig.onboardFee
}

/* --- Configure a ScopedFTProvider --- */
//
// Issue and store bridge-dedicated Provider Capability in storage if necessary
if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {
let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
/storage/flowTokenVault
)
signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
}
// Copy the stored Provider capability and create a ScopedFTProvider
let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
from: FlowEVMBridgeConfig.providerCapabilityStoragePath
) ?? panic("Invalid FungibleToken Provider Capability found in storage at path "
.concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString()))
let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)
self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(
provider: providerCapCopy,
filters: [ providerFilter ],
expiration: getCurrentBlock().timestamp + 1.0
)
}

execute {
if self.requiresOnboarding {
// Onboard the NFT to the bridge
FlowEVMBridge.onboardByType(
self.nftType,
feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
)
}

// Iterate over requested IDs and bridge each NFT to the provided recipient in EVM
for id in ids {
// Withdraw the NFT & ensure it is the correct type
let nft <-self.collection.withdraw(withdrawID: id)
assert(
nft.getType() == self.nftType,
message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier)
.concat(", received: ").concat(nft.getType().identifier)
)
// Execute the bridge to EVM
let recipientEVMAddress = EVM.addressFromString(recipient)
FlowEVMBridge.bridgeNFTToEVM(
token: <-nft,
to: EVM.addressFromString(recipient),
feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
)
}

// Destroy the ScopedFTProvider
destroy self.scopedProvider
}
}
Loading
Loading