Skip to content

Commit

Permalink
add helpers to FlowEVMBridgeUtils to simplify NFT bridging logic
Browse files Browse the repository at this point in the history
  • Loading branch information
sisyphusSmiling committed Apr 25, 2024
1 parent e008d1e commit dc137e1
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 74 deletions.
121 changes: 47 additions & 74 deletions cadence/contracts/bridge/FlowEVMBridge.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,14 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
!token.isInstance(Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported"
self.typeRequiresOnboarding(token.getType()) == false: "NFT must first be onboarded"
}
/* Gather identifying information */
//
let tokenType = token.getType()
let tokenID = token.id
let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id)

/* Metadata assignement */
//
// Grab the URI from the NFT if available
var uri: String = ""
// Default to project-specified URI
Expand All @@ -219,74 +223,43 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
uri = SerializeMetadata.serializeNFTMetadataAsURI(&token as &{NonFungibleToken.NFT})
}

/* Secure NFT in escrow & deposit calculated fees */
//
// 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(bytes: storageUsed)
// Withdraw fee from feeProvider and deposit
FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount)

/* Determine EVM handling */
//
// Does the bridge control the EVM contract associated with this type?
let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: tokenType)
?? panic("No EVMAddress found for token type")
let isFactoryDeployed = FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress)
// Controlled by the bridge - mint or transfer based on existence
if isFactoryDeployed {

/* Third-party controlled ERC721 handling */
//
// Not bridge-controlled, transfer existing ownership
if !isFactoryDeployed {
FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID)
return
}

// Check if the ERC721 exists
let existsResponse = EVM.decodeABI(
types: [Type<Bool>()],
data: FlowEVMBridgeUtils.call(
signature: "exists(uint256)",
targetEVMAddress: associatedAddress,
args: [evmID],
gasLimit: 12000000,
value: 0.0
).data,
)
assert(existsResponse.length == 1, message: "Invalid response length")
let exists = existsResponse[0] as! Bool
if exists {
// If so transfer
let transferResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeTransferFrom(address,address,uint256)",
targetEVMAddress: associatedAddress,
args: [self.getBridgeCOAEVMAddress(), to, evmID],
gasLimit: 15000000,
value: 0.0
)
assert(transferResult.status == EVM.Status.successful, message: "Tranfer to bridge recipient failed")

// And update the URI to reflect current metadata
let updateURIResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "updateTokenURI(uint256,string)",
targetEVMAddress: associatedAddress,
args: [evmID, uri],
gasLimit: 15000000,
value: 0.0
)
assert(updateURIResult.status == EVM.Status.successful, message: "Tranfer to bridge recipient failed")
} else {
// Otherwise mint with current URI
let callResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeMint(address,uint256,string)",
targetEVMAddress: associatedAddress,
args: [to, evmID, uri],
gasLimit: 15000000,
value: 0.0
)
assert(callResult.status == EVM.Status.successful, message: "Tranfer to bridge recipient failed")
}
/* Bridge-owned ERC721 handling */
//
// Check if the ERC721 exists in the EVM contract - determines if bridge mints or transfers
let exists = FlowEVMBridgeUtils.erc721Exists(erc721Address: associatedAddress, id: evmID)
if exists {
// Transfer the existing NFT
FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID)

// And update the URI to reflect current metadata
FlowEVMBridgeUtils.mustUpdateTokenURI(erc721Address: associatedAddress, id: evmID, uri: uri)
} else {
// Not bridge-controlled, transfer existing ownership
let callResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeTransferFrom(address,address,uint256)",
targetEVMAddress: associatedAddress,
args: [self.getBridgeCOAEVMAddress(), to, evmID],
gasLimit: 15000000,
value: 0.0
)
assert(callResult.status == EVM.Status.successful, message: "Transfer to bridge recipient failed")
// Otherwise mint with current URI
FlowEVMBridgeUtils.mustSafeMintERC721(erc721Address: associatedAddress, to: to, id: evmID, uri: uri)
}
}

