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 1 commit
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
32 changes: 26 additions & 6 deletions cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ access(all) contract FlowEVMBridgeNFTEscrow {
return self.borrowLocker(forType: type)?.getEVMID(from: cadenceID) ?? nil
}

/// 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 id: ID of the locked NFT
///
/// @returns The metadata view types supported by the locked NFT if it is in escrow, nil otherwise
///
access(all) view fun getViews(nftType: Type, id: UInt64): [Type]? {
if let nft = self.borrowLockedNFT(type: nftType, id: id) {
return nft.getViews()
}
return nil
}

/// Resolves the requested view type for the given NFT type if it is locked and supports the requested view type
///
/// @param nftType: Type of the locked NFT
Expand All @@ -88,9 +102,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 +115,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 +128,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 +284,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 +300,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
16 changes: 10 additions & 6 deletions cadence/contracts/bridge/FlowEVMBridgeTokenEscrow.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,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 +102,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 +115,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 +162,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)
}
4 changes: 2 additions & 2 deletions cadence/scripts/escrow/resolve_locked_nft_metadata.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import "FlowEVMBridge"
/// @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")
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)
}
19 changes: 19 additions & 0 deletions cadence/scripts/escrow/resolve_locked_vault_metadata.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import "NonFungibleToken"
import "MetadataViews"

import "FlowEVMBridgeTokenEscrow"
import "FlowEVMBridge"

/// Resolves the view for the requested locked Vault or nil if the Vault is not locked in escrow
///
/// @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(vaultTypeIdentifier: String, viewIdentifier: String): AnyStruct? {
let vaultType: Type = CompositeType(vaultTypeIdentifier) ?? panic("Malformed vault type identifier=".concat(vaultTypeIdentifier))
let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier=".concat(viewIdentifier))

return FlowEVMBridgeTokenEscrow.resolveLockedTokenView(tokenType: vaultType, viewType: view)
}
10 changes: 10 additions & 0 deletions cadence/tests/flow_evm_bridge_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,9 @@ 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)
}

access(all)
Expand Down Expand Up @@ -1091,6 +1094,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 +1266,10 @@ 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)
}

access(all)
Expand Down
20 changes: 20 additions & 0 deletions cadence/tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,26 @@ 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?
}

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

access(all)
Expand Down
Loading