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

Add metadata discovery & resolution paths to escrow contracts #113

Merged
merged 4 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,16 @@ access(all) contract FlowEVMBridgeNFTEscrow {
return self.borrowLocker(forType: type)?.getEVMID(from: cadenceID) ?? nil
}

/// Resolves the requested view type for the given NFT type if it is locked and supports the requested view type
/// Returns the metadata view types supported by a given NFT if it is in escrow, nil otherwise
///
/// @param nftType: Type of the locked NFT
/// @param viewType: Type of the view to resolve
/// @param id: ID of the locked NFT
///
/// @returns The resolved view as AnyStruct if the NFT is locked and the view is supported, otherwise returns nil
/// @returns The metadata view types supported by the locked NFT if it is in escrow, nil otherwise
///
access(all) fun resolveLockedNFTView(nftType: Type, id: UInt256, viewType: Type): AnyStruct? {
if let locker = self.borrowLocker(forType: nftType) {
if let cadenceID = locker.getCadenceID(from: id) {
return locker.borrowViewResolver(id: cadenceID)?.resolveView(viewType) ?? nil
}
access(all) view fun getViews(nftType: Type, id: UInt64): [Type]? {
if let nft = self.borrowLockedNFT(type: nftType, id: id) {
return nft.getViews()
}
return nil
}
Expand All @@ -88,9 +85,9 @@ access(all) contract FlowEVMBridgeNFTEscrow {
///
access(account) fun initializeEscrow(forType: Type, name: String, symbol: String, erc721Address: EVM.EVMAddress) {
let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: forType)
?? panic("Problem deriving locker path")
?? panic("Problem deriving Locker path for NFT type identifier=".concat(forType.identifier))
if self.account.storage.type(at: lockerPath) != nil {
panic("Locker already stored at storage path: ".concat(lockerPath.toString()))
panic("NFT Locker already stored at storage path=".concat(lockerPath.toString()))
}

let locker <- create Locker(name: name, symbol: symbol, lockedType: forType, erc721Address: erc721Address)
Expand All @@ -101,7 +98,7 @@ access(all) contract FlowEVMBridgeNFTEscrow {
///
access(account) fun lockNFT(_ nft: @{NonFungibleToken.NFT}): UInt64 {
let locker = self.borrowLocker(forType: nft.getType())
?? panic("Problem deriving locker path")
?? panic("Problem borrowing reference to Locker for NFT type identifier=".concat(nft.getType().identifier))

let preStorageSnapshot = self.account.storage.used
locker.deposit(token: <-nft)
Expand All @@ -114,7 +111,7 @@ access(all) contract FlowEVMBridgeNFTEscrow {
///
access(account) fun unlockNFT(type: Type, id: UInt64): @{NonFungibleToken.NFT} {
let locker = self.borrowLocker(forType: type)
?? panic("Problem deriving locker path")
?? panic("Problem borrowing reference to Locker for NFT type identifier=".concat(type.identifier))
return <- locker.withdraw(withdrawID: id)
}

Expand Down Expand Up @@ -270,7 +267,9 @@ access(all) contract FlowEVMBridgeNFTEscrow {
access(all)
fun deposit(token: @{NonFungibleToken.NFT}) {
pre {
self.borrowNFT(token.id) == nil: "NFT with this ID already exists in the Locker"
self.borrowNFT(token.id) == nil:
"NFT type=".concat(token.getType().identifier).concat(" with id=").concat(token.id.toString())
.concat(" already exists in the Locker")
}
if let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) {
self.evmIDToFlowID[evmID] = token.id
Expand All @@ -284,7 +283,11 @@ access(all) contract FlowEVMBridgeNFTEscrow {
access(NonFungibleToken.Withdraw)
fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
// Should not happen, but prevent potential underflow
assert(self.lockedNFTCount > 0, message: "No NFTs to withdraw")
assert(
self.lockedNFTCount > 0,
message: "Attempting to withdraw NFT id=".concat(withdrawID.toString())
.concat(" - no NFTs of type=").concat(self.lockedType.identifier).concat(" to withdraw")
)
self.lockedNFTCount = self.lockedNFTCount - 1
let token <- self.ownedNFTs.remove(key: withdrawID)!
if let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) {
Expand Down
28 changes: 10 additions & 18 deletions cadence/contracts/bridge/FlowEVMBridgeTokenEscrow.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,6 @@ access(all) contract FlowEVMBridgeTokenEscrow {
return self.borrowLocker(forType: tokenType)?.getViews() ?? []
}

/// Resolves the requested view type for the given FT type if it is locked and supports the requested view type
///
/// @param tokenType: Type of the locked fungible tokens
/// @param viewType: Type of the view to resolve
///
/// @returns The resolved view as AnyStruct if the vault is locked and the view is supported, otherwise returns nil
///
access(all) fun resolveLockedTokenView(tokenType: Type, viewType: Type): AnyStruct? {
// The Locker implements Resolver, which has basic resolveView functionality
return self.borrowLocker(forType: tokenType)?.resolveView(viewType) ?? nil
}

/**********************
Bridge Methods
***********************/
Expand All @@ -77,13 +65,15 @@ access(all) contract FlowEVMBridgeTokenEscrow {
evmTokenAddress: EVM.EVMAddress
) {
pre {
vault.balance == 0.0: "Can only initialize Escrow with an empty vault"
vault.balance == 0.0:
"Vault contains a balance=".concat(vault.balance.toString())
.concat(" - can only initialize Escrow with an empty vault")
}
let lockedType = vault.getType()
let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: lockedType)
?? panic("Problem deriving locker path")
?? panic("Problem deriving Locker path for Vault type identifier=".concat(lockedType.identifier))
if self.account.storage.type(at: lockerPath) != nil {
panic("Collision at derived Locker path for type: ".concat(lockedType.identifier))
panic("Token Locker already stored at storage path=".concat(lockedType.identifier))
}

// Create the Locker, lock a new vault of given type and save at the derived path
Expand All @@ -100,7 +90,8 @@ access(all) contract FlowEVMBridgeTokenEscrow {
/// Locks the fungible tokens in escrow returning the storage used by locking the Vault
///
access(account) fun lockTokens(_ vault: @{FungibleToken.Vault}): UInt64 {
let locker = self.borrowLocker(forType: vault.getType()) ?? panic("Locker doesn't exist for given type")
let locker = self.borrowLocker(forType: vault.getType())
?? panic("Locker doesn't exist for given type=".concat(vault.getType().identifier))

let preStorageSnapshot = self.account.storage.used
locker.deposit(from: <-vault)
Expand All @@ -112,7 +103,8 @@ access(all) contract FlowEVMBridgeTokenEscrow {
/// Unlocks the tokens of the given type and amount, reverting if it isn't in escrow
///
access(account) fun unlockTokens(type: Type, amount: UFix64): @{FungibleToken.Vault} {
let locker = self.borrowLocker(forType: type) ?? panic("Locker doesn't exist for given type")
let locker = self.borrowLocker(forType: type)
?? panic("Locker doesn't exist for given type=".concat(type.identifier))
return <- locker.withdraw(amount: amount)

}
Expand Down Expand Up @@ -158,7 +150,7 @@ access(all) contract FlowEVMBridgeTokenEscrow {
// Locked Vaults must accept their own type as Lockers escrow Vaults on a 1:1 type basis
assert(
self.lockedVault.isSupportedVaultType(type: self.lockedVault.getType()),
message: "Locked Vault does not accept its own type"
message: "Locked Vault does not accept its own type=".concat(self.lockedVault.getType().identifier)
)
}

Expand Down
11 changes: 11 additions & 0 deletions cadence/scripts/escrow/get_locked_token_balance.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "FlowEVMBridgeTokenEscrow"

/// Returns the balance of a given FungibleToken Vault type locked in escrow or nil if a vault of the given type is not
/// locked in escrow
///
/// @param vaultTypeIdentifier: The type identifier of the FungibleToken Vault
///
access(all) fun main(vaultTypeIdentifier: String): UFix64? {
let tokenType = CompositeType(vaultTypeIdentifier) ?? panic("Malformed Vault type identifier=".concat(vaultTypeIdentifier))
return FlowEVMBridgeTokenEscrow.getLockedTokenBalance(tokenType: tokenType)
}
16 changes: 16 additions & 0 deletions cadence/scripts/escrow/get_nft_views.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "NonFungibleToken"

import "FlowEVMBridgeNFTEscrow"
import "FlowEVMBridge"

/// Returns the views supported by an escrowed NFT or nil if the NFT is not locked in escrow
///
/// @param nftTypeIdentifier: The type identifier of the NFT
/// @param id: The ID of the NFT
///
/// @return The metadata view types supported by the escrowed NFT or nil if the NFT is not locked in escrow
///
access(all) fun main(nftTypeIdentifier: String, id: UInt64): [Type]? {
let type = CompositeType(nftTypeIdentifier) ?? panic("Malformed NFT type identifier=".concat(nftTypeIdentifier))
return FlowEVMBridgeNFTEscrow.getViews(nftType: type, id: id)
}
16 changes: 16 additions & 0 deletions cadence/scripts/escrow/get_vault_views.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "NonFungibleToken"

import "FlowEVMBridgeTokenEscrow"
import "FlowEVMBridge"

/// Returns the views supported by an escrowed FungibleToken Vault or nil if there is no Vault of the given type locked
/// in escrow
///
/// @param vaultTypeIdentifier: The type identifier of the NFT
///
/// @return The metadata view types supported by the escrowed FT Vault or nil if there is not Vault locked in escrow
///
access(all) fun main(vaultTypeIdentifier: String, id: UInt64): [Type]? {
let type = CompositeType(vaultTypeIdentifier) ?? panic("Malformed Vault type identifier=".concat(vaultTypeIdentifier))
return FlowEVMBridgeTokenEscrow.getViews(tokenType: type)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ import "FlowEVMBridge"
/// @return true if the NFT is locked in escrow and false otherwise
///
access(all) fun main(nftTypeIdentifier: String, id: UInt64): Bool {
let type = CompositeType(nftTypeIdentifier) ?? panic("Malformed type identifier")
let type = CompositeType(nftTypeIdentifier) ?? panic("Malformed NFT type identifier=".concat(nftTypeIdentifier))
return FlowEVMBridgeNFTEscrow.isLocked(type: type, id: id)
}
31 changes: 26 additions & 5 deletions cadence/scripts/escrow/resolve_locked_nft_metadata.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,40 @@ import "NonFungibleToken"
import "MetadataViews"

import "FlowEVMBridgeNFTEscrow"
import "FlowEVMBridge"
import "FlowEVMBridgeUtils"

/// Resolves the view for the requested locked NFT or nil if the NFT is not locked
/// NOTE: This functionality is not available via the escrow contract as `resolveView` is not a `view` method, but the
/// escrow contract does provide the necessary functionality to resolve the view from the context of a script
///
/// @param bridgeAddress: The address of the bridge contract (included as the VM bridge address varies across networks)
/// @param nftTypeIdentifier: The identifier of the NFT type
/// @param id: The ERC721 id of the escrowed NFT
/// @param viewIdentifier: The identifier of the view to resolve
///
/// @return The resolved view if the NFT is escrowed & the view is resolved by it or nil if the NFT is not locked
///
access(all) fun main(nftTypeIdentifier: String, id: UInt256, viewIdentifier: String): AnyStruct? {
let nftType: Type = CompositeType(nftTypeIdentifier) ?? panic("Malformed nft type identifier")
let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier")
access(all) fun main(bridgeAddress: Address, nftTypeIdentifier: String, id: UInt256, viewIdentifier: String): AnyStruct? {
// Construct runtime types from provided identifiers
let nftType: Type = CompositeType(nftTypeIdentifier) ?? panic("Malformed NFT type identifier=".concat(nftTypeIdentifier))
let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier=".concat(viewIdentifier))

return FlowEVMBridgeNFTEscrow.resolveLockedNFTView(nftType: nftType, id: id, viewType: view)
// Derive the Locker path for the given NFT type
let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: nftType)
?? panic("Problem deriving Locker path for NFT type identifier=".concat(nftTypeIdentifier))

// Borrow the locker from the bridge account's storage
if let locker = getAuthAccount<auth(BorrowValue) &Account>(bridgeAddress).storage.borrow<&FlowEVMBridgeNFTEscrow.Locker>(
from: lockerPath
) {
// Retrieve the NFT type's cadence ID from the locker
if let cadenceID = locker.getCadenceID(from: id) {
// Resolve the requested view for the given NFT type, returning nil if the view is not supported or the NFT
// is not locked in escrow
return locker.borrowViewResolver(id: cadenceID)?.resolveView(view)
}
}

// Return nil if no locker was found for the given NFT type
return nil
}
34 changes: 34 additions & 0 deletions cadence/scripts/escrow/resolve_locked_vault_metadata.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import "NonFungibleToken"
import "MetadataViews"

import "FlowEVMBridgeTokenEscrow"
import "FlowEVMBridgeUtils"

/// Resolves the view for the requested locked Vault or nil if the Vault is not locked in escrow
/// NOTE: This functionality is not available via the escrow contract as `resolveView` is not a `view` method, but the
/// escrow contract does provide the necessary functionality to resolve the view from the context of a script
///
/// @param bridgeAddress: The address of the bridge contract (included as the VM bridge address varies across networks)
/// @param vaultTypeIdentifier: The identifier of the Vault type
/// @param viewIdentifier: The identifier of the view to resolve
///
/// @return The resolved view if the Vault is escrowed & the view is resolved by it or nil if the Vault is not locked
///
access(all) fun main(bridgeAddress: Address, vaultTypeIdentifier: String, viewIdentifier: String): AnyStruct? {
// Construct runtime types from provided identifiers
let vaultType: Type = CompositeType(vaultTypeIdentifier) ?? panic("Malformed vault type identifier=".concat(vaultTypeIdentifier))
let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier=".concat(viewIdentifier))

// Derive the Locker path for the given Vault type
let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: vaultType)
?? panic("Problem deriving Locker path for NFT type identifier=".concat(vaultTypeIdentifier))

// Borrow the locker from the bridge account's storage & return the requested view if the locker exists
if let locker = getAuthAccount<auth(BorrowValue) &Account>(bridgeAddress).storage.borrow<&FlowEVMBridgeTokenEscrow.Locker>(
from: lockerPath
) {
return locker.resolveView(view)
}

return nil
}
18 changes: 18 additions & 0 deletions cadence/tests/flow_evm_bridge_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import BlockchainHelpers

import "FungibleToken"
import "NonFungibleToken"
import "MetadataViews"
import "FungibleTokenMetadataViews"
import "ExampleNFT"
import "ExampleToken"
import "FlowStorageFees"
Expand Down Expand Up @@ -1053,6 +1055,12 @@ fun testBridgeCadenceNativeNFTToEVMSucceeds() {
)
Test.expect(isOwnerResult, Test.beSucceeded())
Test.assertEqual(true, isOwnerResult.returnValue as! Bool? ?? panic("Problem getting owner status"))

let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID)
Test.assertEqual(true, isNFTLocked)

let metadata = resolveLockedNFTView(bridgeAddress: bridgeAccount.address, nftTypeIdentifier: exampleNFTIdentifier, id: UInt256(mintedNFTID), viewIdentifier: Type<MetadataViews.Display>().identifier)
Test.assert(metadata != nil, message: "Expected NFT metadata to be resolved from escrow but none was returned")
}

access(all)
Expand Down Expand Up @@ -1091,6 +1099,9 @@ fun testCrossVMTransferCadenceNativeNFTFromEVMSucceeds() {
let bobOwnedIDs = getIDs(ownerAddr: bob.address, storagePathIdentifier: "cadenceExampleNFTCollection")
Test.assertEqual(1, bobOwnedIDs.length)
Test.assertEqual(mintedNFTID, bobOwnedIDs[0])

let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID)
Test.assertEqual(false, isNFTLocked)
}

access(all)
Expand Down Expand Up @@ -1260,6 +1271,13 @@ fun testBridgeCadenceNativeTokenToEVMSucceeds() {
let expectedEVMBalance = ufix64ToUInt256(exampleTokenMintAmount, decimals: decimals)
let evmBalance = balanceOf(evmAddressHex: aliceCOAAddressHex, erc20AddressHex: associatedEVMAddressHex)
Test.assertEqual(expectedEVMBalance, evmBalance)

// Confirm the token is locked
let lockedBalance = getLockedTokenBalance(vaultTypeIdentifier: exampleTokenIdentifier) ?? panic("Problem getting locked balance")
Test.assertEqual(exampleTokenMintAmount, lockedBalance)

let metadata = resolveLockedTokenView(bridgeAddress: bridgeAccount.address, vaultTypeIdentifier: exampleTokenIdentifier, viewIdentifier: Type<FungibleTokenMetadataViews.FTDisplay>().identifier)
Test.assert(metadata != nil, message: "Expected Vault metadata to be resolved from escrow but none was returned")
}

access(all)
Expand Down
40 changes: 40 additions & 0 deletions cadence/tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,46 @@ fun evmAddressRequiresOnboarding(_ addressHex: String): Bool? {
return onboardingRequiredResult.returnValue as! Bool? ?? panic("Problem getting onboarding requirement")
}

access(all)
fun isNFTLocked(nftTypeIdentifier: String, id: UInt64): Bool {
let isLockedResult = _executeScript(
"../scripts/escrow/is_nft_locked.cdc",
[nftTypeIdentifier, id]
)
Test.expect(isLockedResult, Test.beSucceeded())
return isLockedResult.returnValue as! Bool? ?? panic("Problem getting locked status")
}

access(all)
fun getLockedTokenBalance(vaultTypeIdentifier: String): UFix64? {
let balanceResult = _executeScript(
"../scripts/escrow/get_locked_token_balance.cdc",
[vaultTypeIdentifier]
)
Test.expect(balanceResult, Test.beSucceeded())
return balanceResult.returnValue as! UFix64?
}

access(all)
fun resolveLockedNFTView(bridgeAddress: Address, nftTypeIdentifier: String, id: UInt256, viewIdentifier: String): AnyStruct? {
let resolvedViewResult = _executeScript(
"../scripts/escrow/resolve_locked_nft_metadata.cdc",
[bridgeAddress, nftTypeIdentifier, id, viewIdentifier]
)
Test.expect(resolvedViewResult, Test.beSucceeded())
return resolvedViewResult.returnValue as! AnyStruct?
}

access(all)
fun resolveLockedTokenView(bridgeAddress: Address, vaultTypeIdentifier: String, viewIdentifier: String): AnyStruct? {
let resolvedViewResult = _executeScript(
"../scripts/escrow/resolve_locked_vault_metadata.cdc",
[bridgeAddress, vaultTypeIdentifier, viewIdentifier]
)
Test.expect(resolvedViewResult, Test.beSucceeded())
return resolvedViewResult.returnValue as! AnyStruct?
}

/* --- Transaction Helpers --- */

access(all)
Expand Down
Loading