Expand Down Expand Up @@ -315,40 +288,36 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
!type.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported"
self.typeRequiresOnboarding(type) == false: "NFT must first be onboarded"
}
/* Provision fee */
//
// Withdraw from feeProvider and deposit to self
let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0)
FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount)

/* Execute escrow transfer */
//
// Get the EVMAddress of the ERC721 contract associated with the type
let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)
?? panic("No EVMAddress found for token type")

// Ensure the caller is either the current owner or approved for the NFT
let isAuthorized: Bool = FlowEVMBridgeUtils.isOwnerOrApproved(
ofNFT: id,
// Execute the transfer call and make needed state assertions to confirm escrow from named owner
FlowEVMBridgeUtils.mustExecuteERC721ProtectedTransferCall(
owner: owner,
evmContractAddress: associatedAddress
)
assert(isAuthorized, message: "Caller is not the owner of or approved for requested NFT")

// Execute the transfer from the calling owner to the bridge's COA, escrowing the NFT in EVM
let callResult = protectedTransferCall()
assert(callResult.status == EVM.Status.successful, message: "Transfer to bridge COA failed")

// Ensure the bridge is now the owner of the NFT after the preceding transfer
let isEscrowed: Bool = FlowEVMBridgeUtils.isOwner(
ofNFT: id,
owner: self.getBridgeCOAEVMAddress(),
evmContractAddress: associatedAddress
id: id,
erc721Address: associatedAddress,
protectedTransferCall: protectedTransferCall
)
assert(isEscrowed, message: "Transfer to bridge COA failed - cannot bridge NFT without bridge escrow")

/* Gather identifying info */
//
// Derive the defining Cadence contract name & address & attempt to borrow it as IEVMBridgeNFTMinter
let contractName = FlowEVMBridgeUtils.getContractName(fromType: type)!
let contractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)!
let nftContract = getAccount(contractAddress).contracts.borrow<&{IEVMBridgeNFTMinter}>(name: contractName)
// Get the token URI from the ERC721 contract
let uri = FlowEVMBridgeUtils.getTokenURI(evmContractAddress: associatedAddress, id: id)

/* Unlock escrowed NFTs */
//
// If the NFT is currently locked, unlock and return
if let cadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) {
let nft <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID)
Expand All @@ -360,9 +329,13 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {

return <-nft
}
// Otherwise, we expect the NFT to be minted in Cadence

/* Mint bridge-defined NFT */
//
// Ensure the NFT is bridge-defined
assert(self.account.address == contractAddress, message: "Unexpected error bridging NFT from EVM")

// We expect the NFT to be minted in Cadence as it is bridge-defined
let nft <- nftContract!.mintNFT(id: id, tokenURI: uri)
return <-nft
}
Expand Down
114 changes: 114 additions & 0 deletions cadence/contracts/bridge/FlowEVMBridgeUtils.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,31 @@ contract FlowEVMBridgeUtils {
return false
}

/// Returns whether the given ERC721 exists, assuming the ERC721 contract implements the `exists` method. While this
/// method is not part of the ERC721 standard, it is implemented in the bridge-deployed ERC721 implementation.
/// Reverts on EVM call failure.
///
/// @param erc721Address: The EVM contract address of the ERC721 token
/// @param id: The ID of the ERC721 token to check
///
/// @return true if the ERC721 token exists, false otherwise
///
access(all)
fun erc721Exists(erc721Address: EVM.EVMAddress, id: UInt256): Bool {
let existsResponse = EVM.decodeABI(
types: [Type<Bool>()],
data: FlowEVMBridgeUtils.call(
signature: "exists(uint256)",
targetEVMAddress: erc721Address,
args: [id],
gasLimit: 12000000,
value: 0.0
).data,
)
assert(existsResponse.length == 1, message: "Invalid response length")
return existsResponse[0] as! Bool
}

/// Returns the ERC20 balance of the owner at the given ERC20 contract address. Reverts on EVM call failure.
///
/// @param amount: The amount to check if the owner has enough balance to cover
Expand Down Expand Up @@ -756,6 +781,95 @@ contract FlowEVMBridgeUtils {
)
}

