Skip to content

Commit

Permalink
Merge pull request #113 from onflow/update-escrow
Browse files Browse the repository at this point in the history
Add metadata discovery & resolution paths to escrow contracts
  • Loading branch information
sisyphusSmiling authored Sep 2, 2024
2 parents f7c98ea + db65396 commit 45c5da5
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 38 deletions.
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

0 comments on commit 45c5da5

Please sign in to comment.