/// Executes a safeTransferFrom call on the given ERC721 contract address, transferring the NFT from bridge escrow
/// in EVM to the named recipient and asserting pre- and post-state changes.
///
access(account)
fun mustSafeTransferERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256) {
let bridgeCOAAddress = self.getBridgeCOAEVMAddress()

let bridgePreStatus = self.isOwner(ofNFT: id, owner: bridgeCOAAddress, evmContractAddress: erc721Address)
let toPreStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
assert(bridgePreStatus, message: "Bridge COA does not own NFT")
assert(!toPreStatus, message: "Recipient already owns NFT")

let transferResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeTransferFrom(address,address,uint256)",
targetEVMAddress: erc721Address,
args: [bridgeCOAAddress, to, id],
gasLimit: 15000000,
value: 0.0
)
assert(transferResult.status == EVM.Status.successful, message: "Transfer to bridge recipient failed")

let bridgePostStatus = self.isOwner(ofNFT: id, owner: bridgeCOAAddress, evmContractAddress: erc721Address)
let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
assert(!bridgePostStatus, message: "Bridge is still owner of NFT after transfer")
assert(toPostStatus, message: "Recipient does not own the NFT after transfer")
}

/// Executes a safeMint call on the given ERC721 contract address, minting an ERC72 to the named recipient and
/// asserting pre- and post-state changes. Assumes the bridge COA has the authority to mint the NFT.
///
access(account)
fun mustSafeMintERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, uri: String) {
let bridgeCOAAddress = self.getBridgeCOAEVMAddress()

let mintResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "safeMint(address,uint256,string)",
targetEVMAddress: erc721Address,
args: [to, id, uri],
gasLimit: 15000000,
value: 0.0
)
assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge recipient failed")

let toPostStatus = self.isOwner(ofNFT: id, owner: to, evmContractAddress: erc721Address)
assert(toPostStatus, message: "Recipient does not own the NFT after minting")
}

/// Executes updateTokenURI call on the given ERC721 contract address, updating the tokenURI of the NFT. This is
/// not a standard ERC721 function, but is implemented in the bridge-deployed ERC721 implementation to enable
/// synchronization of token metadata with Cadence NFT state on bridging.
///
access(account)
fun mustUpdateTokenURI(erc721Address: EVM.EVMAddress, id: UInt256, uri: String) {
let bridgeCOAAddress = self.getBridgeCOAEVMAddress()

let updateResult: EVM.Result = FlowEVMBridgeUtils.call(
signature: "updateTokenURI(uint256,string)",
targetEVMAddress: erc721Address,
args: [id, uri],
gasLimit: 15000000,
value: 0.0
)
assert(updateResult.status == EVM.Status.successful, message: "URI update failed")
}

/// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was
/// successful by validating the named owner is authorized to act on the NFT before the transfer, the transfer
/// was successful, and the bridge COA owns the NFT after the protected transfer call.
///
access(account)
fun mustExecuteERC721ProtectedTransferCall(
owner: EVM.EVMAddress,
id: UInt256,
erc721Address: EVM.EVMAddress,
protectedTransferCall: fun (): EVM.Result
) {
// Ensure the named owner is authorized to act on the NFT
let isAuthorized = self.isOwnerOrApproved(ofNFT: id, owner: owner, evmContractAddress: erc721Address)
assert(isAuthorized, message: "Named owner is not the owner of the NFT")

// Call the protected transfer function which should execute a transfer call from the owner to escrow
let transferResult = protectedTransferCall()
assert(transferResult.status == EVM.Status.successful, message: "Transfer to escrow via callback failed")

// Validate the NFT is now owned by the bridge COA, escrow the NFT
let isEscrowed = self.isOwner(ofNFT: id, owner: self.getBridgeCOAEVMAddress(), evmContractAddress: erc721Address)
assert(isEscrowed, message: "Bridge COA does not own NFT after transfer")
}

/// Mints ERC20 tokens to the recipient and confirms that the recipient's balance was updated
///
access(account)
Expand Down

0 comments on commit dc137e1

Please sign in to comment.