From 279da36874d26d4556d02a1578cce6d51507005c Mon Sep 17 00:00:00 2001 From: vishal <1117327+vishalchangrani@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:50:42 -0400 Subject: [PATCH 1/2] Snapshot of current mainnet-deployed contract versions Downloads all 27 contracts currently deployed on Flow mainnet to establish a baseline for the upcoming contract upgrade review. Core contracts (in this repo): overwritten with mainnet versions External contracts (flow-ft, flow-evm-bridge): added to contracts/external/ Mainnet addresses used: - 0x8624b52f9ddcd04a: FlowIDTableStaking, FlowClusterQC, FlowDKG, FlowEpoch - 0xe467b9dd11fa00df: FlowServiceAccount, NodeVersionBeacon, RandomBeaconHistory, FlowTransactionScheduler, FlowTransactionSchedulerUtils - 0xf919ee77447b7497: FlowFees - 0x1654653399040a61: FlowToken - 0x8d0e87b65159ae63: LockedTokens, FlowStakingCollection - 0x62430cf28c26d095: StakingProxy - 0xf426ff57ee8f6110: FlowExecutionParameters - 0xf233dcee88fe0abe: FungibleToken, FungibleTokenSwitchboard, FungibleTokenMetadataViews - 0x1e4aa0b87d10b141: FlowEVMBridge, FlowEVMBridgeAccessor, FlowEVMBridgeConfig, FlowEVMBridgeCustomAssociations, FlowEVMBridgeHandlers, FlowEVMBridgeUtils, FlowEVMBridgeHandlerInterfaces, IFlowEVMNFTBridge, IFlowEVMTokenBridge Co-Authored-By: Claude Opus 4.6 --- contracts/FlowExecutionParameters.cdc | 9 +- contracts/FlowFees.cdc | 117 +- contracts/FlowIDTableStaking.cdc | 233 ++- contracts/FlowServiceAccount.cdc | 24 +- contracts/FlowStakingCollection.cdc | 131 +- contracts/FlowToken.cdc | 36 +- contracts/FlowTransactionScheduler.cdc | 45 +- contracts/FlowTransactionSchedulerUtils.cdc | 19 +- contracts/LockedTokens.cdc | 67 +- contracts/NodeVersionBeacon.cdc | 18 +- contracts/RandomBeaconHistory.cdc | 20 +- contracts/StakingProxy.cdc | 4 +- contracts/epochs/FlowClusterQC.cdc | 33 +- contracts/epochs/FlowDKG.cdc | 20 +- contracts/epochs/FlowEpoch.cdc | 70 +- contracts/external/FlowEVMBridge.cdc | 1186 ++++++++++++ contracts/external/FlowEVMBridgeAccessor.cdc | 202 +++ contracts/external/FlowEVMBridgeConfig.cdc | 683 +++++++ .../FlowEVMBridgeCustomAssociations.cdc | 212 +++ .../FlowEVMBridgeHandlerInterfaces.cdc | 207 +++ contracts/external/FlowEVMBridgeHandlers.cdc | 468 +++++ contracts/external/FlowEVMBridgeUtils.cdc | 1600 +++++++++++++++++ contracts/external/FungibleToken.cdc | 332 ++++ .../external/FungibleTokenMetadataViews.cdc | 186 ++ .../external/FungibleTokenSwitchboard.cdc | 376 ++++ contracts/external/IFlowEVMNFTBridge.cdc | 119 ++ contracts/external/IFlowEVMTokenBridge.cdc | 112 ++ 27 files changed, 6069 insertions(+), 460 deletions(-) create mode 100644 contracts/external/FlowEVMBridge.cdc create mode 100644 contracts/external/FlowEVMBridgeAccessor.cdc create mode 100644 contracts/external/FlowEVMBridgeConfig.cdc create mode 100644 contracts/external/FlowEVMBridgeCustomAssociations.cdc create mode 100644 contracts/external/FlowEVMBridgeHandlerInterfaces.cdc create mode 100644 contracts/external/FlowEVMBridgeHandlers.cdc create mode 100644 contracts/external/FlowEVMBridgeUtils.cdc create mode 100644 contracts/external/FungibleToken.cdc create mode 100644 contracts/external/FungibleTokenMetadataViews.cdc create mode 100644 contracts/external/FungibleTokenSwitchboard.cdc create mode 100644 contracts/external/IFlowEVMNFTBridge.cdc create mode 100644 contracts/external/IFlowEVMTokenBridge.cdc diff --git a/contracts/FlowExecutionParameters.cdc b/contracts/FlowExecutionParameters.cdc index 7cdb1dc5e..3375a47eb 100644 --- a/contracts/FlowExecutionParameters.cdc +++ b/contracts/FlowExecutionParameters.cdc @@ -1,21 +1,18 @@ -/// FlowExecutionParameters stores the parameters for metering -/// transaction fees for Flow transactions - access(all) contract FlowExecutionParameters { - // Gets Execution Effort Weights from the parameters account's storage + // Gets Execution Effort Weights from the service account's storage access(all) view fun getExecutionEffortWeights(): {UInt64: UInt64} { return self.account.storage.copy<{UInt64: UInt64}>(from: /storage/executionEffortWeights) ?? panic("execution effort weights not set yet") } - // Gets Execution Memory Weights from the parameters account's storage + // Gets Execution Memory Weights from the service account's storage access(all) view fun getExecutionMemoryWeights(): {UInt64: UInt64} { return self.account.storage.copy<{UInt64: UInt64}>(from: /storage/executionMemoryWeights) ?? panic("execution memory weights not set yet") } - // Gets Execution Memory Limit from the parameters account's storage + // Gets Execution Memory Limit from the service account's storage access(all) view fun getExecutionMemoryLimit(): UInt64 { return self.account.storage.copy(from: /storage/executionMemoryLimit) ?? panic("execution memory limit not set yet") diff --git a/contracts/FlowFees.cdc b/contracts/FlowFees.cdc index 515d401f9..7d90414c4 100644 --- a/contracts/FlowFees.cdc +++ b/contracts/FlowFees.cdc @@ -1,6 +1,6 @@ -import "FungibleToken" -import "FlowToken" -import "FlowStorageFees" +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import FlowStorageFees from 0xe467b9dd11fa00df access(all) contract FlowFees { @@ -28,23 +28,7 @@ access(all) contract FlowFees { /// Get the balance of the Fees Vault access(all) fun getFeeBalance(): UFix64 { - let childFeeAccounts = self.account.storage.borrow<&[Capability]>(from: /storage/ChildFeeAccounts) - - // fallback in case no child accounts were created yet - if childFeeAccounts == nil || childFeeAccounts!.length == 0 { - return self.vault.balance - } - - var totalFees = 0.0 - totalFees = totalFees + self.vault.balance - - for feeAccountRef in childFeeAccounts! { - if let feeAccount = feeAccountRef.borrow() { - totalFees = totalFees + feeAccount.availableBalance - } - } - - return totalFees + return self.vault.balance } access(all) resource Administrator { @@ -52,61 +36,9 @@ access(all) contract FlowFees { // // Allows the administrator to withdraw tokens from the fee vault access(all) fun withdrawTokensFromFeeVault(amount: UFix64): @{FungibleToken.Vault} { - var remainingAmount = amount - var withdrawAmount = 0.0 - if FlowFees.vault.balance < remainingAmount { - withdrawAmount = FlowFees.vault.balance - } else { - withdrawAmount = remainingAmount - } - remainingAmount = remainingAmount - withdrawAmount - var vault <- FlowFees.vault.withdraw(amount: withdrawAmount) - - let childFeeAccounts = FlowFees.account.storage.borrow<&[Capability]>(from: /storage/ChildFeeAccounts) - - // fallback in case no child accounts were created yet - if childFeeAccounts == nil || childFeeAccounts!.length == 0 { - if remainingAmount > 0.0 { - panic("Cannot withdraw the requested amount of fee tokens. The amount of FLOW of \(amount) requested to withdraw is greater than the total fees in the fee vaults.") - } - if vault.balance != amount { - // unreachable - panic("Unexpected return vault balance!") - } - - emit TokensWithdrawn(amount: amount) - return <- vault - } - - var accountIndex = 0; - while accountIndex < childFeeAccounts!.length && remainingAmount > 0.0 { - if let feeAccount = childFeeAccounts![accountIndex].borrow() { - if let childVaultRef = feeAccount.storage.borrow(from: /storage/flowTokenVault) { - let availableBalance = feeAccount.availableBalance - - var withdrawAmount = 0.0 - if availableBalance < remainingAmount { - withdrawAmount = availableBalance - } else { - withdrawAmount = remainingAmount - } - remainingAmount = remainingAmount - withdrawAmount - vault.deposit(from: <- childVaultRef.withdraw(amount: withdrawAmount)) - } - } - accountIndex = accountIndex + 1 - } - - if remainingAmount > 0.0 { - panic("Cannot withdraw the requested amount of fee tokens. The amount of FLOW of \(amount) requested to withdraw is greater than the total fees in the fee vaults.") - } - if vault.balance != amount { - // unreachable - panic("Unexpected return vault balance!") - } - + let vault <- FlowFees.vault.withdraw(amount: amount) emit TokensWithdrawn(amount: amount) - return <- vault + return <-vault } /// Allows the administrator to change all the fee parameters at once @@ -211,7 +143,7 @@ access(all) contract FlowFees { } let tokenVault = acct.storage.borrow(from: /storage/flowTokenVault) - ?? panic("FlowFees.deductTransactionFee: Unable to borrow reference to the default token vault") + ?? panic("Unable to borrow reference to the default token vault") if feeAmount > tokenVault.balance { @@ -224,39 +156,14 @@ access(all) contract FlowFees { } let feeVault <- tokenVault.withdraw(amount: feeAmount) - - self.collectFeesOnChildAccounts(<- feeVault) + self.vault.deposit(from: <-feeVault) // The fee calculation can be reconstructed using the data from this event and the FeeParameters at the block when the event happened emit FeesDeducted(amount: feeAmount, inclusionEffort: inclusionEffort, executionEffort: executionEffort) } - /// The supplied vault will go to one child fee account according to the current number of child fee accounts and the current transaction index. - /// if there are no child accounts, the fees will be collected in the self.vault - access(self) fun collectFeesOnChildAccounts(_ vault: @{FungibleToken.Vault}) { - let childFeeAccounts = self.account.storage.borrow<&[Capability]>(from: /storage/ChildFeeAccounts) - - // fallback in case no child accounts were created yet - if childFeeAccounts == nil || childFeeAccounts!.length == 0 { - self.vault.deposit(from: <-vault) - return - } - - let txIndex = getTransactionIndex() - let accountIndex = Int(txIndex % UInt32(childFeeAccounts!.length)) - - if let feeAccount = childFeeAccounts![accountIndex].borrow() { - if let receiver = feeAccount.capabilities.borrow<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) { - receiver.deposit(from: <-vault) - return - } - } - // fallback in case there is a problem borrowing a child account - self.vault.deposit(from: <-vault) - } - access(all) view fun getFeeParameters(): FeeParameters { - return self.account.storage.copy(from: /storage/FlowTxFeeParameters) ?? panic("FlowFees.getFeeParameters: Error getting tx fee parameters. They need to be initialized first!") + return self.account.storage.copy(from: /storage/FlowTxFeeParameters) ?? panic("Error getting tx fee parameters. They need to be initialized first!") } access(self) fun setFeeParameters(_ feeParameters: FeeParameters) { @@ -275,11 +182,11 @@ access(all) contract FlowFees { return totalFees } - init() { + init(adminAccount: auth(SaveValue) &Account) { // Create a new FlowToken Vault and save it in storage self.vault <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) as! @FlowToken.Vault let admin <- create Administrator() - self.account.storage.save(<-admin, to: /storage/flowFeesAdmin) + adminAccount.storage.save(<-admin, to: /storage/flowFeesAdmin) } -} +} \ No newline at end of file diff --git a/contracts/FlowIDTableStaking.cdc b/contracts/FlowIDTableStaking.cdc index f22e9b49c..e46ac7495 100644 --- a/contracts/FlowIDTableStaking.cdc +++ b/contracts/FlowIDTableStaking.cdc @@ -27,10 +27,10 @@ */ -import "FungibleToken" -import "FlowToken" -import "Burner" -import "FlowFees" +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import FlowFees from 0xf919ee77447b7497 +import Burner from 0xf233dcee88fe0abe import Crypto access(all) contract FlowIDTableStaking { @@ -143,10 +143,7 @@ access(all) contract FlowIDTableStaking { /// List of delegators for this node operator access(all) let delegators: @{UInt32: DelegatorRecord} - /// The incrementing ID used to register new delegators. - /// NOTE: delegatorIDs are scoped per node — the unique identifier for a delegator is - /// the pair (nodeID, delegatorID), not delegatorID alone. Two delegators at different - /// nodes may share the same delegatorID without any collision or conflict. + /// The incrementing ID used to register new delegators access(all) var delegatorIDCounter: UInt32 /// The amount of tokens that this node has requested to unstake for the next epoch @@ -165,16 +162,16 @@ access(all) contract FlowIDTableStaking { tokensCommitted: @{FungibleToken.Vault} ) { pre { - id.length == 64: "FlowIDTableStaking.NodeRecord.init: Node ID length must be 32 bytes (64 hex characters) but got \(id.length)" - FlowIDTableStaking.isValidNodeID(id): "FlowIDTableStaking.NodeRecord.init: The node ID must have only numbers and lowercase hex characters but got \(id)" - FlowIDTableStaking.nodes[id] == nil: "FlowIDTableStaking.NodeRecord.init: The node ID \(id) already exists in the identity table and cannot be used again" - role >= UInt8(1) && role <= UInt8(5): "FlowIDTableStaking.NodeRecord.init: The role must be 1, 2, 3, 4, or 5 but got \(role)" - FlowIDTableStaking.isValidNetworkingAddress(address: networkingAddress): "FlowIDTableStaking.NodeRecord.init: The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address, but got \(networkingAddress)" - networkingKey.length == 128: "FlowIDTableStaking.NodeRecord.init: The networkingKey length must be exactly 64 bytes (128 hex characters) but got \(networkingKey.length)" - stakingKey.length == 192: "FlowIDTableStaking.NodeRecord.init: The stakingKey length must be exactly 96 bytes (192 hex characters) but got \(stakingKey.length)" - !FlowIDTableStaking.getNetworkingAddressClaimed(address: networkingAddress): "FlowIDTableStaking.NodeRecord.init: The networkingAddress \(networkingAddress) has already been claimed by another node and cannot be used again" - !FlowIDTableStaking.getNetworkingKeyClaimed(key: networkingKey): "FlowIDTableStaking.NodeRecord.init: The networkingKey \(networkingKey) has already been claimed by another node and cannot be used again" - !FlowIDTableStaking.getStakingKeyClaimed(key: stakingKey): "FlowIDTableStaking.NodeRecord.init: The stakingKey \(stakingKey) has already been claimed by another node and cannot be used again" + id.length == 64: "Node ID length must be 32 bytes (64 hex characters)" + FlowIDTableStaking.isValidNodeID(id): "The node ID must have only numbers and lowercase hex characters" + FlowIDTableStaking.nodes[id] == nil: "The ID cannot already exist in the record" + role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" + FlowIDTableStaking.isValidNetworkingAddress(address: networkingAddress): "The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address" + networkingKey.length == 128: "The networkingKey length must be exactly 64 bytes (128 hex characters)" + stakingKey.length == 192: "The stakingKey length must be exactly 96 bytes (192 hex characters)" + !FlowIDTableStaking.getNetworkingAddressClaimed(address: networkingAddress): "The networkingAddress cannot have already been claimed" + !FlowIDTableStaking.getNetworkingKeyClaimed(key: networkingKey): "The networkingKey cannot have already been claimed" + !FlowIDTableStaking.getStakingKeyClaimed(key: stakingKey): "The stakingKey cannot have already been claimed" } let stakeKey = PublicKey( @@ -185,8 +182,10 @@ access(all) contract FlowIDTableStaking { // Verify the proof of possesion of the private staking key assert( stakeKey.verifyPoP(stakingKeyPoP.decodeHex()), - message: - "FlowIDTableStaking.NodeRecord.init: Cannot create node with ID \(id). The Proof of Possession (\(stakingKeyPoP)) for the node's staking key (\(stakingKey)) is invalid" + message: + "FlowIDTableStaking.NodeRecord.init: Cannot create node with ID " + .concat(id).concat(". The Proof of Possession (").concat(stakingKeyPoP) + .concat(") for the node's staking key (").concat(") is invalid") ) let netKey = PublicKey( @@ -230,7 +229,7 @@ access(all) contract FlowIDTableStaking { access(account) view fun borrowDelegatorRecord(_ delegatorID: UInt32): auth(FungibleToken.Withdraw) &DelegatorRecord { pre { self.delegators[delegatorID] != nil: - "FlowIDTableStaking.NodeRecord.borrowDelegatorRecord: Specified delegator ID \(delegatorID) does not exist in the record" + "Specified delegator ID does not exist in the record" } return (&self.delegators[delegatorID] as auth(FungibleToken.Withdraw) &DelegatorRecord?)! } @@ -437,9 +436,9 @@ access(all) contract FlowIDTableStaking { /// Change the node's networking address to a new one access(NodeOperator) fun updateNetworkingAddress(_ newAddress: String) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.updateNetworkingAddress: Cannot update networking address if the staking auction isn't in progress" - FlowIDTableStaking.isValidNetworkingAddress(address: newAddress): "FlowIDTableStaking.NodeStaker.updateNetworkingAddress: The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address" - !FlowIDTableStaking.getNetworkingAddressClaimed(address: newAddress): "FlowIDTableStaking.NodeStaker.updateNetworkingAddress: The networkingAddress cannot have already been claimed" + FlowIDTableStaking.stakingEnabled(): "Cannot update networking address if the staking auction isn't in progress" + FlowIDTableStaking.isValidNetworkingAddress(address: newAddress): "The networkingAddress must be a valid domain name with a port (e.g., node.flow.com:3569), must not exceed 510 characters, and cannot be an IP address" + !FlowIDTableStaking.getNetworkingAddressClaimed(address: newAddress): "The networkingAddress cannot have already been claimed" } // Borrow the node's record from the staking contract @@ -457,7 +456,7 @@ access(all) contract FlowIDTableStaking { /// Add new tokens to the system to stake during the next epoch access(NodeOperator) fun stakeNewTokens(_ tokens: @{FungibleToken.Vault}) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.stakeNewTokens: Cannot stake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress" } // Borrow the node's record from the staking contract @@ -480,7 +479,7 @@ access(all) contract FlowIDTableStaking { /// Stake tokens that are in the tokensUnstaked bucket access(NodeOperator) fun stakeUnstakedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.stakeUnstakedTokens: Cannot stake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -514,7 +513,7 @@ access(all) contract FlowIDTableStaking { /// Stake tokens that are in the tokensRewarded bucket access(NodeOperator) fun stakeRewardedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.stakeRewardedTokens: Cannot stake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot stake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -535,7 +534,7 @@ access(all) contract FlowIDTableStaking { /// Request amount tokens to be removed from staking at the end of the next epoch access(NodeOperator) fun requestUnstaking(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.requestUnstaking: Cannot unstake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot unstake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -546,7 +545,7 @@ access(all) contract FlowIDTableStaking { nodeRecord.tokensStaked.balance + nodeRecord.tokensCommitted.balance >= amount + nodeRecord.tokensRequestedToUnstake, - message: "FlowIDTableStaking.NodeStaker.requestUnstaking: Not enough tokens to unstake! Only have \(nodeRecord.tokensStaked.balance + nodeRecord.tokensCommitted.balance) tokens but requested to unstake \(amount + nodeRecord.tokensRequestedToUnstake) tokens" + message: "Not enough tokens to unstake!" ) // Node operators who have delegators have to have enough of their own tokens staked @@ -554,7 +553,7 @@ access(all) contract FlowIDTableStaking { assert ( nodeRecord.delegators.length == 0 || FlowIDTableStaking.isGreaterThanMinimumForRole(numTokens: FlowIDTableStaking.NodeInfo(nodeID: nodeRecord.id).totalCommittedWithoutDelegators() - amount, role: nodeRecord.role), - message: "FlowIDTableStaking.NodeStaker.requestUnstaking: Cannot unstake below the minimum stake requirement if there are delegators." + message: "Cannot unstake below the minimum if there are delegators" ) let amountCommitted = nodeRecord.tokensCommitted.balance @@ -585,14 +584,14 @@ access(all) contract FlowIDTableStaking { // Remove the node as a candidate node if they were one before but aren't now if !self.isEligibleForCandidateNodeStatus(nodeRecord) { FlowIDTableStaking.removeFromCandidateNodeList(nodeID: self.id, role: nodeRecord.role) - } + } } /// Requests to unstake all of the node operators staked and committed tokens /// as well as all the staked and committed tokens of all of their delegators access(NodeOperator) fun unstakeAll() { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.unstakeAll: Cannot unstake if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot unstake if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.id) @@ -639,7 +638,7 @@ access(all) contract FlowIDTableStaking { } /// Public interface to query information about a delegator - /// from the account it is stored in + /// from the account it is stored in access(all) resource interface NodeDelegatorPublic { access(all) let id: UInt32 access(all) let nodeID: String @@ -661,7 +660,7 @@ access(all) contract FlowIDTableStaking { /// Delegate new tokens to the node operator access(DelegatorOwner) fun delegateNewTokens(from: @{FungibleToken.Vault}) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.delegateNewTokens: Cannot delegate if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress" } // borrow the node record of the node in order to get the delegator record @@ -679,7 +678,7 @@ access(all) contract FlowIDTableStaking { /// Delegate tokens from the unstaked bucket to the node operator access(DelegatorOwner) fun delegateUnstakedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.delegateUnstakedTokens: Cannot delegate if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID) @@ -708,7 +707,7 @@ access(all) contract FlowIDTableStaking { /// Delegate tokens from the rewards bucket to the node operator access(DelegatorOwner) fun delegateRewardedTokens(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.delegateRewardedTokens: Cannot delegate if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot delegate if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID) @@ -724,7 +723,7 @@ access(all) contract FlowIDTableStaking { /// Request to unstake delegated tokens during the next epoch access(DelegatorOwner) fun requestUnstaking(amount: UFix64) { pre { - FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeDelegator.requestUnstaking: Cannot request unstaking if the staking auction isn't in progress" + FlowIDTableStaking.stakingEnabled(): "Cannot request unstaking if the staking auction isn't in progress" } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(self.nodeID) @@ -735,7 +734,7 @@ access(all) contract FlowIDTableStaking { delRecord.tokensStaked.balance + delRecord.tokensCommitted.balance >= amount + delRecord.tokensRequestedToUnstake, - message: "FlowIDTableStaking.NodeDelegator.requestUnstaking: Not enough tokens to unstake! Only have \(delRecord.tokensStaked.balance + delRecord.tokensCommitted.balance) tokens but requested to unstake \(amount + delRecord.tokensRequestedToUnstake) tokens" + message: "Not enough tokens to unstake!" ) // if the request can come from committed, withdraw from committed to unstaked @@ -818,7 +817,7 @@ access(all) contract FlowIDTableStaking { self.delegatorRewards[delegatorID] = reward * scalingFactor } } - + access(all) fun scaleOperatorRewards(scalingFactor: UFix64) { self.nodeRewards = self.nodeRewards * scalingFactor } @@ -858,7 +857,7 @@ access(all) contract FlowIDTableStaking { access(all) fun setMinimumStakeRequirements(_ newRequirements: {UInt8: UFix64}) { pre { newRequirements.keys.length == 5: - "FlowIDTableStaking.Admin.setMinimumStakeRequirements: There must be five entries for node minimum stake requirements but got \(newRequirements.keys.length)" + "There must be six entries for node minimum stake requirements" } FlowIDTableStaking.minimumStakeRequired = newRequirements emit NewStakingMinimums(newMinimums: newRequirements) @@ -884,7 +883,7 @@ access(all) contract FlowIDTableStaking { access(all) fun setCutPercentage(_ newCutPercentage: UFix64) { pre { newCutPercentage > 0.0 && newCutPercentage < 1.0: - "FlowIDTableStaking.Admin.setCutPercentage: Cut percentage must be between 0 and 1 but got \(newCutPercentage)" + "Cut percentage must be between 0 and 1!" } if newCutPercentage != FlowIDTableStaking.nodeDelegatingRewardCut { emit NewDelegatorCutPercentage(newCutPercentage: newCutPercentage) @@ -892,10 +891,10 @@ access(all) contract FlowIDTableStaking { FlowIDTableStaking.nodeDelegatingRewardCut = newCutPercentage } - /// Sets new limits to the number of candidate nodes for an epoch for a specific role + /// Sets new limits to the number of candidate nodes for an epoch access(all) fun setCandidateNodeLimit(role: UInt8, newLimit: UInt64) { pre { - role >= UInt8(1) && role <= UInt8(5): "FlowIDTableStaking.Admin.setCandidateNodeLimit: The role must be 1, 2, 3, 4, or 5 but got \(role)" + role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" } let candidateNodeLimits = FlowIDTableStaking.account.storage.load<{UInt8: UInt64}>(from: /storage/idTableCandidateNodeLimits)! @@ -909,12 +908,12 @@ access(all) contract FlowIDTableStaking { /// even if the number of participant nodes exceeds the slot limit. access(all) fun setSlotLimits(slotLimits: {UInt8: UInt16}) { pre { - slotLimits.keys.length == 5: "FlowIDTableStaking.Admin.setSlotLimits: Slot Limits Dictionary can only have 5 entries but got \(slotLimits.keys.length)" - slotLimits[1] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for collector nodes" - slotLimits[2] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for consensus nodes" - slotLimits[3] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for execution nodes" - slotLimits[4] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for verification nodes" - slotLimits[5] != nil: "FlowIDTableStaking.Admin.setSlotLimits: Need to have a limit set for access nodes" + slotLimits.keys.length == 5: "Slot Limits Dictionary can only have 5 entries" + slotLimits[1] != nil: "Need to have a limit set for collector nodes" + slotLimits[2] != nil: "Need to have a limit set for consensus nodes" + slotLimits[3] != nil: "Need to have a limit set for execution nodes" + slotLimits[4] != nil: "Need to have a limit set for verification nodes" + slotLimits[5] != nil: "Need to have a limit set for access nodes" } FlowIDTableStaking.account.storage.load<{UInt8: UInt16}>(from: /storage/flowStakingSlotLimits) @@ -926,7 +925,7 @@ access(all) contract FlowIDTableStaking { /// but other node types will be added in the future access(all) fun setOpenNodeSlots(openSlots: {UInt8: UInt16}) { pre { - openSlots[5] != nil: "FlowIDTableStaking.Admin.setOpenNodeSlots: Need to have a value set for access nodes" + openSlots[5] != nil: "Need to have a value set for access nodes" } FlowIDTableStaking.account.storage.load<{UInt8: UInt16}>(from: /storage/flowStakingOpenNodeSlots) @@ -934,7 +933,7 @@ access(all) contract FlowIDTableStaking { } /// Sets a list of node IDs who will not receive rewards for the current epoch - /// This is used during epochs to punish nodes who have poor uptime + /// This is used during epochs to punish nodes who have poor uptime /// or who do not update to latest node software quickly enough /// The parameter is a dictionary mapping node IDs /// to a percentage, which is the percentage of their expected rewards that @@ -943,7 +942,7 @@ access(all) contract FlowIDTableStaking { for percentage in nodeIDs.values { assert( percentage >= 0.0 && percentage < 1.0, - message: "FlowIDTableStaking.Admin.setNonOperationalNodesList: Percentage value to decrease rewards payout should be between 0 and 1 but got \(percentage)" + message: "Percentage value to decrease rewards payout should be between 0 and 1" ) } FlowIDTableStaking.account.storage.load<{String: UFix64}>(from: /storage/idTableNonOperationalNodesList) @@ -954,7 +953,7 @@ access(all) contract FlowIDTableStaking { /// if their staked amount changes or if they are removed access(all) fun setNodeWeight(nodeID: String, weight: UInt64) { if weight > 100 { - panic("FlowIDTableStaking.Admin.setNodeWeight: Specified node weight out of range. Must be between 0 and 100 but got \(weight)") + panic("Specified node weight out of range.") } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) @@ -967,18 +966,18 @@ access(all) contract FlowIDTableStaking { /// and not considered to be a proposed/staked node access(all) fun setApprovedList(_ newApproveList: {String: Bool}) { let currentApproveList = FlowIDTableStaking.getApprovedList() - ?? panic("FlowIDTableStaking.Admin.setApprovedList: Could not load approve list from storage") + ?? panic("Could not load approve list from storage") - for id in newApproveList { + for id in newApproveList.keys { if FlowIDTableStaking.nodes[id] == nil { - panic("FlowIDTableStaking.Admin.setApprovedList: Approved node \(id) does not already exist in the identity table") + panic("Approved node ".concat(id).concat(" does not already exist in the identity table")) } } // If one of the nodes has been removed from the approve list // it need to be set as movesPending so it will be caught in the `removeInvalidNodes` method // If this happens not during the staking auction, the node should be removed and marked to unstake immediately - for id in currentApproveList { + for id in currentApproveList.keys { if newApproveList[id] == nil { if FlowIDTableStaking.stakingEnabled() { FlowIDTableStaking.modifyNewMovesPending(nodeID: id, delegatorID: nil, existingList: nil) @@ -993,7 +992,7 @@ access(all) contract FlowIDTableStaking { /// Sets the approved list without validating it (requires caller to validate) access(self) fun unsafeSetApprovedList(_ newApproveList: {String: Bool}) { let currentApproveList = FlowIDTableStaking.account.storage.load<{String: Bool}>(from: /storage/idTableApproveList) - ?? panic("FlowIDTableStaking.Admin.unsafeSetApprovedList: Could not load the current approve list from storage") + ?? panic("Could not load the current approve list from storage") FlowIDTableStaking.account.storage.save<{String: Bool}>(newApproveList, to: /storage/idTableApproveList) } @@ -1026,7 +1025,7 @@ access(all) contract FlowIDTableStaking { } var movesPendingList = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableMovesPendingList) - ?? panic("FlowIDTableStaking.Admin.unsafeRemoveAndRefundNodeRecord: No moves pending list in account storage") + ?? panic("No moves pending list in account storage") // Iterate through all delegators and unstake their tokens // since their node has unstaked @@ -1065,7 +1064,7 @@ access(all) contract FlowIDTableStaking { access(all) fun removeAndRefundNodeRecord(_ nodeID: String) { // remove the refunded node from the approve list let approveList = FlowIDTableStaking.getApprovedList() - ?? panic("FlowIDTableStaking.Admin.removeAndRefundNodeRecord: Could not load approve list from storage") + ?? panic("Could not load approve list from storage") approveList.remove(key: nodeID) self.unsafeSetApprovedList(approveList) self.unsafeRemoveAndRefundNodeRecord(nodeID) @@ -1098,13 +1097,13 @@ access(all) contract FlowIDTableStaking { /// it moves their committed tokens to their unstaked bucket access(all) fun removeInvalidNodes(): {String: Bool} { let approvedNodeIDs = FlowIDTableStaking.getApprovedList() - ?? panic("FlowIDTableStaking.Admin.removeInvalidNodes: Could not read the approve list from storage") + ?? panic("Could not read the approve list from storage") let movesPendingList = FlowIDTableStaking.getMovesPendingList() - ?? panic("FlowIDTableStaking.Admin.removeInvalidNodes: Could not copy moves pending list from storage") + ?? panic("Could not copy moves pending list from storage") let participantList = FlowIDTableStaking.getParticipantNodeList() - ?? panic("FlowIDTableStaking.Admin.removeInvalidNodes: Could not copy participant list from storage") + ?? panic("Could not copy participant list from storage") // We only iterate through movesPendingList here because any node // that has insufficient stake committed will be because it has submitted @@ -1113,7 +1112,7 @@ access(all) contract FlowIDTableStaking { // to get their initialWeight set to 100 // Nodes removed from the approve list are already refunded at the time // of removal in the setApprovedList method - for nodeID in movesPendingList { + for nodeID in movesPendingList.keys { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) let totalTokensCommitted = nodeRecord.nodeFullCommittedBalance() @@ -1132,7 +1131,7 @@ access(all) contract FlowIDTableStaking { // permissionless node roles (access) // NOTE: Access nodes which registered prior to the 100-FLOW stake requirement - // (which must be approved) are not removed during a temporary grace period during + // (which must be approved) are not removed during a temporary grace period during // which these grandfathered node operators may submit the necessary stake requirement. // Therefore Access nodes must either be approved OR have sufficient stake: // - Old ANs must be approved, but are allowed to have zero stake @@ -1153,9 +1152,9 @@ access(all) contract FlowIDTableStaking { /// so if there are more candidate nodes for that role than there are slots /// nodes are randomly selected from the list to be included. /// Nodes which are not selected for inclusion are removed and refunded in this function. - /// All candidate nodes left staked after this function exits are implicitly selected to fill the + /// All candidate nodes left staked after this function exits are implicitly selected to fill the /// available slots, and will become participants at the next epoch transition. - /// + /// access(all) fun fillNodeRoleSlots(): [String] { var currentNodeCount: {UInt8: UInt16} = FlowIDTableStaking.getCurrentRoleNodeCounts() @@ -1163,7 +1162,7 @@ access(all) contract FlowIDTableStaking { let slotLimits: {UInt8: UInt16} = FlowIDTableStaking.getRoleSlotLimits() let openSlots = FlowIDTableStaking.getOpenNodeSlots() - + let nodesToAdd: [String] = [] // Load and reset the candidate node list @@ -1178,20 +1177,20 @@ access(all) contract FlowIDTableStaking { if currentNodeCount[role]! >= slotLimits[role]! { // if all slots are full, remove and refund all pending nodes - for nodeID in candidateNodesForRole { + for nodeID in candidateNodesForRole.keys { self.removeAndRefundNodeRecord(nodeID) } } else if currentNodeCount[role]! + UInt16(candidateNodesForRole.keys.length) > slotLimits[role]! { - + // Not all slots are full, but addition of all the candidate nodes exceeds the slot limit // Calculate how many nodes to remove from the candidate list for this role var numNodesToRemove: UInt16 = currentNodeCount[role]! + UInt16(candidateNodesForRole.keys.length) - slotLimits[role]! - + let numNodesToAdd = UInt16(candidateNodesForRole.keys.length) - numNodesToRemove // Indicates which indicies in the candidate nodes array will be removed var deletionList: {UInt16: Bool} = {} - + // Randomly select which indicies will be removed while numNodesToRemove > 0 { let selection = revertibleRandom(modulo: UInt16(candidateNodesForRole.keys.length)) @@ -1204,7 +1203,7 @@ access(all) contract FlowIDTableStaking { } // Remove and Refund the selected nodes - for nodeIndex in deletionList { + for nodeIndex in deletionList.keys { let nodeID = candidateNodesForRole.keys[nodeIndex] self.removeAndRefundNodeRecord(nodeID) nodesToRemoveFromCandidateNodes.append(nodeID) @@ -1249,7 +1248,7 @@ access(all) contract FlowIDTableStaking { let rewardsBreakdownArray = rewardsSummary.breakdown let totalRewards = rewardsSummary.totalRewards - + // If there are no node operators to pay rewards to, do not mint new tokens if rewardsBreakdownArray.length == 0 { emit EpochTotalRewardsPaid(total: totalRewards, fromFees: 0.0, minted: 0.0, feesBurned: 0.0, epochCounterForRewards: forEpochCounter) @@ -1259,7 +1258,7 @@ access(all) contract FlowIDTableStaking { self.setNonOperationalNodesList(emptyNodeList) return - } + } let feeBalance = FlowFees.getFeeBalance() var mintedRewards: UFix64 = 0.0 @@ -1274,20 +1273,20 @@ access(all) contract FlowIDTableStaking { // Mint the remaining FLOW for rewards if mintedRewards > 0.0 { let flowTokenMinter = FlowIDTableStaking.account.storage.borrow<&FlowToken.Minter>(from: /storage/flowTokenMinter) - ?? panic("FlowIDTableStaking.NodeStaker.payRewards: Could not borrow minter reference") + ?? panic("Could not borrow minter reference") rewardsVault.deposit(from: <-flowTokenMinter.mintTokens(amount: mintedRewards)) } for rewardBreakdown in rewardsBreakdownArray { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(rewardBreakdown.nodeID) let nodeReward = rewardBreakdown.nodeRewards - + nodeRecord.tokensRewarded.deposit(from: <-rewardsVault.withdraw(amount: nodeReward)) for delegator in rewardBreakdown.delegatorRewards.keys { let delRecord = nodeRecord.borrowDelegatorRecord(delegator) let delegatorReward = rewardBreakdown.delegatorRewards[delegator]! - + delRecord.tokensRewarded.deposit(from: <-rewardsVault.withdraw(amount: delegatorReward)) emit DelegatorRewardsPaid(nodeID: rewardBreakdown.nodeID, delegatorID: delegator, amount: delegatorReward, epochCounter: forEpochCounter) } @@ -1332,7 +1331,7 @@ access(all) contract FlowIDTableStaking { // Iterate through all the non-operational nodes and calculate // their rewards that will be withheld let nonOperationalNodes = FlowIDTableStaking.getNonOperationalNodesList() - for nodeID in nonOperationalNodes { + for nodeID in nonOperationalNodes.keys { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) // Each node's rewards can be decreased to a different percentage @@ -1388,17 +1387,11 @@ access(all) contract FlowIDTableStaking { rewardsBreakdownArray.append(rewardsBreakdown) } - // If all staked nodes are non-operational, the denominator is zero and there are - // no operational nodes to redistribute withheld rewards to, so the scale is zero. - var withheldRewardsScale = 0.0 - let operationalStake = totalStaked - sumStakeFromNonOperationalStakers - if operationalStake > 0.0 { - withheldRewardsScale = sumRewardsWithheld / operationalStake - } + var withheldRewardsScale = sumRewardsWithheld / (totalStaked - sumStakeFromNonOperationalStakers) let totalRewardsPlusWithheld = totalRewardScale + withheldRewardsScale /// iterate through all the nodes to pay - for nodeID in stakedNodeIDs { + for nodeID in stakedNodeIDs.keys { if nonOperationalNodes[nodeID] != nil { continue } let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) @@ -1435,7 +1428,7 @@ access(all) contract FlowIDTableStaking { } rewardsBreakdown.setDelegatorReward(delegatorID: delegator, rewards: delegatorRewardAmount) } - + rewardsBreakdown.setNodeRewards(nodeRewardAmount) rewardsBreakdownArray.append(rewardsBreakdown) } @@ -1452,24 +1445,24 @@ access(all) contract FlowIDTableStaking { /// Unstaking requests are filled by moving those tokens from staked to unstaking access(all) fun moveTokens(newEpochCounter: UInt64) { pre { - !FlowIDTableStaking.stakingEnabled(): "FlowIDTableStaking.NodeStaker.moveTokens: Cannot move tokens if the staking auction is still in progress" + !FlowIDTableStaking.stakingEnabled(): "Cannot move tokens if the staking auction is still in progress" } let approvedNodeIDs = FlowIDTableStaking.getApprovedList() - ?? panic("FlowIDTableStaking.NodeStaker.moveTokens: Could not read the approve list from storage") + ?? panic("Could not read the approve list from storage") let movesPendingNodeIDs = FlowIDTableStaking.account.storage.load<{String: {UInt32: Bool}}>(from: /storage/idTableMovesPendingList) - ?? panic("FlowIDTableStaking.NodeStaker.moveTokens: No moves pending list in account storage") + ?? panic("No moves pending list in account storage") // Reset the movesPendingList var emptyMovesPendingList: {String: {UInt32: Bool}} = {} FlowIDTableStaking.account.storage.save(emptyMovesPendingList, to: /storage/idTableMovesPendingList) let newMovesPendingList = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableMovesPendingList) - ?? panic("FlowIDTableStaking.NodeStaker.moveTokens: No moves pending list in account storage") + ?? panic("No moves pending list in account storage") let stakedNodeIDs: {String: Bool} = FlowIDTableStaking.getParticipantNodeList()! - for nodeID in movesPendingNodeIDs { + for nodeID in movesPendingNodeIDs.keys { let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) let approved = approvedNodeIDs[nodeID] ?? false @@ -1504,7 +1497,7 @@ access(all) contract FlowIDTableStaking { let pendingDelegatorsList = movesPendingNodeIDs[nodeID]! // move all the delegators' tokens between buckets - for delegator in pendingDelegatorsList { + for delegator in pendingDelegatorsList.keys { let delRecord = nodeRecord.borrowDelegatorRecord(delegator) // If the delegator's committed tokens for the next epoch @@ -1578,13 +1571,9 @@ access(all) contract FlowIDTableStaking { { assert ( FlowIDTableStaking.stakingEnabled(), - message: "FlowIDTableStaking.NodeStaker.addNodeRecord: Cannot register a node operator if the staking auction isn't in progress" + message: "Cannot register a node operator if the staking auction isn't in progress" ) - // NOTE: An empty vault is intentionally passed to NodeRecord here. The caller-provided - // tokensCommitted vault is validated against the minimum stake requirement at line below, - // then deposited via stakeNewTokens() immediately after the node is stored. The NodeRecord - // is created first so it exists in storage before tokens are committed to it. let newNode <- create NodeRecord(id: id, role: role, networkingAddress: networkingAddress, @@ -1597,7 +1586,7 @@ access(all) contract FlowIDTableStaking { assert( self.isGreaterThanMinimumForRole(numTokens: tokensCommitted.balance, role: role), - message: "FlowIDTableStaking.NodeStaker.addNodeRecord: The amount of tokens committed for registration of \(tokensCommitted.balance) is not above the minimum (\(minimum)) for the chosen node role (\(role))" + message: "Tokens committed for registration is not above the minimum (".concat(minimum.toString()).concat(") for the chosen node role (".concat(role.toString()).concat(")")) ) FlowIDTableStaking.nodes[id] <-! newNode @@ -1615,25 +1604,25 @@ access(all) contract FlowIDTableStaking { access(all) fun registerNewDelegator(nodeID: String, tokensCommitted: @{FungibleToken.Vault}): @NodeDelegator { assert ( FlowIDTableStaking.stakingEnabled(), - message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: Cannot register a delegator if the staking auction isn't in progress" + message: "Cannot register a delegator if the staking auction isn't in progress" ) let nodeRecord = FlowIDTableStaking.borrowNodeRecord(nodeID) assert ( nodeRecord.role != UInt8(5), - message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: Cannot register a delegator for an access node" + message: "Cannot register a delegator for an access node" ) let minimum = self.getDelegatorMinimumStakeRequirement() assert( tokensCommitted.balance >= minimum, - message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: The amount of tokens committed for registration of \(tokensCommitted.balance) is not above the minimum (\(minimum)) for delegators" + message: "Tokens committed for delegator registration is not above the minimum (".concat(minimum.toString()).concat(")") ) assert ( FlowIDTableStaking.isGreaterThanMinimumForRole(numTokens: nodeRecord.nodeFullCommittedBalance(), role: nodeRecord.role), - message: "FlowIDTableStaking.NodeStaker.registerNewDelegator: Cannot register a delegator because the node operator for ID \(nodeID) is below the minimum stake requirement for their node role (\(nodeRecord.role))" + message: "Cannot register a delegator if the node operator is below the minimum stake" ) // increment the delegator ID counter for this node @@ -1656,7 +1645,7 @@ access(all) contract FlowIDTableStaking { access(account) view fun borrowNodeRecord(_ nodeID: String): auth(FungibleToken.Withdraw) &NodeRecord { pre { FlowIDTableStaking.nodes[nodeID] != nil: - "FlowIDTableStaking.NodeStaker.borrowNodeRecord: Specified node ID \(nodeID) does not exist in the identity table" + "Specified node ID does not exist in the record" } return (&FlowIDTableStaking.nodes[nodeID] as auth(FungibleToken.Withdraw) &NodeRecord?)! } @@ -1664,7 +1653,7 @@ access(all) contract FlowIDTableStaking { /// borrow a reference to the `FlowFees` admin resource for paying rewards access(account) view fun borrowFeesAdmin(): &FlowFees.Administrator { let feesAdmin = self.account.storage.borrow<&FlowFees.Administrator>(from: /storage/flowFeesAdmin) - ?? panic("FlowIDTableStaking.NodeStaker.borrowFeesAdmin: Could not borrow a reference to the FlowFees Admin object") + ?? panic("Could not borrow a reference to the FlowFees Admin object") return feesAdmin } @@ -1673,7 +1662,7 @@ access(all) contract FlowIDTableStaking { /// a piece of node metadata has been claimed by a node access(account) fun updateClaimed(path: StoragePath, _ key: String, claimed: Bool) { let claimedDictionary = self.account.storage.borrow(from: path) - ?? panic("FlowIDTableStaking.NodeStaker.updateClaimed: Invalid path for dictionary") + ?? panic("Invalid path for dictionary") if claimed { claimedDictionary[key] = true @@ -1710,7 +1699,7 @@ access(all) contract FlowIDTableStaking { existingList: auth(Mutate) &{String: {UInt32: Bool}}?) { let movesPendingList = existingList ?? (self.account.storage.borrow(from: /storage/idTableMovesPendingList) - ?? panic("FlowIDTableStaking.NodeStaker.modifyNewMovesPending: No moves pending list in account storage")) + ?? panic("No moves pending list in account storage")) // If there is already a list for the given node ID, overwrite the created one if let existingDelegatorList = movesPendingList.remove(key: nodeID) { @@ -1753,15 +1742,15 @@ access(all) contract FlowIDTableStaking { /// Adds the provided node ID to the candidate node list access(contract) fun addToCandidateNodeList(nodeID: String, roleToAdd: UInt8) { pre { - roleToAdd >= UInt8(1) && roleToAdd <= UInt8(5): "FlowIDTableStaking.NodeStaker.addToCandidateNodeList: The role must be 1, 2, 3, 4, or 5 but got \(roleToAdd)" + roleToAdd >= UInt8(1) && roleToAdd <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" } var candidateNodes = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableCandidateNodes)! var candidateNodesForRole = candidateNodes.remove(key: roleToAdd) - ?? panic("FlowIDTableStaking.NodeStaker.addToCandidateNodeList: Could not get candidate nodes for role: \(roleToAdd)") + ?? panic("Could not get candidate nodes for role: ".concat(roleToAdd.toString())) if UInt64(candidateNodesForRole.keys.length) >= self.getCandidateNodeLimits()![roleToAdd]! { - panic("FlowIDTableStaking.NodeStaker.addToCandidateNodeList: Candidate node limit of \(self.getCandidateNodeLimits()![roleToAdd]!) exceeded for node role \(roleToAdd)") + panic("Candidate node limit exceeded for node role ".concat(roleToAdd.toString())) } candidateNodesForRole[nodeID] = true @@ -1771,14 +1760,14 @@ access(all) contract FlowIDTableStaking { /// Removes the provided node ID from the candidate node list access(contract) fun removeFromCandidateNodeList(nodeID: String, role: UInt8) { pre { - role >= UInt8(1) && role <= UInt8(5): "FlowIDTableStaking.NodeStaker.removeFromCandidateNodeList: The role must be 1, 2, 3, 4, or 5 but got \(role)" + role >= UInt8(1) && role <= UInt8(5): "The role must be 1, 2, 3, 4, or 5" } var candidateNodes = FlowIDTableStaking.account.storage.borrow(from: /storage/idTableCandidateNodes) - ?? panic("FlowIDTableStaking.NodeStaker.removeFromCandidateNodeList: Could not load candidate node list from storage") + ?? panic("Could not load candidate node list from storage") var candidateNodesForRole = candidateNodes.remove(key: role) - ?? panic("FlowIDTableStaking.NodeStaker.removeFromCandidateNodeList: Could not get candidate nodes for role: \(role)") - + ?? panic("Could not get candidate nodes for role: ".concat(role.toString())) + candidateNodesForRole.remove(key: nodeID) candidateNodes[role] = candidateNodesForRole } @@ -1795,7 +1784,7 @@ access(all) contract FlowIDTableStaking { ?? {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} } - /// Gets the number of auto-opened slots for each node role. + /// Gets the number of auto-opened slots for each node role. access(all) fun getOpenNodeSlots(): {UInt8: UInt16} { return FlowIDTableStaking.account.storage.copy<{UInt8: UInt16}>(from: /storage/flowStakingOpenNodeSlots) ?? ({} as {UInt8: UInt16}) @@ -1811,7 +1800,7 @@ access(all) contract FlowIDTableStaking { let roleCounts: {UInt8: UInt16} = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} - for nodeID in participantNodeIDs { + for nodeID in participantNodeIDs.keys { let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: nodeID) roleCounts[nodeInfo.role] = roleCounts[nodeInfo.role]! + 1 } @@ -1903,7 +1892,7 @@ access(all) contract FlowIDTableStaking { let nodeIDs = FlowIDTableStaking.getNodeIDs() let approvedNodeIDs: {String: Bool} = FlowIDTableStaking.getApprovedList() - ?? panic("FlowIDTableStaking.NodeStaker.getProposedNodeIDs: Could not read the approve list from storage") + ?? panic("Could not read the approve list from storage") let proposedNodeIDs: {String: Bool} = {} for nodeID in nodeIDs { @@ -1925,7 +1914,7 @@ access(all) contract FlowIDTableStaking { // permissionless node roles (access) // NOTE: Access nodes which registered prior to the 100-FLOW stake requirement - // (which must be approved) are not removed during a temporary grace period during + // (which must be approved) are not removed during a temporary grace period during // which these grandfathered node operators may submit the necessary stake requirement. // Therefore Access nodes must either be approved OR have sufficient stake: // - Old ANs must be approved, but are allowed to have zero stake @@ -1950,7 +1939,7 @@ access(all) contract FlowIDTableStaking { /// for the specified node role access(all) view fun isGreaterThanMinimumForRole(numTokens: UFix64, role: UInt8): Bool { let minimumStake = self.minimumStakeRequired[role] - ?? panic("FlowIDTableStaking.NodeStaker.isGreaterThanMinimumForRole: Incorrect role provided for minimum stake. Must be 1, 2, 3, 4, or 5 but got \(role)") + ?? panic("Incorrect role provided for minimum stake. Must be 1, 2, 3, 4, or 5") return numTokens >= minimumStake } @@ -1973,7 +1962,7 @@ access(all) contract FlowIDTableStaking { /// Gets the claimed status of a particular piece of node metadata access(account) view fun getClaimed(path: StoragePath, key: String): Bool { let claimedDictionary = self.account.storage.borrow<&{String: Bool}>(from: path) - ?? panic("FlowIDTableStaking.NodeStaker.getClaimed: Invalid path for dictionary") + ?? panic("Invalid path for dictionary") return claimedDictionary[key] ?? false } @@ -1985,7 +1974,7 @@ access(all) contract FlowIDTableStaking { /// Returns the list of node IDs whose rewards will be reduced in the next payment access(all) view fun getNonOperationalNodesList(): {String: UFix64} { return self.account.storage.copy<{String: UFix64}>(from: /storage/idTableNonOperationalNodesList) - ?? panic("FlowIDTableStaking.NodeStaker.getNonOperationalNodesList: Could not get non-operational node list") + ?? panic("could not get non-operational node list") } /// Gets the minimum stake requirements for all the node types diff --git a/contracts/FlowServiceAccount.cdc b/contracts/FlowServiceAccount.cdc index 6f8f3e292..8fc654d75 100644 --- a/contracts/FlowServiceAccount.cdc +++ b/contracts/FlowServiceAccount.cdc @@ -1,8 +1,8 @@ -import "FungibleToken" -import "FlowToken" -import "FlowFees" -import "FlowStorageFees" -import "FlowExecutionParameters" +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import FlowFees from 0xf919ee77447b7497 +import FlowStorageFees from 0xe467b9dd11fa00df +import FlowExecutionParameters from 0xf426ff57ee8f6110 access(all) contract FlowServiceAccount { @@ -56,7 +56,7 @@ access(all) contract FlowServiceAccount { /// Return a reference to the default token vault on an account access(all) view fun defaultTokenVault(_ acct: auth(BorrowValue) &Account): auth(FungibleToken.Withdraw) &FlowToken.Vault { return acct.storage.borrow(from: /storage/flowTokenVault) - ?? panic("FlowServiceAccount.defaultTokenVault: Unable to borrow reference to the default token vault") + ?? panic("Unable to borrow reference to the default token vault") } /// Will be deprecated and can be deleted after the switchover to FlowFees.deductTransactionFee @@ -73,7 +73,7 @@ access(all) contract FlowServiceAccount { if self.transactionFee > tokenVault.balance { feeAmount = tokenVault.balance } - + let feeVault <- tokenVault.withdraw(amount: feeAmount) FlowFees.deposit(from: <-feeVault) } @@ -87,12 +87,12 @@ access(all) contract FlowServiceAccount { ) { if !FlowServiceAccount.isAccountCreator(payer.address) { - panic("FlowServiceAccount.setupNewAccount: Account \(payer.address) is not authorized to create accounts") + panic("Account not authorized to create accounts") } if self.accountCreationFee < FlowStorageFees.minimumStorageReservation { - panic("FlowServiceAccount.setupNewAccount: Account creation fees setup incorrectly") + panic("Account creation fees setup incorrectly") } let tokenVault = self.defaultTokenVault(payer) @@ -127,12 +127,12 @@ access(all) contract FlowServiceAccount { return self.accountCreators.keys } - // Gets Execution Effort Weights from the service account's storage + // Gets Execution Effort Weights from the service account's storage access(all) view fun getExecutionEffortWeights(): {UInt64: UInt64} { return FlowExecutionParameters.getExecutionEffortWeights() } - // Gets Execution Memory Weights from the service account's storage + // Gets Execution Memory Weights from the service account's storage access(all) view fun getExecutionMemoryWeights(): {UInt64: UInt64} { return FlowExecutionParameters.getExecutionMemoryWeights() } @@ -198,4 +198,4 @@ access(all) contract FlowServiceAccount { self.account.storage.save(<-admin, to: /storage/flowServiceAdmin) } -} +} \ No newline at end of file diff --git a/contracts/FlowStakingCollection.cdc b/contracts/FlowStakingCollection.cdc index c56d1db68..debf01604 100644 --- a/contracts/FlowStakingCollection.cdc +++ b/contracts/FlowStakingCollection.cdc @@ -10,15 +10,15 @@ */ -import "FungibleToken" -import "FlowToken" -import "FlowIDTableStaking" -import "LockedTokens" -import "FlowStorageFees" -import "FlowClusterQC" -import "FlowDKG" -import "FlowEpoch" -import "Burner" +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import FlowIDTableStaking from 0x8624b52f9ddcd04a +import LockedTokens from 0x8d0e87b65159ae63 +import FlowStorageFees from 0xe467b9dd11fa00df +import FlowClusterQC from 0x8624b52f9ddcd04a +import FlowDKG from 0x8624b52f9ddcd04a +import FlowEpoch from 0x8624b52f9ddcd04a +import Burner from 0xf233dcee88fe0abe access(all) contract FlowStakingCollection { @@ -137,7 +137,8 @@ access(all) contract FlowStakingCollection { ) { pre { unlockedVault.check(): - "FlowStakingCollection.StakingCollection.init: Cannot Initialize a Staking Collection! The provided FlowToken Vault capability with withdraw entitlements is invalid." + "FlowStakingCollection.StakingCollection.init: Cannot Initialize a Staking Collection! " + .concat("The provided FlowToken Vault capability with withdraw entitlements is invalid.") } self.unlockedVault = unlockedVault @@ -172,13 +173,19 @@ access(all) contract FlowStakingCollection { /// /// @return String: The full error message to print access(all) view fun getStakerDoesntExistInCollectionError(funcName: String, nodeID: String, delegatorID: UInt32?): String { - let errorPrefix = "FlowStakingCollection.StakingCollection.\(funcName): " - + // Construct the function name for the beginning of the error + let errorBeginning = "FlowStakingCollection.StakingCollection.".concat(funcName).concat(": ") + // The error message is different if it is a delegator vs a node if let delegator = delegatorID { - return "\(errorPrefix)The specified delegator with node ID \(nodeID) and delegatorID \(delegator) does not exist in the owner's collection. Make sure that the IDs you entered correspond to a delegator that is controlled by this staking collection." + return errorBeginning.concat("The specified delegator with node ID ") + .concat(nodeID).concat(" and delegatorID ").concat(delegator.toString()) + .concat(" does not exist in the owner's collection. ") + .concat("Make sure that the IDs you entered correspond to a delegator that is controlled by this staking collection.") } else { - return "\(errorPrefix)The specified node with ID \(nodeID) does not exist in the owner's collection. Make sure that the ID you entered corresponds to a node that is controlled by this staking collection." + return errorBeginning.concat("The specified node with ID ") + .concat(nodeID).concat(" does not exist in the owner's collection. ") + .concat("Make sure that the ID you entered corresponds to a node that is controlled by this staking collection.") } } @@ -202,17 +209,21 @@ access(all) contract FlowStakingCollection { access(self) fun getTokens(amount: UFix64): @{FungibleToken.Vault} { let unlockedVault = self.unlockedVault.borrow()! - let unlockedBalance = unlockedVault.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) + let unlockedBalance = unlockedVault.balance - FlowStorageFees.minimumStorageReservation // If there is a locked account, use the locked vault first if self.lockedVault != nil { let lockedVault = self.lockedVault!.borrow()! - let lockedBalance = lockedVault.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) + let lockedBalance = lockedVault.balance - FlowStorageFees.minimumStorageReservation assert( amount <= lockedBalance + unlockedBalance, - message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! The amount of FLOW requested to use, \(amount), is more than the sum of locked and unlocked FLOW, \(lockedBalance + unlockedBalance), in the owner's accounts." + message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! " + .concat("The amount of FLOW requested to use, ") + .concat(amount.toString()).concat(", is more than the sum of ") + .concat("locked and unlocked FLOW, ").concat((lockedBalance+unlockedBalance).toString()) + .concat(", in the owner's accounts.") ) // If all the tokens can be removed from locked, withdraw and return them @@ -250,7 +261,11 @@ access(all) contract FlowStakingCollection { assert( amount <= unlockedBalance, - message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! The amount of FLOW requested to use, \(amount), is more than the amount of FLOW, \(unlockedBalance), in the owner's account." + message: "FlowStakingCollection.StakingCollection.getTokens: Cannot get tokens to stake! " + .concat("The amount of FLOW requested to use, ") + .concat(amount.toString()).concat(", is more than the amount of FLOW, ") + .concat((unlockedBalance).toString()) + .concat(", in the owner's account.") ) self.unlockedTokensUsed = self.unlockedTokensUsed + amount @@ -266,7 +281,8 @@ access(all) contract FlowStakingCollection { // This error should never be triggered in production becasue the tokens used fields // should be properly managed by all the other functions from.balance <= self.unlockedTokensUsed + self.lockedTokensUsed: - "FlowStakingCollection.StakingCollection.depositTokens: Cannot return more FLOW to the account than is already in use for staking." + "FlowStakingCollection.StakingCollection.depositTokens: " + .concat(" Cannot return more FLOW to the account than is already in use for staking.") } let unlockedVault = self.unlockedVault.borrow()! @@ -374,7 +390,10 @@ access(all) contract FlowStakingCollection { self.doesStakeExist(nodeID: nodeID, delegatorID: nil): self.getStakerDoesntExistInCollectionError(funcName: "removeNode", nodeID: nodeID, delegatorID: nil) self.lockedTokensUsed == UFix64(0.0): - "FlowStakingCollection.StakingCollection.removeNode: Cannot remove a node from the collection because the collection still manages \(self.lockedTokensUsed) locked tokens. This is to prevent locked tokens from being unlocked and withdrawn before their allotted unlocking time." + "FlowStakingCollection.StakingCollection.removeNode: Cannot remove a node from the collection " + .concat("because the collection still manages ").concat(self.lockedTokensUsed.toString()) + .concat(" locked tokens. This is to prevent locked tokens ") + .concat("from being unlocked and withdrawn before their allotted unlocking time.") } if self.nodeStakers[nodeID] != nil { @@ -395,7 +414,7 @@ access(all) contract FlowStakingCollection { return <- nodeStaker } else { // The function does not allow for removing a NodeStaker stored in the locked account, if one exists. - panic("FlowStakingCollection.StakingCollection.removeNode: Cannot remove node \(nodeID) because it is stored in the locked account and must be managed through the locked account interface.") + panic("Cannot remove node stored in locked account.") } } @@ -406,7 +425,10 @@ access(all) contract FlowStakingCollection { self.doesStakeExist(nodeID: nodeID, delegatorID: delegatorID): self.getStakerDoesntExistInCollectionError(funcName: "removeDelegator", nodeID: nodeID, delegatorID: delegatorID) self.lockedTokensUsed == UFix64(0.0): - "FlowStakingCollection.StakingCollection.removeDelegator: Cannot remove a delegator from the collection because the collection still manages \(self.lockedTokensUsed) locked tokens. This is to prevent locked tokens from being unlocked and withdrawn before their allotted unlocking time." + "FlowStakingCollection.StakingCollection.removeDelegator: Cannot remove a delegator from the collection " + .concat("because the collection still manages ").concat(self.lockedTokensUsed.toString()) + .concat(" locked tokens. This is to prevent locked tokens ") + .concat("from being unlocked and withdrawn before their allotted unlocking time.") } if self.nodeDelegators[nodeID] != nil { @@ -428,12 +450,17 @@ access(all) contract FlowStakingCollection { ) return <- nodeDelegator - } else { - panic("FlowStakingCollection.StakingCollection.removeDelegator: Expected delegatorID \(delegatorID) does not correspond to the Staking Collection's delegator ID \(delegatorRef.id)") + } else { + panic("FlowStakingCollection.StakingCollection.removeDelegator: " + .concat("Expected delegatorID ").concat(delegatorID.toString()) + .concat(" does not correspond to the Staking Collection's delegator ID ") + .concat(delegatorRef.id.toString())) } } else { // The function does not allow for removing a NodeDelegator stored in the locked account, if one exists. - panic("FlowStakingCollection.StakingCollection.removeDelegator: Cannot remove a delegator with ID \(delegatorID) because it is stored in the locked account.") + panic("FlowStakingCollection.StakingCollection.removeDelegator: " + .concat("Cannot remove a delegator with ID ").concat(delegatorID.toString()) + .concat(" because it is stored in the locked account.")) } } @@ -473,7 +500,9 @@ access(all) contract FlowStakingCollection { self.nodeStakers[id] <-! nodeStaker let nodeReference = self.borrowNode(id) - ?? panic("FlowStakingCollection.StakingCollection.registerNode: Could not borrow a reference to the newly created node with ID \(id).") + ?? panic("FlowStakingCollection.StakingCollection.registerNode: " + .concat("Could not borrow a reference to the newly created node with ID ") + .concat(id).concat(".")) let nodeInfo = FlowIDTableStaking.NodeInfo(nodeID: nodeReference.id) @@ -573,19 +602,25 @@ access(all) contract FlowStakingCollection { if nodeInfo.role == FlowEpoch.NodeRole.Collector.rawValue { let qcVoterRef = machineAccount.storage.borrow<&FlowClusterQC.Voter>(from: FlowClusterQC.VoterStoragePath) - ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: Could not access a QC Voter object from the provided machine account with address \(machineAccount.address)") + ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: " + .concat("Could not access a QC Voter object from the provided machine account with address ").concat(machineAccount.address.toString())) assert( nodeID == qcVoterRef.nodeID, - message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: The QC Voter Object in the machine account with node ID \(qcVoterRef.nodeID) does not match the Staking Collection's specified node ID \(nodeID)" + message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: " + .concat("The QC Voter Object in the machine account with node ID ").concat(qcVoterRef.nodeID) + .concat(" does not match the Staking Collection's specified node ID ").concat(nodeID) ) } else if nodeInfo.role == FlowEpoch.NodeRole.Consensus.rawValue { let dkgParticipantRef = machineAccount.storage.borrow<&FlowDKG.Participant>(from: FlowDKG.ParticipantStoragePath) - ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: Could not access a DKG Participant object from the provided machine account with address \(machineAccount.address)") + ?? panic("FlowStakingCollection.StakingCollection.addMachineAccountRecord: " + .concat("Could not access a DKG Participant object from the provided machine account with address ").concat(machineAccount.address.toString())) assert( nodeID == dkgParticipantRef.nodeID, - message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: The DKG Participant Object in the machine account with node ID \(dkgParticipantRef.nodeID) does not match the Staking Collection's specified node ID \(nodeID)" + message: "FlowStakingCollection.StakingCollection.addMachineAccountRecord: " + .concat("The DKG Participant Object in the machine account with node ID ").concat(dkgParticipantRef.nodeID) + .concat(" does not match the Staking Collection's specified node ID ").concat(nodeID) ) } @@ -623,7 +658,8 @@ access(all) contract FlowStakingCollection { let lockedTokenManager = tokenHolderObj.borrow()!.borrowTokenManager() let lockedNodeReference = lockedTokenManager.borrowNode() - ?? panic("FlowStakingCollection.StakingCollection.createMachineAccountForExistingNode: Could not borrow a node reference from the locked account.") + ?? panic("FlowStakingCollection.StakingCollection.createMachineAccountForExistingNode: " + .concat("Could not borrow a node reference from the locked account.")) return self.registerMachineAccount(nodeReference: lockedNodeReference, payer: payer) } @@ -640,7 +676,8 @@ access(all) contract FlowStakingCollection { } if let machineAccountInfo = self.machineAccounts[nodeID] { let vaultRef = machineAccountInfo.machineAccountVaultProvider.borrow() - ?? panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: Could not borrow reference to machine account vault.") + ?? panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: " + .concat("Could not borrow reference to machine account vault.")) let tokens <- vaultRef.withdraw(amount: amount) @@ -648,7 +685,9 @@ access(all) contract FlowStakingCollection { unlockedVault.deposit(from: <-tokens) } else { - panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: Could not find a machine account for the specified node ID \(nodeID).") + panic("FlowStakingCollection.StakingCollection.withdrawFromMachineAccount: " + .concat("Could not find a machine account for the specified node ID ") + .concat(nodeID).concat(".")) } } @@ -656,8 +695,10 @@ access(all) contract FlowStakingCollection { access(CollectionOwner) fun registerDelegator(nodeID: String, amount: UFix64) { let delegatorIDs = self.getDelegatorIDs() for idInfo in delegatorIDs { - if idInfo.delegatorNodeID == nodeID { - panic("FlowStakingCollection.StakingCollection.registerDelegator: Cannot register a delegator for node \(nodeID) because that node is already being delegated to from this Staking Collection.") + if idInfo.delegatorNodeID == nodeID { + panic("FlowStakingCollection.StakingCollection.registerDelegator: " + .concat("Cannot register a delegator for node ").concat(nodeID) + .concat(" because that node is already being delegated to from this Staking Collection.")) } } @@ -734,11 +775,11 @@ access(all) contract FlowStakingCollection { let tokenHolder = self.tokenHolder!.borrow()! // Get any needed unlocked tokens, and deposit them to the locked vault. - let lockedBalance = self.lockedVault!.borrow()!.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) + let lockedBalance = self.lockedVault!.borrow()!.balance - FlowStorageFees.minimumStorageReservation if (amount > lockedBalance) { let numUnlockedTokensToUse = amount - lockedBalance tokenHolder.deposit(from: <- self.unlockedVault.borrow()!.withdraw(amount: numUnlockedTokensToUse)) - } + } // Use the delegator stored in the locked account let delegator = tokenHolder.borrowDelegator() @@ -750,11 +791,11 @@ access(all) contract FlowStakingCollection { node.stakeNewTokens(<-self.getTokens(amount: amount)) } else { // Get any needed unlocked tokens, and deposit them to the locked vault. - let lockedBalance = self.lockedVault!.borrow()!.balance.saturatingSubtract(FlowStorageFees.minimumStorageReservation) + let lockedBalance = self.lockedVault!.borrow()!.balance - FlowStorageFees.minimumStorageReservation if (amount > lockedBalance) { let numUnlockedTokensToUse = amount - lockedBalance self.tokenHolder!.borrow()!.deposit(from: <- self.unlockedVault.borrow()!.withdraw(amount: numUnlockedTokensToUse)) - } + } // Use the staker stored in the locked account let staker = self.tokenHolder!.borrow()!.borrowStaker() @@ -933,7 +974,8 @@ access(all) contract FlowStakingCollection { assert( delegatorInfo.tokensStaked + delegatorInfo.tokensCommitted + delegatorInfo.tokensUnstaking == 0.0, - message: "FlowStakingCollection.StakingCollection.closeStake: Cannot close a delegation until all tokens have been withdrawn, or moved to a withdrawable state." + message: "FlowStakingCollection.StakingCollection.closeStake: " + .concat("Cannot close a delegation until all tokens have been withdrawn, or moved to a withdrawable state.") ) if delegatorInfo.tokensUnstaked > 0.0 { @@ -1209,9 +1251,14 @@ access(all) contract FlowStakingCollection { /// @return String: The full error message access(all) view fun getCollectionMissingError(_ account: Address?): String { if let address = account { - return "FlowStakingCollection: The account \(address) does not store a Staking Collection object at the path \(FlowStakingCollection.StakingCollectionStoragePath). They must initialize their account with this object first!" + return "The account ".concat(address.toString()) + .concat(" does not store a Staking Collection object at the path ") + .concat(FlowStakingCollection.StakingCollectionStoragePath.toString()) + .concat(". They must initialize their account with this object first!") } else { - return "FlowStakingCollection: The signer does not store a Staking Collection object at the path \(FlowStakingCollection.StakingCollectionStoragePath). The signer must initialize their account with this object first!" + return "The signer does not store a Staking Collection object at the path " + .concat(FlowStakingCollection.StakingCollectionStoragePath.toString()) + .concat(". The signer must initialize their account with this object first!") } } diff --git a/contracts/FlowToken.cdc b/contracts/FlowToken.cdc index 8a2f5d1c6..d58feb68f 100644 --- a/contracts/FlowToken.cdc +++ b/contracts/FlowToken.cdc @@ -1,6 +1,6 @@ -import "FungibleToken" -import "MetadataViews" -import "FungibleTokenMetadataViews" +import FungibleToken from 0xf233dcee88fe0abe +import MetadataViews from 0x1d7e57aa55817448 +import FungibleTokenMetadataViews from 0xf233dcee88fe0abe access(all) contract FlowToken: FungibleToken { @@ -80,22 +80,16 @@ access(all) contract FlowToken: FungibleToken { // If the owner is the staking account, do not emit the contract defined events // this is to help with the performance of the epoch transition operations - // Either way, event listeners should be paying attention to the + // Either way, event listeners should be paying attention to the // FungibleToken.Withdrawn events anyway because those contain // much more comprehensive metadata // Additionally, these events will eventually be removed from this contract completely // in favor of the FungibleToken events - // - // NOTE: The address exclusions below are NOT a security bypass — they are a documented - // performance optimization for known service accounts that process high volumes of - // token movements during epoch transitions. These are fixed, well-known protocol - // addresses (emulator service account, testnet staking contract, mainnet staking contract). - // This does not affect the security of user funds in any way. if let address = self.owner?.address { if address != 0xf8d6e0586b0a20c7 && address != 0xf4527793ee68aede && address != 0x9eca2b38b18b5dfe && - address != 0x8624b52f9ddcd04a + address != 0x8624b52f9ddcd04a { emit TokensWithdrawn(amount: amount, from: address) } @@ -212,7 +206,7 @@ access(all) contract FlowToken: FungibleToken { ) case Type(): let vaultRef = FlowToken.account.storage.borrow(from: /storage/flowTokenVault) - ?? panic("FlowToken.resolveContractView: Could not borrow reference to the contract's Vault!") + ?? panic("Could not borrow reference to the contract's Vault!") return FungibleTokenMetadataViews.FTVaultData( storagePath: /storage/flowTokenVault, receiverPath: /public/flowTokenReceiver, @@ -256,8 +250,8 @@ access(all) contract FlowToken: FungibleToken { // access(all) fun mintTokens(amount: UFix64): @FlowToken.Vault { pre { - amount > UFix64(0): "FlowToken.Minter.mintTokens: Amount minted must be greater than zero but got \(amount)" - amount <= self.allowedAmount: "FlowToken.Minter.mintTokens: Amount minted (\(amount)) must be less than or equal to the allowed amount (\(self.allowedAmount))" + amount > UFix64(0): "Amount minted must be greater than zero" + amount <= self.allowedAmount: "Amount minted must be less than the allowed amount" } FlowToken.totalSupply = FlowToken.totalSupply + amount self.allowedAmount = self.allowedAmount - amount @@ -275,29 +269,29 @@ access(all) contract FlowToken: FungibleToken { return FlowToken.account.storage.copy(from: /storage/flowTokenLogoURI) ?? "" } - init() { + init(adminAccount: auth(Storage, Capabilities) &Account) { self.totalSupply = 0.0 // Create the Vault with the total supply of tokens and save it in storage // let vault <- create Vault(balance: self.totalSupply) - self.account.storage.save(<-vault, to: /storage/flowTokenVault) + adminAccount.storage.save(<-vault, to: /storage/flowTokenVault) // Create a public capability to the stored Vault that only exposes // the `deposit` method through the `Receiver` interface // - let receiverCapability = self.account.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) - self.account.capabilities.publish(receiverCapability, at: /public/flowTokenReceiver) + let receiverCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + adminAccount.capabilities.publish(receiverCapability, at: /public/flowTokenReceiver) // Create a public capability to the stored Vault that only exposes // the `balance` field through the `Balance` interface // - let balanceCapability = self.account.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) - self.account.capabilities.publish(balanceCapability, at: /public/flowTokenBalance) + let balanceCapability = adminAccount.capabilities.storage.issue<&FlowToken.Vault>(/storage/flowTokenVault) + adminAccount.capabilities.publish(balanceCapability, at: /public/flowTokenBalance) let admin <- create Administrator() - self.account.storage.save(<-admin, to: /storage/flowTokenAdmin) + adminAccount.storage.save(<-admin, to: /storage/flowTokenAdmin) } } diff --git a/contracts/FlowTransactionScheduler.cdc b/contracts/FlowTransactionScheduler.cdc index 53eb9fdae..dd98c09aa 100644 --- a/contracts/FlowTransactionScheduler.cdc +++ b/contracts/FlowTransactionScheduler.cdc @@ -1,8 +1,8 @@ -import "FungibleToken" -import "FlowToken" -import "FlowFees" -import "FlowStorageFees" -import "ViewResolver" +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import FlowFees from 0xf919ee77447b7497 +import FlowStorageFees from 0xe467b9dd11fa00df +import ViewResolver from 0x1d7e57aa55817448 /// FlowTransactionScheduler enables smart contracts to schedule autonomous execution in the future. /// @@ -600,9 +600,6 @@ access(all) contract FlowTransactionScheduler { } /// sets all the configuration details for the Scheduler resource - /// NOTE: This function is guarded by the UpdateConfig entitlement, which is an admin-only - /// capability. It is not callable by regular users. Any configuration changes (including - /// txRemovalLimit) require explicit authorization from the contract administrator. access(UpdateConfig) fun setConfig(newConfig: {SchedulerConfig}, txRemovalLimit: UInt) { self.config = newConfig FlowTransactionScheduler.account.storage.load(from: /storage/txRemovalLimit) @@ -652,11 +649,11 @@ access(all) contract FlowTransactionScheduler { var timestampTransactions: {UInt8: [UInt64]} = {} - for priority in transactionPriorities { + for priority in transactionPriorities.keys { let transactionIDs = transactionPriorities[priority] ?? {} var priorityTransactions: [UInt64] = [] - - for id in transactionIDs { + + for id in transactionIDs.keys { priorityTransactions.append(id) } @@ -722,15 +719,8 @@ access(all) contract FlowTransactionScheduler { return Status.Canceled } - // If we reach this point, the transaction is not in the active transactions map - // and not in canceledTransactions. Since Scheduled transactions always remain in - // the transactions map until execution, a transaction can only reach this code path - // after it has been executed and aged out. The inference below uses the sorted - // canceledTransactions array as a lower-bound anchor: if the requested ID is greater - // than the oldest known canceled ID, it must have been executed (not canceled), - // because any cancellation would have added it to the canceledTransactions array. - // NOTE: Scheduled (future) transactions cannot be incorrectly reported as Executed - // here — they are still in the transactions map and are returned as Scheduled above. + // if transaction ID is after first canceled ID it must be executed + // otherwise it would have been canceled and part of this list let firstCanceledID = self.canceledTransactions[0] if id > firstCanceledID { return Status.Executed @@ -1004,11 +994,7 @@ access(all) contract FlowTransactionScheduler { } self.slotQueue[slot] = transactionsForSlot - // Add the execution effort for this transaction to the per-priority total for the slot. - // NOTE: This addition cannot overflow in practice. executionEffort is validated against - // maximumIndividualEffort and priorityEffortLimit before reaching this point (in estimate()), - // and the cumulative slot total is bounded by priorityEffortLimit[priority] which is - // checked on every schedule() call. UInt64 max (~1.8e19) far exceeds any reachable sum. + // Add the execution effort for this transaction to the per-priority total for the slot let slotEfforts = &self.slotUsedEffort[slot]! as auth(Mutate) &{Priority: UInt64} slotEfforts[txData.priority] = slotEfforts[txData.priority]! + txData.executionEffort @@ -1074,9 +1060,9 @@ access(all) contract FlowTransactionScheduler { var medium: [&TransactionData] = [] var low: [&TransactionData] = [] - for priority in transactionPriorities { + for priority in transactionPriorities.keys { let transactionIDs = transactionPriorities[priority] ?? {} - for id in transactionIDs { + for id in transactionIDs.keys { let tx = self.borrowTransaction(id: id) if tx == nil { emit CriticalIssue(message: "Invalid ID: \(id) transaction not found while preparing pending queue") @@ -1129,9 +1115,9 @@ access(all) contract FlowTransactionScheduler { for timestamp in pastTimestamps { let transactionPriorities = self.slotQueue[timestamp] ?? {} - for priority in transactionPriorities { + for priority in transactionPriorities.keys { let transactionIDs = transactionPriorities[priority] ?? {} - for id in transactionIDs { + for id in transactionIDs.keys { numRemoved = numRemoved + 1 @@ -1232,7 +1218,6 @@ access(all) contract FlowTransactionScheduler { if midCanceledID == id { emit CriticalIssue(message: "Invalid ID: \(id) transaction already in canceled transactions array") - break } else if midCanceledID > id { high = mid } else { diff --git a/contracts/FlowTransactionSchedulerUtils.cdc b/contracts/FlowTransactionSchedulerUtils.cdc index d0dafb18e..94384be38 100644 --- a/contracts/FlowTransactionSchedulerUtils.cdc +++ b/contracts/FlowTransactionSchedulerUtils.cdc @@ -1,8 +1,8 @@ -import "FlowTransactionScheduler" -import "FungibleToken" -import "FlowToken" -import "EVM" -import "MetadataViews" +import FlowTransactionScheduler from 0xe467b9dd11fa00df +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import EVM from 0xe467b9dd11fa00df +import MetadataViews from 0x1d7e57aa55817448 /// FlowTransactionSchedulerUtils provides utility functionality for working with scheduled transactions /// on the Flow blockchain. @@ -294,9 +294,8 @@ access(all) contract FlowTransactionSchedulerUtils { if self.idsByTimestamp.containsKey(timestamp) { let ids = &self.idsByTimestamp[timestamp]! as auth(Mutate) &[UInt64] - if let index = ids.firstIndex(of: id) { - ids.remove(at: index) - } + let index = ids.firstIndex(of: id) + ids.remove(at: index!) if ids.length == 0 { self.idsByTimestamp.remove(key: timestamp) self.sortedTimestamps.remove(timestamp: timestamp) @@ -342,7 +341,7 @@ access(all) contract FlowTransactionSchedulerUtils { } // Then remove and destroy the identified transactions - for id in transactionsToRemove { + for id in transactionsToRemove.keys { if let tx <- self.scheduledTransactions.remove(key: id) { self.removeID(id: id, timestamp: transactionsToRemove[id]!, handlerTypeIdentifier: tx.handlerTypeIdentifier) destroy tx @@ -414,7 +413,7 @@ access(all) contract FlowTransactionSchedulerUtils { for handlerTypeIdentifier in self.handlerInfos.keys { let handlerUUIDs: [UInt64] = [] let handlerTypes = self.handlerInfos[handlerTypeIdentifier]! - for uuid in handlerTypes { + for uuid in handlerTypes.keys { let handlerInfo = handlerTypes[uuid]! if !handlerInfo.capability.check() { continue diff --git a/contracts/LockedTokens.cdc b/contracts/LockedTokens.cdc index 32eb76d5b..299229f89 100644 --- a/contracts/LockedTokens.cdc +++ b/contracts/LockedTokens.cdc @@ -25,11 +25,11 @@ */ -import "FlowToken" -import "FungibleToken" -import "FlowIDTableStaking" -import "FlowStorageFees" -import "StakingProxy" +import FlowToken from 0x1654653399040a61 +import FungibleToken from 0xf233dcee88fe0abe +import FlowIDTableStaking from 0x8624b52f9ddcd04a +import FlowStorageFees from 0xe467b9dd11fa00df +import StakingProxy from 0x62430cf28c26d095 access(all) contract LockedTokens { @@ -166,14 +166,11 @@ access(all) contract LockedTokens { access(self) fun withdrawUnlockedTokens(amount: UFix64): @{FungibleToken.Vault} { pre { - // NOTE: This precondition prevents underflow — unlockLimit can never go negative. - // The subtraction in the postcondition is safe because this check guarantees - // unlockLimit >= amount before any state is modified. - self.unlockLimit >= amount: "LockedTokens.LockedTokenManager.withdrawUnlockedTokens: Requested amount \(amount) exceeds unlocked token limit of \(self.unlockLimit)" + self.unlockLimit >= amount: "Requested amount exceeds unlocked token limit" } post { - self.unlockLimit == before(self.unlockLimit) - amount: "LockedTokens.LockedTokenManager.withdrawUnlockedTokens: Updated unlocked token limit is incorrect" + self.unlockLimit == before(self.unlockLimit) - amount: "Updated unlocked token limit is incorrect" } let vaultRef = self.vault.borrow()! @@ -215,7 +212,7 @@ access(all) contract LockedTokens { assert( stakingInfo.tokensStaked + stakingInfo.tokensCommitted + stakingInfo.tokensUnstaking + stakingInfo.tokensUnstaked + stakingInfo.tokensRewarded == 0.0, - message: "LockedTokens.LockedTokenManager.registerNode: Cannot register a new node until all tokens from the previous node have been withdrawn" + message: "Cannot register a new node until all tokens from the previous node have been withdrawn" ) destroy nodeStaker @@ -247,7 +244,7 @@ access(all) contract LockedTokens { assert( delegatorInfo.tokensStaked + delegatorInfo.tokensCommitted + delegatorInfo.tokensUnstaking + delegatorInfo.tokensUnstaked + delegatorInfo.tokensRewarded == 0.0, - message: "LockedTokens.LockedTokenManager.registerDelegator: Cannot register a new delegator until all tokens from the previous delegator have been withdrawn" + message: "Cannot register a new delegator until all tokens from the previous node have been withdrawn" ) destroy delegator @@ -257,7 +254,7 @@ access(all) contract LockedTokens { assert( vaultRef.balance >= FlowIDTableStaking.getDelegatorMinimumStakeRequirement(), - message: "LockedTokens.LockedTokenManager.registerDelegator: Must have the delegation minimum FLOW requirement (\(FlowIDTableStaking.getDelegatorMinimumStakeRequirement())) in the locked vault to register a delegator but only have \(vaultRef.balance)" + message: "Must have the delegation minimum FLOW requirement in the locked vault to register a node" ) let tokens <- vaultRef.withdraw(amount: amount) @@ -338,7 +335,7 @@ access(all) contract LockedTokens { init(lockedAddress: Address, tokenManager: Capability) { pre { - tokenManager.borrow() != nil: "LockedTokens.TokenHolder.init: Must pass a LockedTokenManager capability" + tokenManager.borrow() != nil: "Must pass a LockedTokenManager capability" } self.address = lockedAddress @@ -435,7 +432,7 @@ access(all) contract LockedTokens { access(TokenOperations) fun borrowStaker(): LockedNodeStakerProxy { pre { self.nodeStakerProxy != nil: - "LockedTokens.TokenHolder.borrowStaker: The NodeStakerProxy doesn't exist!" + "The NodeStakerProxy doesn't exist!" } return self.nodeStakerProxy! } @@ -451,7 +448,7 @@ access(all) contract LockedTokens { access(TokenOperations) fun borrowDelegator(): LockedNodeDelegatorProxy { pre { self.nodeDelegatorProxy != nil: - "LockedTokens.TokenHolder.borrowDelegator: The NodeDelegatorProxy doesn't exist!" + "The NodeDelegatorProxy doesn't exist!" } return self.nodeDelegatorProxy! } @@ -477,7 +474,7 @@ access(all) contract LockedTokens { init(tokenManager: Capability) { pre { - tokenManager.borrow() != nil: "LockedTokens.LockedNodeStakerProxy.init: Invalid token manager capability" + tokenManager.borrow() != nil: "Invalid token manager capability" } self.tokenManager = tokenManager } @@ -492,7 +489,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.updateNetworkingAddress: Cannot change networking address if there is no node object!" + message: "Cannot change networking address if there is no node object!" ) tokenManagerRef.borrowNode()?.updateNetworkingAddress(newAddress) @@ -504,7 +501,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.stakeNewTokens: Cannot stake if there is no node object!" + message: "Cannot stake if there is no node object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -518,7 +515,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.stakeUnstakedTokens: Cannot stake if there is no node object!" + message: "Cannot stake if there is no node object!" ) tokenManagerRef.borrowNode()?.stakeUnstakedTokens(amount: amount) @@ -532,7 +529,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.stakeRewardedTokens: Cannot stake if there is no node object!" + message: "Cannot stake if there is no node object!" ) tokenManagerRef.borrowNode()?.stakeRewardedTokens(amount: amount) @@ -546,7 +543,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.requestUnstaking: Cannot unstake if there is no node object!" + message: "Cannot stake if there is no node object!" ) tokenManagerRef.borrowNode()?.requestUnstaking(amount: amount) @@ -559,7 +556,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.unstakeAll: Cannot unstake if there is no node object!" + message: "Cannot stake if there is no node object!" ) tokenManagerRef.borrowNode()?.unstakeAll() @@ -574,7 +571,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.withdrawUnstakedTokens: Cannot withdraw if there is no node object!" + message: "Cannot stake if there is no node object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -591,7 +588,7 @@ access(all) contract LockedTokens { assert( self.nodeObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeStakerProxy.withdrawRewardedTokens: Cannot withdraw if there is no node object!" + message: "Cannot stake if there is no node object!" ) tokenManagerRef.deposit(from: <-tokenManagerRef.borrowNode()?.withdrawRewardedTokens(amount: amount)!) @@ -605,7 +602,7 @@ access(all) contract LockedTokens { init(tokenManager: Capability) { pre { - tokenManager.borrow() != nil: "LockedTokens.LockedNodeDelegatorProxy.init: Invalid LockedTokenManager capability" + tokenManager.borrow() != nil: "Invalid LockedTokenManager capability" } self.tokenManager = tokenManager } @@ -620,7 +617,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeDelegatorProxy.delegateNewTokens: Cannot delegate if there is no delegator object!" + message: "Cannot stake if there is no delegator object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -634,7 +631,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeDelegatorProxy.delegateUnstakedTokens: Cannot delegate if there is no delegator object!" + message: "Cannot stake if there is no delegator object!" ) tokenManagerRef.borrowDelegator()?.delegateUnstakedTokens(amount: amount) @@ -647,7 +644,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeDelegatorProxy.delegateRewardedTokens: Cannot delegate if there is no delegator object!" + message: "Cannot stake if there is no delegator object!" ) tokenManagerRef.borrowDelegator()?.delegateRewardedTokens(amount: amount) @@ -661,7 +658,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeDelegatorProxy.requestUnstaking: Cannot unstake if there is no delegator object!" + message: "Cannot stake if there is no delegator object!" ) tokenManagerRef.borrowDelegator()?.requestUnstaking(amount: amount) @@ -674,7 +671,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeDelegatorProxy.withdrawUnstakedTokens: Cannot withdraw if there is no delegator object!" + message: "Cannot stake if there is no delegator object!" ) let vaultRef = tokenManagerRef.vault.borrow()! @@ -690,7 +687,7 @@ access(all) contract LockedTokens { assert( self.delegatorObjectExists(tokenManagerRef), - message: "LockedTokens.LockedNodeDelegatorProxy.withdrawRewardedTokens: Cannot withdraw if there is no delegator object!" + message: "Cannot stake if there is no delegator object!" ) tokenManagerRef.deposit(from: <-tokenManagerRef.borrowDelegator()?.withdrawRewardedTokens(amount: amount)!) @@ -756,7 +753,7 @@ access(all) contract LockedTokens { access(all) fun addCapability(cap: Capability) { pre { - cap.borrow() != nil: "LockedTokens.LockedAccountCreator.addCapability: Invalid token admin collection capability" + cap.borrow() != nil: "Invalid token admin collection capability" } self.addAccountCapability = cap } @@ -767,9 +764,9 @@ access(all) contract LockedTokens { pre { self.addAccountCapability != nil: - "LockedTokens.LockedAccountCreator.addAccount: Cannot add account until the token admin has deposited the account registration capability" + "Cannot add account until the token admin has deposited the account registration capability" tokenAdmin.borrow() != nil: - "LockedTokens.LockedAccountCreator.addAccount: Invalid tokenAdmin capability" + "Invalid tokenAdmin capability" } let adminRef = self.addAccountCapability!.borrow()! diff --git a/contracts/NodeVersionBeacon.cdc b/contracts/NodeVersionBeacon.cdc index 93b25e730..5b6f241b6 100644 --- a/contracts/NodeVersionBeacon.cdc +++ b/contracts/NodeVersionBeacon.cdc @@ -187,7 +187,7 @@ access(all) contract NodeVersionBeacon { access(all) fun setVersionBoundary(versionBoundary: VersionBoundary) { pre { versionBoundary.blockHeight > getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod - : "NodeVersionBeacon.Admin.setVersionBoundary: Cannot set/update a version boundary for past blocks or blocks in the near future. Must be greater than \(getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod) but got \(versionBoundary.blockHeight)" + : "Cannot set/update a version boundary for past blocks or blocks in the near future." } // Set the flag to true so the event will be emitted next time emitChanges is called NodeVersionBeacon.emitEventOnNextHeartbeat = true @@ -225,8 +225,8 @@ access(all) contract NodeVersionBeacon { access(all) fun deleteVersionBoundary(blockHeight: UInt64) { pre { blockHeight > getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod - : "NodeVersionBeacon.Admin.deleteVersionBoundary: Cannot delete a version boundary for past blocks or blocks in the near future. Must be greater than \(getCurrentBlock().height + NodeVersionBeacon.versionBoundaryFreezePeriod) but got \(blockHeight)" - NodeVersionBeacon.versionBoundary.containsKey(blockHeight): "NodeVersionBeacon.Admin.deleteVersionBoundary: No boundary defined at block height \(blockHeight)" + : "Cannot delete a version for past blocks or blocks in the near future." + NodeVersionBeacon.versionBoundary.containsKey(blockHeight): "No boundary defined at that blockHeight." } // Set the flag to true so the event will be emitted next time emitChanges is called NodeVersionBeacon.emitEventOnNextHeartbeat = true @@ -242,7 +242,7 @@ access(all) contract NodeVersionBeacon { i = i - 1 } assert(NodeVersionBeacon.versionBoundaryBlockList[i] == blockHeight, - message: "NodeVersionBeacon.Admin.deleteVersionBoundary: version boundary exists in map, so it should also exist in the ordered list") + message: "version boundary exists in map, so it should also exist in the ordered list") NodeVersionBeacon.versionBoundaryBlockList.remove(at: i) @@ -259,7 +259,7 @@ access(all) contract NodeVersionBeacon { /// Updates the number of blocks in which version boundaries are frozen. access(all) fun setVersionBoundaryFreezePeriod(newFreezePeriod: UInt64) { post { - NodeVersionBeacon.versionBoundaryFreezePeriod == newFreezePeriod: "NodeVersionBeacon.Admin.setVersionBoundaryFreezePeriod: Update buffer was not properly set!" + NodeVersionBeacon.versionBoundaryFreezePeriod == newFreezePeriod: "Update buffer was not properly set!" } // Get current block height. @@ -278,7 +278,7 @@ access(all) contract NodeVersionBeacon { assert( currentBlockHeight + NodeVersionBeacon.versionBoundaryFreezePeriod < nextBlockBoundary && currentBlockHeight + newFreezePeriod < nextBlockBoundary, - message: "NodeVersionBeacon.Admin.setVersionBoundaryFreezePeriod: Updating buffer now breaks version boundary update expectations. Try updating buffer after next version boundary." + message: "Updating buffer now breaks version boundary update expectations. Try updating buffer after next version boundary." ) NodeVersionBeacon.versionBoundaryFreezePeriod = newFreezePeriod @@ -407,7 +407,7 @@ access(all) contract NodeVersionBeacon { // index is never 0 since version 0 is always in the past if let index = NodeVersionBeacon.firstUpcomingBoundary { - assert(index > 0, message: "NodeVersionBeacon.getCurrentVersionBoundary: index should never be 0 since version 0 is always in the past") + assert(index > 0, message: "index should never be 0 since version 0 is always in the past") current = self.versionBoundaryBlockList[index-1] } else { current = UInt64(NodeVersionBeacon.versionBoundaryBlockList.length - 1) @@ -455,8 +455,8 @@ access(all) contract NodeVersionBeacon { /// results are sorted by block height access(all) fun getVersionBoundariesPage(page: Int, perPage: Int) : VersionBoundaryPage { pre { - page >= 0: "NodeVersionBeacon.getVersionBoundariesPage: page must be greater than or equal to 0 but got \(page)" - perPage > 0: "NodeVersionBeacon.getVersionBoundariesPage: perPage must be greater than 0 but got \(perPage)" + page >= 0: "page must be greater than or equal to 0" + perPage > 0: "perPage must be greater than 0" } let totalLength = NodeVersionBeacon.versionBoundaryBlockList.length diff --git a/contracts/RandomBeaconHistory.cdc b/contracts/RandomBeaconHistory.cdc index fd34f12a6..759e5b854 100644 --- a/contracts/RandomBeaconHistory.cdc +++ b/contracts/RandomBeaconHistory.cdc @@ -58,7 +58,7 @@ access(all) contract RandomBeaconHistory { assert( // random source must be at least 128 bits randomSourceHistory.length >= 128 / 8, - message: "RandomBeaconHistory.Heartbeat.heartbeat: Random source must be at least 128 bits but got \(randomSourceHistory.length * 8) bits" + message: "Random source must be at least 128 bits" ) let currentBlockHeight = getCurrentBlock().height @@ -111,7 +111,7 @@ access(all) contract RandomBeaconHistory { access(all) fun setMaxEntriesPerCall(max: UInt64) { assert( max > 0, - message: "RandomBeaconHistory.Backfiller.setMaxEntriesPerCall: the maximum entry per call must be strictly positive but got \(max)" + message: "the maximum entry per call must be strictly positive" ) self.maxEntriesPerCall = max } @@ -253,18 +253,18 @@ access(all) contract RandomBeaconHistory { /// access(all) fun sourceOfRandomness(atBlockHeight blockHeight: UInt64): RandomSource { pre { - self.lowestHeight != nil: "RandomBeaconHistory.sourceOfRandomness: History has not yet been initialized" - blockHeight >= self.lowestHeight!: "RandomBeaconHistory.sourceOfRandomness: Requested block height \(blockHeight) precedes recorded history, which starts at \(self.lowestHeight!)" - blockHeight < getCurrentBlock().height: "RandomBeaconHistory.sourceOfRandomness: Source of randomness not yet recorded for block height \(blockHeight)" + self.lowestHeight != nil: "History has not yet been initialized" + blockHeight >= self.lowestHeight!: "Requested block height precedes recorded history" + blockHeight < getCurrentBlock().height: "Source of randomness not yet recorded" } let index = blockHeight - self.lowestHeight! assert( index >= 0, - message: "RandomBeaconHistory.sourceOfRandomness: Problem finding random source history index" + message: "Problem finding random source history index" ) assert( index < UInt64(self.randomSourceHistory.length) && self.randomSourceHistory[index].length > 0, - message: "RandomBeaconHistory.sourceOfRandomness: Source of randomness is currently not available but will be available soon" + message: "Source of randomness is currently not available but will be available soon" ) return RandomSource(blockHeight: blockHeight, value: self.randomSourceHistory[index]) } @@ -279,7 +279,7 @@ access(all) contract RandomBeaconHistory { /// access(all) fun getRandomSourceHistoryPage(_ page: UInt64, perPage: UInt64): RandomSourceHistoryPage { pre { - self.lowestHeight != nil: "RandomBeaconHistory.getRandomSourceHistoryPage: History has not yet been initialized" + self.lowestHeight != nil: "History has not yet been initialized" } let values: [RandomSource] = [] let totalLength = UInt64(self.randomSourceHistory.length) @@ -303,7 +303,7 @@ access(all) contract RandomBeaconHistory { for i, value in self.randomSourceHistory.slice(from: Int(startIndex), upTo: Int(endIndex)) { assert( value.length > 0, - message: "RandomBeaconHistory.getRandomSourceHistoryPage: Source of randomness is currently not available but will be available soon" + message: "Source of randomness is currently not available but will be available soon" ) values.append( RandomSource( @@ -326,7 +326,7 @@ access(all) contract RandomBeaconHistory { /// @return The block height at which the first source of randomness was recorded /// access(all) view fun getLowestHeight(): UInt64 { - return self.lowestHeight ?? panic("RandomBeaconHistory.getLowestHeight: History has not yet been initialized") + return self.lowestHeight ?? panic("History has not yet been initialized") } /// Getter for the contract's Backfiller resource diff --git a/contracts/StakingProxy.cdc b/contracts/StakingProxy.cdc index 30ee856da..a657c8187 100644 --- a/contracts/StakingProxy.cdc +++ b/contracts/StakingProxy.cdc @@ -27,9 +27,9 @@ access(all) contract StakingProxy { init(nodeID: String, role: UInt8, networkingAddress: String, networkingKey: String, stakingKey: String) { pre { - nodeID.length == 64: "StakingProxy.NodeInfo.init: Node ID length must be 32 bytes (64 hex characters) but got \(nodeID.length)" + nodeID.length == 64: "Node ID length must be 32 bytes (64 hex characters)" networkingAddress.length > 0 && networkingKey.length > 0 && stakingKey.length > 0: - "StakingProxy.NodeInfo.init: Address and keys must all be non-empty" + "Address and Key have to be the correct length" } self.id = nodeID self.role = role diff --git a/contracts/epochs/FlowClusterQC.cdc b/contracts/epochs/FlowClusterQC.cdc index a9446d1cc..2a2ed71a3 100644 --- a/contracts/epochs/FlowClusterQC.cdc +++ b/contracts/epochs/FlowClusterQC.cdc @@ -102,8 +102,6 @@ access(all) contract FlowClusterQC { /// Returns the minimum sum of vote weight required in order to be able to generate a /// valid quorum certificate for this cluster. - /// This is an INCLUSIVE lower bound: a vote weight >= this value meets quorum. - /// The threshold is set above 2/3 of total weight, ensuring Byzantine fault tolerance. access(all) view fun voteThreshold(): UInt64 { if self.totalWeight == 0 { return 0 @@ -125,15 +123,10 @@ access(all) contract FlowClusterQC { } /// Returns the status of this cluster's QC process - /// If a vote message accumulates weight >= voteThreshold(), the QC is complete. - /// Then this cluster's QC generation is considered complete and this method returns - /// the vote message that reached quorum. - /// If no vote is found to reach quorum, then `nil` is returned. - /// - /// NOTE: The `>=` comparison is correct and intentional. voteThreshold() returns an - /// inclusive lower bound — a total weight equal to the threshold satisfies quorum. - /// This is NOT a weakened quorum check; the threshold value itself is calculated to - /// require strictly more than 2/3 of total weight. + /// If there is a number of weight for identical votes exceeding the `voteThreshold`, + /// Then this cluster's QC generation is considered complete and this method returns + /// the vote message that reached quorum + /// If no vote is found to reach quorum, then `nil` is returned access(all) view fun isComplete(): String? { for message in self.uniqueVoteMessageTotalWeights.keys { if self.uniqueVoteMessageTotalWeights[message]! >= self.voteThreshold() { @@ -215,7 +208,7 @@ access(all) contract FlowClusterQC { view init(nodeID: String, clusterIndex: UInt16, voteWeight: UInt64) { pre { - nodeID.length == 64: "FlowClusterQC.Vote.init: Voter ID must be a valid length of 64 hex characters but got \(nodeID.length)" + nodeID.length == 64: "Voter ID must be a valid length node ID" } self.signature = nil self.message = nil @@ -292,7 +285,7 @@ access(all) contract FlowClusterQC { init(nodeID: String, stakingKey: String) { pre { - !FlowClusterQC.voterIsClaimed(nodeID): "FlowClusterQC.Voter.init: Cannot create a Voter resource for a node ID (\(nodeID)) that has already been claimed" + !FlowClusterQC.voterIsClaimed(nodeID): "Cannot create a Voter resource for a node ID that has already been claimed" } self.nodeID = nodeID @@ -307,10 +300,10 @@ access(all) contract FlowClusterQC { /// access(all) fun vote(voteSignature: String, voteMessage: String) { pre { - FlowClusterQC.inProgress: "FlowClusterQC.Voter.vote: Voting phase is not in progress" - voteSignature.length > 0: "FlowClusterQC.Voter.vote: Vote signature must not be empty" - voteMessage.length > 0: "FlowClusterQC.Voter.vote: Vote message must not be empty" - !FlowClusterQC.nodeHasVoted(self.nodeID): "FlowClusterQC.Voter.vote: Vote must not have been cast already for node ID \(self.nodeID)" + FlowClusterQC.inProgress: "Voting phase is not in progress" + voteSignature.length > 0: "Vote signature must not be empty" + voteMessage.length > 0: "Vote message must not be empty" + !FlowClusterQC.nodeHasVoted(self.nodeID): "Vote must not have been cast already" } // Get the public key object from the stored key @@ -330,12 +323,12 @@ access(all) contract FlowClusterQC { // Assert the validity assert ( isValid, - message: "FlowClusterQC.Voter.vote: Vote Signature cannot be verified for node ID \(self.nodeID)" + message: "Vote Signature cannot be verified" ) // Get the cluster that this node belongs to let clusterIndex = FlowClusterQC.nodeCluster[self.nodeID] - ?? panic("FlowClusterQC.Voter.vote: Node \(self.nodeID) cannot vote during the current epoch because it is not registered") + ?? panic("This node cannot vote during the current epoch") let cluster = FlowClusterQC.clusters[clusterIndex]! // Get this node's allocated vote @@ -406,7 +399,7 @@ access(all) contract FlowClusterQC { /// majority of each cluster has submitted a vote. access(all) fun stopVoting() { pre { - FlowClusterQC.votingCompleted(): "FlowClusterQC.Admin.stopVoting: Voting must be complete before it can be stopped" + FlowClusterQC.votingCompleted(): "Voting must be complete before it can be stopped" } FlowClusterQC.inProgress = false } diff --git a/contracts/epochs/FlowDKG.cdc b/contracts/epochs/FlowDKG.cdc index 5335b595f..bbaa43bf9 100644 --- a/contracts/epochs/FlowDKG.cdc +++ b/contracts/epochs/FlowDKG.cdc @@ -258,9 +258,13 @@ access(all) contract FlowDKG { access(all) fun addSubmission(nodeID: String, submission: ResultSubmission) { pre { self.authorized[nodeID] != nil: - "FlowDKG.addSubmission: Submittor (node ID: \(nodeID)) is not authorized for this DKG instance." + "FlowDKG.addSubmission: Submittor (node ID: " + .concat(nodeID) + .concat(") is not authorized for this DKG instance.") self.byNodeID[nodeID] == nil: - "FlowDKG.SubmissionTracker.addSubmission: Submittor (node ID: \(nodeID)) may only submit once and has already submitted" + "FlowDKG.SubmissionTracker.addSubmission: Submittor (node ID: " + .concat(nodeID) + .concat(") may only submit once and has already submitted") submission.isValidForCommittee(authorized: self.authorized.keys): "FlowDKG.SubmissionTracker.addSubmission: Submission must contain exactly one public key per authorized participant" } @@ -334,7 +338,9 @@ access(all) contract FlowDKG { init(nodeID: String) { pre { FlowDKG.participantIsClaimed(nodeID) == nil: - "FlowDKG.Participant.init: Cannot create Participant resource for a node ID (\(nodeID)) that has already been claimed" + "FlowDKG.Participant.init: Cannot create Participant resource for a node ID (" + .concat(nodeID) + .concat(") that has already been claimed") } self.nodeID = nodeID FlowDKG.nodeClaimed[nodeID] = true @@ -344,7 +350,9 @@ access(all) contract FlowDKG { access(all) fun postMessage(_ content: String) { pre { FlowDKG.participantIsRegistered(self.nodeID): - "FlowDKG.Participant.postMessage: Cannot post whiteboard message. Sender (node ID: \(self.nodeID)) is not registered for the current DKG instance" + "FlowDKG.Participant.postMessage: Cannot post whiteboard message. Sender (node ID: " + .concat(self.nodeID) + .concat(") is not registered for the current DKG instance") content.length > 0: "FlowDKG.Participant.postMessage: Cannot post empty message to the whiteboard" FlowDKG.dkgEnabled: @@ -392,8 +400,8 @@ access(all) contract FlowDKG { pre { !FlowDKG.dkgEnabled: "FlowDKG.Admin.setSafeSuccessThreshold: Cannot set the DKG success threshold while the DKG is enabled" - newThresholdPercentage == nil || newThresholdPercentage! < 1.0: - "FlowDKG.Admin.setSafeSuccessThreshold: Invalid input. Safe threshold percentage must be in [0,1) but got \(newThresholdPercentage!)" + newThresholdPercentage == nil || newThresholdPercentage! < 1.0: + "FlowDKG.Admin.setSafeSuccessThreshold: Invalid input. Safe threshold percentage must be in [0,1)" } FlowDKG.account.storage.load(from: /storage/flowDKGSafeThreshold) diff --git a/contracts/epochs/FlowEpoch.cdc b/contracts/epochs/FlowEpoch.cdc index 29555cb3e..d87104e19 100644 --- a/contracts/epochs/FlowEpoch.cdc +++ b/contracts/epochs/FlowEpoch.cdc @@ -1,9 +1,9 @@ -import "FungibleToken" -import "FlowToken" -import "FlowIDTableStaking" -import "FlowClusterQC" -import "FlowDKG" -import "FlowFees" +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 +import FlowIDTableStaking from 0x8624b52f9ddcd04a +import FlowClusterQC from 0x8624b52f9ddcd04a +import FlowDKG from 0x8624b52f9ddcd04a +import FlowFees from 0xf919ee77447b7497 // The top-level smart contract managing the lifecycle of epochs. In Flow, // epochs are the smallest unit of time where the identity table (the set of @@ -417,18 +417,16 @@ access(all) contract FlowEpoch { /// Saves a modified EpochMetadata struct to the metadata in account storage access(contract) fun saveEpochMetadata(_ newMetadata: EpochMetadata) { pre { - // The `currentEpochCounter == 0` guard ensures the subtraction is only - // evaluated when the counter is >= 1, making `currentEpochCounter - 1` safe. self.currentEpochCounter == 0 || (newMetadata.counter >= self.currentEpochCounter - 1 && newMetadata.counter <= self.proposedEpochCounter()): - "FlowEpoch.saveEpochMetadata: Cannot modify epoch metadata from epochs after the proposed epoch \(self.proposedEpochCounter()) or before the previous epoch \(self.currentEpochCounter - 1)" + "Cannot modify epoch metadata from epochs after the proposed epoch or before the previous epoch" } if let metadataDictionary = self.account.storage.borrow(from: self.metadataStoragePath) { if let metadata = metadataDictionary[newMetadata.counter] { assert ( metadata.counter == newMetadata.counter, - message: "FlowEpoch.saveEpochMetadata: Cannot save metadata with mismatching epoch counters" + message: "Cannot save metadata with mismatching epoch counters" ) } metadataDictionary[newMetadata.counter] = newMetadata @@ -439,7 +437,10 @@ access(all) contract FlowEpoch { access(contract) fun generateRandomSource(): String { post { result.length == 32: - "FlowEpoch.generateRandomSource: Critical invariant violated! Expected hex random source with length 32 (128 bits) but got length \(result.length) instead." + "FlowEpoch.generateRandomSource: Critical invariant violated! " + .concat("Expected hex random source with length 32 (128 bits) but got length ") + .concat(result.length.toString()) + .concat(" instead.") } var randomSource = String.encodeHex(revertibleRandom().toBigEndianBytes()) return randomSource @@ -464,10 +465,10 @@ access(all) contract FlowEpoch { access(all) resource Admin { access(all) fun updateEpochViews(_ newEpochViews: UInt64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateEpochViews: Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" FlowEpoch.isValidPhaseConfiguration(FlowEpoch.configurableMetadata.numViewsInStakingAuction, FlowEpoch.configurableMetadata.numViewsInDKGPhase, - newEpochViews): "FlowEpoch.Admin.updateEpochViews: New Epoch Views must be greater than the sum of staking and DKG Phase views" + newEpochViews): "New Epoch Views must be greater than the sum of staking and DKG Phase views" } FlowEpoch.configurableMetadata.setNumViewsInEpoch(newEpochViews) @@ -475,10 +476,10 @@ access(all) contract FlowEpoch { access(all) fun updateAuctionViews(_ newAuctionViews: UInt64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateAuctionViews: Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" FlowEpoch.isValidPhaseConfiguration(newAuctionViews, FlowEpoch.configurableMetadata.numViewsInDKGPhase, - FlowEpoch.configurableMetadata.numViewsInEpoch): "FlowEpoch.Admin.updateAuctionViews: Epoch Views must be greater than the sum of new staking and DKG Phase views" + FlowEpoch.configurableMetadata.numViewsInEpoch): "Epoch Views must be greater than the sum of new staking and DKG Phase views" } FlowEpoch.configurableMetadata.setNumViewsInStakingAuction(newAuctionViews) @@ -486,10 +487,10 @@ access(all) contract FlowEpoch { access(all) fun updateDKGPhaseViews(_ newPhaseViews: UInt64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateDKGPhaseViews: Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" FlowEpoch.isValidPhaseConfiguration(FlowEpoch.configurableMetadata.numViewsInStakingAuction, newPhaseViews, - FlowEpoch.configurableMetadata.numViewsInEpoch): "FlowEpoch.Admin.updateDKGPhaseViews: Epoch Views must be greater than the sum of staking and new DKG Phase views" + FlowEpoch.configurableMetadata.numViewsInEpoch): "Epoch Views must be greater than the sum of staking and new DKG Phase views" } FlowEpoch.configurableMetadata.setNumViewsInDKGPhase(newPhaseViews) @@ -497,7 +498,7 @@ access(all) contract FlowEpoch { access(all) fun updateEpochTimingConfig(_ newConfig: EpochTimingConfig) { pre { - FlowEpoch.currentEpochCounter >= newConfig.refCounter: "FlowEpoch.Admin.updateEpochTimingConfig: Reference epoch must be before next epoch" + FlowEpoch.currentEpochCounter >= newConfig.refCounter: "Reference epoch must be before next epoch" } FlowEpoch.account.storage.load(from: /storage/flowEpochTimingConfig) FlowEpoch.account.storage.save(newConfig, to: /storage/flowEpochTimingConfig) @@ -505,7 +506,7 @@ access(all) contract FlowEpoch { access(all) fun updateNumCollectorClusters(_ newNumClusters: UInt16) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateNumCollectorClusters: Can only update fields during the staking auction" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" } FlowEpoch.configurableMetadata.setNumCollectorClusters(newNumClusters) @@ -513,8 +514,8 @@ access(all) contract FlowEpoch { access(all) fun updateFLOWSupplyIncreasePercentage(_ newPercentage: UFix64) { pre { - FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "FlowEpoch.Admin.updateFLOWSupplyIncreasePercentage: Can only update fields during the staking auction" - newPercentage <= 1.0: "FlowEpoch.Admin.updateFLOWSupplyIncreasePercentage: New FLOW supply increase percentage must be between zero and one but got \(newPercentage)" + FlowEpoch.currentEpochPhase == EpochPhase.STAKINGAUCTION: "Can only update fields during the staking auction" + newPercentage <= 1.0: "New value must be between zero and one" } FlowEpoch.configurableMetadata.setFLOWsupplyIncreasePercentage(newPercentage) @@ -592,21 +593,24 @@ access(all) contract FlowEpoch { { pre { FlowEpoch.isValidPhaseConfiguration(stakingEndView-startView+1, FlowEpoch.configurableMetadata.numViewsInDKGPhase, endView-startView+1): - "FlowEpoch.Admin.recoverEpochPreChecks: Invalid startView, stakingEndView, and endView configuration" + "Invalid startView, stakingEndView, and endView configuration" } /// sanity check all nodes should have a weight > 0 for nodeID in nodeIDs { assert( FlowIDTableStaking.NodeInfo(nodeID: nodeID).initialWeight > 0, - message: "FlowEpoch.Admin.recoverEpochPreChecks: All nodes in node ids list for recovery epoch must have a weight > 0. The node \(nodeID) has a weight of 0." + message: "FlowEpoch.Admin.recoverEpochPreChecks: All nodes in node ids list for recovery epoch must have a weight > 0. The node " + .concat(nodeID).concat(" has a weight of 0.") ) } // sanity check we must receive qc vote data for each cluster assert( numOfClusterAssignments == numOfClusterQCVoteData, - message: "FlowEpoch.Admin.recoverEpochPreChecks: The number of cluster assignments \(numOfClusterAssignments) does not match the number of cluster qc vote data \(numOfClusterQCVoteData)" + message: "FlowEpoch.Admin.recoverEpochPreChecks: The number of cluster assignments " + .concat(numOfClusterAssignments.toString()).concat(" does not match the number of cluster qc vote data ") + .concat(numOfClusterQCVoteData.toString()) ) } @@ -646,7 +650,12 @@ access(all) contract FlowEpoch { { pre { recoveryEpochCounter == FlowEpoch.proposedEpochCounter(): - "FlowEpoch.Admin.recoverNewEpoch: Recovery epoch counter must equal current epoch counter + 1. Got recovery epoch counter (\(recoveryEpochCounter)) with current epoch counter (\(FlowEpoch.currentEpochCounter))." + "FlowEpoch.Admin.recoverNewEpoch: Recovery epoch counter must equal current epoch counter + 1. " + .concat("Got recovery epoch counter (") + .concat(recoveryEpochCounter.toString()) + .concat(") with current epoch counter (") + .concat(FlowEpoch.currentEpochCounter.toString()) + .concat(").") } self.stopEpochComponents() @@ -722,7 +731,12 @@ access(all) contract FlowEpoch { { pre { recoveryEpochCounter == FlowEpoch.currentEpochCounter: - "FlowEpoch.Admin.recoverCurrentEpoch: Recovery epoch counter must equal current epoch counter. Got recovery epoch counter (\(recoveryEpochCounter)) with current epoch counter (\(FlowEpoch.currentEpochCounter))." + "FlowEpoch.Admin.recoverCurrentEpoch: Recovery epoch counter must equal current epoch counter. " + .concat("Got recovery epoch counter (") + .concat(recoveryEpochCounter.toString()) + .concat(") with current epoch counter (") + .concat(FlowEpoch.currentEpochCounter.toString()) + .concat(").") } self.stopEpochComponents() @@ -944,8 +958,6 @@ access(all) contract FlowEpoch { /// Pays rewards to the nodes and delegators of the previous epoch access(account) fun payRewardsForPreviousEpoch() { - // `currentEpochCounter` is always >= 1 here because this function is only called - // during epoch transitions after the first epoch has completed, making `- 1` safe. if let previousEpochMetadata = self.getEpochMetadata(self.currentEpochCounter - 1) { if !previousEpochMetadata.rewardsPaid { let summary = FlowIDTableStaking.EpochRewardsSummary(totalRewards: previousEpochMetadata.totalRewards, breakdown: previousEpochMetadata.rewardAmounts) @@ -975,8 +987,6 @@ access(all) contract FlowEpoch { // Update the epoch counters self.currentEpochCounter = self.proposedEpochCounter() - // `proposedEpochCounter()` returns `currentEpochCounter + 1`, so after the - // assignment above `currentEpochCounter` is >= 1, making `- 1` safe. let previousEpochMetadata = self.getEpochMetadata(self.currentEpochCounter - 1)! let newEpochMetadata = self.getEpochMetadata(self.currentEpochCounter)! diff --git a/contracts/external/FlowEVMBridge.cdc b/contracts/external/FlowEVMBridge.cdc new file mode 100644 index 000000000..7a8b8287d --- /dev/null +++ b/contracts/external/FlowEVMBridge.cdc @@ -0,0 +1,1186 @@ +import Burner from 0xf233dcee88fe0abe +import FungibleToken from 0xf233dcee88fe0abe +import FungibleTokenMetadataViews from 0xf233dcee88fe0abe +import NonFungibleToken from 0x1d7e57aa55817448 +import MetadataViews from 0x1d7e57aa55817448 +import CrossVMMetadataViews from 0x1d7e57aa55817448 +import ViewResolver from 0x1d7e57aa55817448 + +import EVM from 0xe467b9dd11fa00df + +import IBridgePermissions from 0x1e4aa0b87d10b141 +import ICrossVM from 0x1e4aa0b87d10b141 +import IEVMBridgeNFTMinter from 0x1e4aa0b87d10b141 +import IEVMBridgeTokenMinter from 0x1e4aa0b87d10b141 +import IFlowEVMNFTBridge from 0x1e4aa0b87d10b141 +import IFlowEVMTokenBridge from 0x1e4aa0b87d10b141 +import CrossVMNFT from 0x1e4aa0b87d10b141 +import CrossVMToken from 0x1e4aa0b87d10b141 +import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141 +import FlowEVMBridgeCustomAssociations from 0x1e4aa0b87d10b141 +import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 +import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141 +import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141 +import FlowEVMBridgeNFTEscrow from 0x1e4aa0b87d10b141 +import FlowEVMBridgeTokenEscrow from 0x1e4aa0b87d10b141 +import FlowEVMBridgeTemplates from 0x1e4aa0b87d10b141 +import SerializeMetadata from 0x1e4aa0b87d10b141 + +/// The FlowEVMBridge contract is the main entrypoint for bridging NFT & FT assets between Flow & FlowEVM. +/// +/// Before bridging, be sure to onboard the asset type which will configure the bridge to handle the asset. From there, +/// the asset can be bridged between VMs via the COA as the entrypoint. +/// +/// See also: +/// - Code in context: https://github.com/onflow/flow-evm-bridge +/// - FLIP #237: https://github.com/onflow/flips/pull/233 +/// +access(all) +contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge { + + /************* + Events + **************/ + + /// Emitted any time a new asset type is onboarded to the bridge + access(all) + event Onboarded(type: String, cadenceContractAddress: Address, evmContractAddress: String) + /// Denotes a defining contract was deployed to the bridge account + access(all) + event BridgeDefiningContractDeployed( + contractName: String, + assetName: String, + symbol: String, + isERC721: Bool, + evmContractAddress: String + ) + /// Emitted whenever a bridged NFT is burned as a part of the bridging process. In the context of this contract, + /// this only occurs when an EVM-native ERC721 updates from a bridged NFT to their own custom Cadence NFT. + access(all) + event BridgedNFTBurned(type: String, id: UInt64, evmID: UInt256, uuid: UInt64, erc721Address: String) + + /************************** + Public Onboarding + **************************/ + + /// Onboards a given asset by type to the bridge. Since we're onboarding by Cadence Type, the asset must be defined + /// in a third-party contract. Attempting to onboard a bridge-defined asset will result in an error as the asset has + /// already been onboarded to the bridge. + /// + /// @param type: The Cadence Type of the NFT to be onboarded + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun onboardByType(_ type: Type, feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) { + pre { + !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" + !FlowEVMBridgeConfig.isCadenceTypeBlocked(type): + "This Cadence Type ".concat(type.identifier).concat(" is currently blocked from being onboarded") + self.typeRequiresOnboarding(type) == true: "Onboarding is not needed for this type" + FlowEVMBridgeUtils.typeAllowsBridging(type): + "This Cadence Type ".concat(type.identifier).concat(" is currently opted-out of bridge onboarding") + FlowEVMBridgeUtils.isCadenceNative(type: type): "Only Cadence-native assets can be onboarded by Type" + } + /* Custom cross-VM Implementation check */ + // + // Register as a custom cross-VM implementation if detected + if FlowEVMBridgeUtils.getEVMPointerView(forType: type) != nil { + self.registerCrossVMNFT(type: type, fulfillmentMinter: nil, feeProvider: feeProvider) + return + } + + /* Provision fees */ + // + // Withdraw from feeProvider and deposit to self + FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee) + + /* EVM setup */ + // + // Deploy an EVM defining contract via the FlowBridgeFactory.sol contract + let onboardingValues = self.deployEVMContract(forAssetType: type) + + /* Cadence escrow setup */ + // + // Initialize bridge escrow for the asset based on its type + if type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) { + FlowEVMBridgeNFTEscrow.initializeEscrow( + forType: type, + name: onboardingValues.name, + symbol: onboardingValues.symbol, + erc721Address: onboardingValues.evmContractAddress + ) + } else if type.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + let createVaultFunction = FlowEVMBridgeUtils.getCreateEmptyVaultFunction(forType: type) + ?? panic("Could not retrieve createEmptyVault function for the given type") + let vault <-createVaultFunction(type) + assert( + vault.getType() == type, + message: "Requested to onboard type=".concat(type.identifier).concat( "but contract returned type=").concat(vault.getType().identifier) + ) + FlowEVMBridgeTokenEscrow.initializeEscrow( + with: <-vault, + name: onboardingValues.name, + symbol: onboardingValues.symbol, + decimals: onboardingValues.decimals!, + evmTokenAddress: onboardingValues.evmContractAddress + ) + } else { + panic("Attempted to onboard unsupported type: ".concat(type.identifier)) + } + + /* Confirmation */ + // + assert( + FlowEVMBridgeNFTEscrow.isInitialized(forType: type) || FlowEVMBridgeTokenEscrow.isInitialized(forType: type), + message: "Failed to initialize escrow for given type" + ) + + emit Onboarded( + type: type.identifier, + cadenceContractAddress: FlowEVMBridgeUtils.getContractAddress(fromType: type)!, + evmContractAddress: onboardingValues.evmContractAddress.toString() + ) + } + + /// Onboards a given EVM contract to the bridge. Since we're onboarding by EVM Address, the asset must be defined in + /// a third-party EVM contract. Attempting to onboard a bridge-defined asset will result in an error as onboarding + /// is not required. + /// + /// @param address: The EVMAddress of the ERC721 or ERC20 to be onboarded + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun onboardByEVMAddress( + _ address: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" + !FlowEVMBridgeConfig.isEVMAddressBlocked(address): + "This EVM contract ".concat(address.toString()).concat(" is currently blocked from being onboarded") + } + /* Custom cross-VM Implementation check */ + // + let cadenceAddr = FlowEVMBridgeUtils.getDeclaredCadenceAddressFromCrossVM(evmContract: address) + let cadenceType = FlowEVMBridgeUtils.getDeclaredCadenceTypeFromCrossVM(evmContract: address) + // Register as a custom cross-VM implementation if detected + if cadenceAddr != nil && cadenceType != nil { + self.registerCrossVMNFT(type: cadenceType!, fulfillmentMinter: nil, feeProvider: feeProvider) + return + } + + /* Validate the EVM contract */ + // + // Ensure the project has not opted out of bridge support + assert( + FlowEVMBridgeUtils.evmAddressAllowsBridging(address), + message: "This contract is not supported as defined by the project's development team" + ) + assert( + self.evmAddressRequiresOnboarding(address) == true, + message: "Onboarding is not needed for this contract" + ) + + /* Provision fees */ + // + // Withdraw fee from feeProvider and deposit + FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee) + + /* Setup Cadence-defining contract */ + // + // Deploy a defining Cadence contract to the bridge account + self.deployDefiningContract(evmContractAddress: address) + } + + /// Registers a custom cross-VM NFT implementation, allowing projects to integrate their Cadence & EVM contracts + /// such that the VM bridge facilitates movement between VMs as the integrated implementations. + /// + /// @param type: The NFT Type to register as cross-VM NFT + /// @param fulfillmentMinter: The optional NFTFulfillmentMinter Capability. This parameter is required for + /// EVM-native NFTs + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun registerCrossVMNFT( + type: Type, + fulfillmentMinter: Capability?, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + FlowEVMBridgeUtils.typeAllowsBridging(type): + "This Cadence Type \(type.identifier) is currently opted-out of bridge onboarding" + type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): + "The provided Type \(type.identifier) is not an NFT - only NFTs can register as cross-VM" + !type.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "The provided Type \(type.identifier) is also a FungibleToken Vault - only NFTs can register as cross-VM" + !FlowEVMBridgeConfig.isCadenceTypeBlocked(type): + "Type \(type.identifier) has been blocked from onboarding" + FlowEVMBridgeUtils.isCadenceNative(type: type): + "Attempting to register a bridge-deployed NFT - cannot update a bridge-defined asset. If updating your EVM " + .concat("contract's Cadence association, deploy your Cadence NFT contract and register using the newly defined Cadence type") + FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) == nil: + "A custom association has already been declared for type \(type.identifier) with EVM address " + .concat(FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)!.toString()) + .concat(". Custom associations can only be declared once for any given Cadence Type or EVM contract") + fulfillmentMinter?.check() ?? true: + "NFTFulfillmentMinter Capability is invalid - Issue a new " + .concat("Capability and try again") + fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().address! == type.address! : true: + "NFTFulfillmentMinter must be defined by a contract deployed to the registered type address \(type.address!) " + .concat(" but found defining address of \(fulfillmentMinter!.borrow()!.getType().address!)") + } + /* Provision fees */ + // + // Withdraw fee from feeProvider and deposit + FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee) + + /* Get pointers from both contracts */ + // + // Get the Cadence side EVMPointer + let evmPointer = FlowEVMBridgeUtils.getEVMPointerView(forType: type) + ?? panic("The CrossVMMetadataViews.EVMPointer is not supported by the type \(type.identifier).") + // EVM contract checks + assert(!FlowEVMBridgeConfig.isEVMAddressBlocked(evmPointer.evmContractAddress), + message: "Type \(type.identifier) has been blocked from onboarding.") + assert( + FlowEVMBridgeUtils.evmAddressAllowsBridging(evmPointer.evmContractAddress), + message: "The EVM contract \(evmPointer.evmContractAddress.toString()) developers have opted out of VM bridge integration." + ) + assert( + FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmPointer.evmContractAddress) == nil, + message: "A custom association has already been declared for EVM address \(evmPointer.evmContractAddress.toString()) with Cadence Type " + .concat(FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmPointer.evmContractAddress)?.identifier ?? "") + .concat(". Custom associations can only be declared once for any given Cadence Type or EVM contract") + ) + assert( + FlowEVMBridgeUtils.isERC721(evmContractAddress: evmPointer.evmContractAddress) + && !FlowEVMBridgeUtils.isERC20(evmContractAddress: evmPointer.evmContractAddress), + message: "Cross-VM NFTs must be implemented as ERC721 exclusively, but detected an invalid EVM interface " + .concat("at EVM contract \(evmPointer.evmContractAddress.toString())") + ) + + // Get pointer on EVM side + let cadenceAddr = FlowEVMBridgeUtils.getDeclaredCadenceAddressFromCrossVM(evmContract: evmPointer.evmContractAddress) + ?? panic("Could not retrieve a Cadence address declaration from the EVM contract \(evmPointer.evmContractAddress.toString())") + let cadenceType = FlowEVMBridgeUtils.getDeclaredCadenceTypeFromCrossVM(evmContract: evmPointer.evmContractAddress) + ?? panic("Could not retrieve a Cadence Type declaration from the EVM contract \(evmPointer.evmContractAddress.toString())") + + /* Pointer validation */ + // + // Assert both point to each other + assert( + type.address == cadenceAddr, + message: "Mismatched Cadence Address pointers: \(type.address!.toString()) and \(cadenceAddr.toString())" + ) + assert( + type == cadenceType, + message: "Mismatched type pointers: \(type.identifier) and \(cadenceType.identifier)" + ) + + /* Cross-VM conformance check */ + // + // Check supportsInterface() for CrossVMBridgeERC721Fulfillment if NFT is Cadence-native + if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence { + assert(FlowEVMBridgeUtils.supportsCadenceNativeNFTEVMInterfaces(evmContract: evmPointer.evmContractAddress), + message: "Corresponding EVM contract does not implement necessary EVM interfaces ICrossVMBridgeERC721Fulfillment " + .concat("and/or ICrossVMBridgeCallable. All Cadence-native cross-VM NFTs must implement these interfaces and ") + .concat("grant the bridge COA the ability to fulfill bridge requests moving NFTs into EVM.")) + let designatedVMBridgeAddress = FlowEVMBridgeUtils.getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: evmPointer.evmContractAddress) + ?? panic("Could not recover declared VM bridge address from EVM contract \(evmPointer.evmContractAddress.toString()). " + .concat("Ensure the contract conforms to ICrossVMBridgeCallable and declare the vmBridgeAddress as \(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString())")) + assert(designatedVMBridgeAddress.equals(FlowEVMBridgeUtils.getBridgeCOAEVMAddress()), + message: "ICrossVMBridgeCallable declared \(designatedVMBridgeAddress.toString())" + .concat(" as vmBridgeAddress which must be declared as \(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString())")) + } + + /* Native VM consistency check */ + // + // Assess if the NFT has been previously onboarded to the bridge + let legacyEVMAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) + let legacyCadenceAssoc = FlowEVMBridgeConfig.getLegacyTypeAssociated(with: evmPointer.evmContractAddress) + assert(legacyEVMAssoc == nil || legacyCadenceAssoc == nil, + message: "Both the EVM contract \(evmPointer.evmContractAddress.toString()) and the Cadence Type \(type.identifier) " + .concat("have already been onboarded to the VM bridge - one side of this association will have to be redeployed ") + .concat("and the declared association updated to a non-onboarded target in order to register as a custom cross-VM asset.")) + // Ensure the native VM is consistent if the NFT has been previously onboarded via the permissionless path + if legacyEVMAssoc != nil { + assert(evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence, + message: "Attempting to register NFT \(type.identifier) as EVM-native after it has already been " + .concat("onboarded as Cadence-native. This NFT must be configured as Cadence-native with an ERC721 ") + .concat("implementing CrossVMBridgeERC721Fulfillment base contract allowing the bridge to fulfill ") + .concat("NFTs moving into EVM")) + } else if legacyCadenceAssoc != nil { + assert(evmPointer.nativeVM == CrossVMMetadataViews.VM.EVM, + message: "Attempting to register NFT \(type.identifier) as Cadence-native after it has already been " + .concat("onboarded as EVM-native. This NFT must be configured as EVM-native and provide an NFTFulfillmentMinter ") + .concat("Capability so the bridge may fulfill NFTs moving into Cadence.")) + } + + FlowEVMBridgeCustomAssociations.saveCustomAssociation( + type: type, + evmContractAddress: evmPointer.evmContractAddress, + nativeVM: evmPointer.nativeVM, + updatedFromBridged: legacyEVMAssoc != nil || legacyCadenceAssoc != nil, + fulfillmentMinter: fulfillmentMinter + ) + + if !FlowEVMBridgeNFTEscrow.isInitialized(forType: type) { + let name = FlowEVMBridgeUtils.getName(evmContractAddress: evmPointer.evmContractAddress) + let symbol = FlowEVMBridgeUtils.getSymbol(evmContractAddress: evmPointer.evmContractAddress) + FlowEVMBridgeNFTEscrow.initializeEscrow( + forType: type, + name: name, + symbol: symbol, + erc721Address: evmPointer.evmContractAddress + ) + } + } + + /************************* + NFT Handling + **************************/ + + /// Public entrypoint to bridge NFTs from Cadence to EVM as ERC721. + /// + /// @param token: The NFT to be bridged + /// @param to: The NFT recipient in FlowEVM + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun bridgeNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" + !token.isInstance(Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported" + self.typeRequiresOnboarding(token.getType()) == false: "NFT must first be onboarded" + FlowEVMBridgeConfig.isTypePaused(token.getType()) == false: "Bridging is currently paused for this NFT" + } + let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: token.getType()) + let customAssocByType = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: token.getType()) + let customAssocByEVMAddr = bridgedAssoc != nil ? FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssoc!) : nil + if bridgedAssoc != nil && customAssocByType == nil && customAssocByEVMAddr == nil { + // Common case - bridge-defined counterpart in non-native VM + return self.handleDefaultNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) + } else if customAssocByType != nil && customAssocByEVMAddr == nil { + // NFT is registered as cross-VM + return self.handleCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) + } else if customAssocByType == nil && customAssocByEVMAddr != nil { + // Dealing with a bridge-defined NFT after a custom association has been configured + return self.handleUpdatedBridgedNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) + } + // customAssocByType != nil && customAssocByEVMAddr != nil + panic("Unknown error encountered bridging NFT \(token.getType().identifier) with ID \(token.id) to EVM recipient \(to.toString())") + } + + /// Entrypoint to bridge ERC721 from EVM to Cadence as NonFungibleToken.NFT + /// + /// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via + /// `protectedTransferCall`) is validated before the bridge request is executed. + /// @param calldata: Caller-provided approve() call, enabling contract COA to operate on NFT in EVM contract + /// @param id: The NFT ID to bridged + /// @param evmContractAddress: Address of the EVM address defining the NFT being bridged - also call target + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the + /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. + /// + /// @returns The bridged NFT + /// + access(account) + fun bridgeNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + pre { + !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" + !type.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported" + self.typeRequiresOnboarding(type) == false: "NFT must first be onboarded" + FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this NFT" + } + let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) + let customAssocByType = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) + let customAssocByEVMAddr = bridgedAssoc != nil ? FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssoc!) : nil + // Initialize the internal handler method that will be used to move the NFT from EVM + var handler: (fun (EVM.EVMAddress, Type, UInt256, auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, fun (EVM.EVMAddress): EVM.Result): @{NonFungibleToken.NFT})? = nil + if bridgedAssoc != nil && customAssocByType == nil && customAssocByEVMAddr == nil { + // Common case - bridge-defined counterpart in non-native VM + handler = self.handleDefaultNFTFromEVM + } else if customAssocByType != nil && customAssocByEVMAddr == nil { + // NFT is registered as cross-VM + handler = self.handleCrossVMNFTFromEVM + } else if customAssocByType == nil && customAssocByEVMAddr != nil { + // Dealing with a bridge-defined NFT after a custom association has been configured + handler = self.handleUpdatedBridgedNFTFromEVM + } else { + // customAssocByType != nil && customAssocByEVMAddr != nil + panic("Unknown error encountered bridging NFT \(type.identifier) with ID \(id) from EVM owner \(owner.toString())") + } + // Return the bridged NFT, using the appropriate handler + return <- handler!(owner: owner, type: type, id: id, feeProvider: feeProvider, protectedTransferCall: protectedTransferCall) + } + + /************************** + FT Handling + ***************************/ + + /// Public entrypoint to bridge FTs from Cadence to EVM as ERC20 tokens. + /// + /// @param vault: The fungible token Vault to be bridged + /// @param to: The fungible token recipient in EVM + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun bridgeTokensToEVM( + vault: @{FungibleToken.Vault}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" + !vault.isInstance(Type<@{NonFungibleToken.NFT}>()): "Mixed asset types are not yet supported" + self.typeRequiresOnboarding(vault.getType()) == false: "FungibleToken must first be onboarded" + FlowEVMBridgeConfig.isTypePaused(vault.getType()) == false: "Bridging is currently paused for this token" + } + /* Handle $FLOW requests via EVM interface & return */ + // + let vaultType = vault.getType() + + // Gather the vault balance before acting on the resource + let vaultBalance = vault.balance + // Initialize fee amount to 0.0 and assign as appropriate for how the token is handled + var feeAmount = 0.0 + + /* TokenHandler coverage */ + // + // Some tokens pre-dating bridge require special case handling - borrow handler and passthrough to fulfill + if FlowEVMBridgeConfig.typeHasTokenHandler(vaultType) { + let handler = FlowEVMBridgeConfig.borrowTokenHandler(vaultType) + ?? panic("Could not retrieve handler for the given type") + handler.fulfillTokensToEVM(tokens: <-vault, to: to) + + // Here we assume burning Vault in Cadence which doesn't require storage consumption + feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount) + return + } + + /* Escrow or burn tokens depending on native environment */ + // + // In most all other cases, if Cadence-native then tokens must be escrowed + if FlowEVMBridgeUtils.isCadenceNative(type: vaultType) { + // Lock the FT balance & calculate the extra used by the FT if any + let storageUsed = FlowEVMBridgeTokenEscrow.lockTokens(<-vault) + // Calculate the bridge fee on current rates + feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: storageUsed) + } else { + // Since not Cadence-native, bridge defines the token - burn the vault and calculate the fee + Burner.burn(<-vault) + feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + } + + /* Provision fees */ + // + // Withdraw fee amount from feeProvider and deposit + FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount) + + /* Gather identifying information */ + // + // Does the bridge control the EVM contract associated with this type? + let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: vaultType) + ?? panic("No EVMAddress found for vault type") + // Convert the vault balance to a UInt256 + let bridgeAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount( + vaultBalance, + erc20Address: associatedAddress + ) + assert(bridgeAmount > UInt256(0), message: "Amount to bridge must be greater than 0") + + // Determine if the EVM contract is bridge-owned - affects how tokens are transmitted to recipient + let isFactoryDeployed = FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress) + + /* Transmit tokens to recipient */ + // + // Mint or transfer based on the bridge's EVM contract authority, making needed state assertions to confirm + if isFactoryDeployed { + FlowEVMBridgeUtils.mustMintERC20(to: to, amount: bridgeAmount, erc20Address: associatedAddress) + } else { + FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: bridgeAmount, erc20Address: associatedAddress) + } + } + + /// Entrypoint to bridge ERC20 tokens from EVM to Cadence as FungibleToken Vaults + /// + /// @param owner: The EVM address of the FT owner. Current ownership and successful transfer (via + /// `protectedTransferCall`) is validated before the bridge request is executed. + /// @param calldata: Caller-provided approve() call, enabling contract COA to operate on FT in EVM contract + /// @param amount: The amount of tokens to be bridged + /// @param evmContractAddress: Address of the EVM address defining the FT being bridged - also call target + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// @param protectedTransferCall: A function that executes the transfer of the FT from the named owner to the + /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. + /// + /// @returns The bridged fungible token Vault + /// + access(account) + fun bridgeTokensFromEVM( + owner: EVM.EVMAddress, + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (): EVM.Result + ): @{FungibleToken.Vault} { + pre { + !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" + !type.isSubtype(of: Type<@{NonFungibleToken.Collection}>()): "Mixed asset types are not yet supported" + self.typeRequiresOnboarding(type) == false: "FungibleToken must first be onboarded" + FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this token" + } + /* Provision fees */ + // + // Withdraw from feeProvider and deposit to self + let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) + FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount) + + /* TokenHandler case coverage */ + // + // Some tokens pre-dating bridge require special case handling. If such a case, fulfill via the related handler + if FlowEVMBridgeConfig.typeHasTokenHandler(type) { + // - borrow handler and passthrough to fulfill + let handler = FlowEVMBridgeConfig.borrowTokenHandler(type) + ?? panic("Could not retrieve handler for the given type") + return <-handler.fulfillTokensFromEVM( + owner: owner, + type: type, + amount: amount, + protectedTransferCall: protectedTransferCall + ) + } + + /* Gather identifying information */ + // + // Get the EVMAddress of the ERC20 contract associated with the type + let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) + ?? panic("No EVMAddress found for token type") + // Find the Cadence defining address and contract name + let definingAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)! + let definingContractName = FlowEVMBridgeUtils.getContractName(fromType: type)! + // Convert the amount to a ufix64 so the amount can be settled on the Cadence side + let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(amount, erc20Address: associatedAddress) + assert(ufixAmount > 0.0, message: "Amount to bridge must be greater than 0") + + /* Execute the transfer call and make needed state assertions */ + // + FlowEVMBridgeUtils.mustEscrowERC20( + owner: owner, + amount: amount, + erc20Address: associatedAddress, + protectedTransferCall: protectedTransferCall + ) + + /* Bridge-defined tokens are minted in Cadence */ + // + // If the Cadence Vault is bridge-defined, mint the tokens + if definingAddress == self.account.address { + let minter = getAccount(definingAddress).contracts.borrow<&{IEVMBridgeTokenMinter}>(name: definingContractName)! + return <- minter.mintTokens(amount: ufixAmount) + } + + /* Cadence-native tokens are withdrawn from escrow */ + // + // Confirm the EVM defining contract is bridge-owned before burning tokens + assert( + FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress), + message: "Unexpected error bridging FT from EVM" + ) + // Burn the EVM tokens that have now been transferred to the bridge in EVM + let burnResult: EVM.Result = FlowEVMBridgeUtils.call( + signature: "burn(uint256)", + targetEVMAddress: associatedAddress, + args: [amount], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(burnResult.status == EVM.Status.successful, message: "Burn of EVM tokens failed") + + // Unlock from escrow and return + return <-FlowEVMBridgeTokenEscrow.unlockTokens(type: type, amount: ufixAmount) + } + + /************************** + Public Getters + **************************/ + + /// Returns the EVM address associated with the provided type + /// + access(all) + view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? { + return FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) + } + + /// Retrieves the bridge contract's COA EVMAddress + /// + /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM + /// + access(all) + view fun getBridgeCOAEVMAddress(): EVM.EVMAddress { + return FlowEVMBridgeUtils.borrowCOA().address() + } + + /// Returns whether an asset needs to be onboarded to the bridge + /// + /// @param type: The Cadence Type of the asset + /// + /// @returns Whether the asset needs to be onboarded + /// + access(all) + view fun typeRequiresOnboarding(_ type: Type): Bool? { + if !FlowEVMBridgeUtils.isValidCadenceAsset(type: type) { + return nil + } + return FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) == nil && + !FlowEVMBridgeConfig.typeHasTokenHandler(type) + } + + /// Returns whether an EVM-native asset needs to be onboarded to the bridge + /// + /// @param address: The EVMAddress of the asset + /// + /// @returns Whether the asset needs to be onboarded, nil if the defined asset is not supported by this bridge + /// + access(all) + fun evmAddressRequiresOnboarding(_ address: EVM.EVMAddress): Bool? { + // See if the bridge already has a known type associated with the given address + if FlowEVMBridgeConfig.getTypeAssociated(with: address) != nil { + return false + } + // Dealing with EVM-native asset, check if it's NFT or FT exclusively + if FlowEVMBridgeUtils.isValidEVMAsset(evmContractAddress: address) { + return true + } + // Not onboarded and not a valid asset, so return nil + return nil + } + + /************************** + Internal Helpers + ***************************/ + + /// Deploys templated EVM contract via Solidity Factory contract supporting bridging of a given asset type + /// + /// @param forAssetType: The Cadence Type of the asset + /// + /// @returns The EVMAddress of the deployed contract + /// + access(self) + fun deployEVMContract(forAssetType: Type): FlowEVMBridgeUtils.EVMOnboardingValues { + pre { + FlowEVMBridgeUtils.isValidCadenceAsset(type: forAssetType): + "Asset type is not supported by the bridge" + } + let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) + + let onboardingValues = FlowEVMBridgeUtils.getCadenceOnboardingValues(forAssetType: forAssetType) + + let deployedContractAddress = FlowEVMBridgeUtils.mustDeployEVMContract( + name: onboardingValues.name, + symbol: onboardingValues.symbol, + cadenceAddress: onboardingValues.contractAddress, + flowIdentifier: onboardingValues.identifier, + contractURI: onboardingValues.contractURI, + isERC721: isNFT + ) + + // Associate the deployed contract with the given type & return the deployed address + FlowEVMBridgeConfig.associateType(forAssetType, with: deployedContractAddress) + return FlowEVMBridgeUtils.EVMOnboardingValues( + evmContractAddress: deployedContractAddress, + name: onboardingValues.name, + symbol: onboardingValues.symbol, + decimals: isNFT ? nil : FlowEVMBridgeConfig.defaultDecimals, + contractURI: onboardingValues.contractURI, + cadenceContractName: FlowEVMBridgeUtils.getContractName(fromType: forAssetType)!, + isERC721: isNFT + ) + } + + /// Helper for deploying templated defining contract supporting EVM-native asset bridging to Cadence + /// Deploys either NFT or FT contract depending on the provided type + /// + /// @param evmContractAddress: The EVMAddress currently defining the asset to be bridged + /// + access(self) + fun deployDefiningContract(evmContractAddress: EVM.EVMAddress) { + // Gather identifying information about the EVM contract + let evmOnboardingValues = FlowEVMBridgeUtils.getEVMOnboardingValues(evmContractAddress: evmContractAddress) + + // Get Cadence code from template & deploy to the bridge account + let cadenceCode: [UInt8] = FlowEVMBridgeTemplates.getBridgedAssetContractCode( + evmOnboardingValues.cadenceContractName, + isERC721: evmOnboardingValues.isERC721 + ) ?? panic("Problem retrieving code for Cadence-defining contract") + if evmOnboardingValues.isERC721 { + self.account.contracts.add( + name: evmOnboardingValues.cadenceContractName, + code: cadenceCode, + evmOnboardingValues.name, + evmOnboardingValues.symbol, + evmContractAddress, + evmOnboardingValues.contractURI + ) + } else { + self.account.contracts.add( + name: evmOnboardingValues.cadenceContractName, + code: cadenceCode, + evmOnboardingValues.name, + evmOnboardingValues.symbol, + evmOnboardingValues.decimals!, + evmContractAddress, evmOnboardingValues.contractURI + ) + } + + emit BridgeDefiningContractDeployed( + contractName: evmOnboardingValues.cadenceContractName, + assetName: evmOnboardingValues.name, + symbol: evmOnboardingValues.symbol, + isERC721: evmOnboardingValues.isERC721, + evmContractAddress: evmContractAddress.toString() + ) + } + + /// Escrows the provided NFT and withdraws the bridging fee on the basis of a base fee + storage fee + /// + access(self) + fun escrowNFTAndWithdrawFee( + token: @{NonFungibleToken.NFT}, + from: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + // 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(from, feeAmount: feeAmount) + } + + /// Handle permissionlessly onboarded NFTs where the bridge deployed and manages the non-native contract + /// + access(self) + fun handleDefaultNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + /* Gather identifying information */ + // + let tokenType = token.getType() + let tokenID = token.id + let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id) + + /* Metadata assignment */ + // + // Grab the URI from the NFT if available + var uri: String = "" + var symbol: String = "" + // Default to project-specified URI + if let metadata = token.resolveView(Type()) as! MetadataViews.EVMBridgedMetadata? { + uri = metadata.uri.uri() + symbol = metadata.symbol + } else { + // Otherwise, serialize the NFT + uri = SerializeMetadata.serializeNFTMetadataAsURI(&token as &{NonFungibleToken.NFT}) + } + + /* Secure NFT in escrow & deposit calculated fees */ + // + // Withdraw fee from feeProvider and deposit + self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider) + + /* 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) + + /* Third-party controlled ERC721 handling */ + // + // Not bridge-controlled, transfer existing ownership + if !isFactoryDeployed { + FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID) + return + } + + /* 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 & update the URI to reflect current metadata + FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID) + FlowEVMBridgeUtils.mustUpdateTokenURI(erc721Address: associatedAddress, id: evmID, uri: uri) + } else { + // Otherwise mint with current URI + FlowEVMBridgeUtils.mustSafeMintERC721(erc721Address: associatedAddress, to: to, id: evmID, uri: uri) + } + // Update the bridged ERC721 symbol if different than the Cadence-defined EVMBridgedMetadata.symbol + if symbol.length > 0 && symbol != FlowEVMBridgeUtils.getSymbol(evmContractAddress: associatedAddress) { + FlowEVMBridgeUtils.tryUpdateSymbol(associatedAddress, symbol: symbol) + } + + } + + /// Handler to move registered cross-VM NFTs to EVM + /// + access(self) + fun handleCrossVMNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) { + let evmPointer = FlowEVMBridgeCustomAssociations.getEVMPointerAsRegistered(forType: token.getType()) + ?? panic("Could not find custom association for cross-VM NFT \(token.getType().identifier) with id \(token.id). " + .concat("Ensure this NFT has been registered as a cross-VM.")) + return evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence ? + self.handleCadenceNativeCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) : + self.handleEVMNativeCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) + } + + /// Handler to move registered cross-VM Cadence-native NFTs to EVM + /// + access(self) + fun handleCadenceNativeCrossVMNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + let type = token.getType() + let id = UInt256(token.id) + + // Check on permissionlessly onboarded association & bridged token existence + if let bridgedERC721 = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) { + // Burn bridged ERC721 if exists - will be replaced by custom ERC721 implementation + if FlowEVMBridgeUtils.erc721Exists(erc721Address: bridgedERC721, id: id) { + FlowEVMBridgeUtils.mustBurnERC721(erc721Address: bridgedERC721, id: id) + } + } + // Make ICrossVMBridgeERC721Fulfillment.fulfillToEVM call, passing any metadata resolved by the NFT allowing + // the ERC721 implementation to update metadata if needed. The base CrossVMBridgeERC721Fulfillment contract + // checks for existence and mints if needed or transfers from vm bridge escrow, following a mint/escrow + // pattern. + let customERC721 = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)! + let data = CrossVMMetadataViews.getEVMBytesMetadata(&token as &{ViewResolver.Resolver}) + FlowEVMBridgeUtils.mustFulfillNFTToEVM(erc721Address: customERC721, to: to, id: id, maybeBytes: data?.bytes) + + // Escrow the NFT & charge the bridge fee + self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider) + } + + /// Handler to move cross-VM EVM-native NFTs to EVM + /// + access(self) + fun handleEVMNativeCrossVMNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + if !FlowEVMBridgeUtils.isCadenceNative(type: token.getType()) { + // Bridge-defined token means this is a bridged token - passthrough to appropriate handler method + return self.handleUpdatedBridgedNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) + } + let type = token.getType() + let id = UInt256(token.id) + let customERC721 = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: token.getType())! + + // Escrow the NFT & charge the bridge fee + self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider) + + // Transfer the ERC721 from escrow to the named recipient + FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: customERC721, to: to, id: id) + } + + /// Handler to move NFTs to EVM that were once bridge-defined but were later updated to a registered custom + /// cross-VM implementation + /// + access(self) + fun handleUpdatedBridgedNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + !FlowEVMBridgeUtils.isCadenceNative(type: token.getType()): + "Expected a bridge-defined NFT but was provided NFT of type \(token.getType().identifier)" + } + let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: token.getType())! + let updatedCadenceAssociation = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssociation) + ?? panic("Could not find a custom cross-VM association for NFT \(token.getType().identifier) #\(token.id). " + .concat("The handleUpdatedBridgedNFTToEVM route is intended for bridged Cadence NFTs associated with ") + .concat(" ERC721 contracts that have registered as a custom cross-VM NFT collection.")) + let tokenRef = (&token as &{NonFungibleToken.NFT}) as! &{CrossVMNFT.EVMNFT} + let evmID = tokenRef.evmID + let bridgedToken <- token as! @{CrossVMNFT.EVMNFT} + emit BridgedNFTBurned( + type: bridgedToken.getType().identifier, + id: bridgedToken.id, + evmID: bridgedToken.evmID, + uuid: bridgedToken.uuid, + erc721Address: bridgedAssociation.toString() + ) + Burner.burn(<-bridgedToken) + // Transfer the ERC721 from escrow to the named recipient + FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: bridgedAssociation, to: to, id: evmID) + } + + /// Handle permissionlessly onboarded NFTs where the bridge deployed and manages the non-native contract + /// + access(self) + fun handleDefaultNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + /* 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") + // Execute the transfer call and make needed state assertions to confirm escrow from named owner + FlowEVMBridgeUtils.mustEscrowERC721( + owner: owner, + id: id, + erc721Address: associatedAddress, + protectedTransferCall: protectedTransferCall + ) + + /* 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) + + // If the NFT is bridge-defined, update the URI from the source ERC721 contract + if self.account.address == FlowEVMBridgeUtils.getContractAddress(fromType: type) { + nftContract!.updateTokenURI(evmID: id, newURI: uri) + } + + return <-nft + } + + /* 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 + } + + /// Handler to move registered cross-VM NFTs from EVM + /// + access(self) + fun handleCrossVMNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + let evmPointer = FlowEVMBridgeCustomAssociations.getEVMPointerAsRegistered(forType: type) + ?? panic("Could not find custom association for cross-VM NFT \(type.identifier) with id \(id). " + .concat("Ensure this NFT has been registered as a cross-VM.")) + if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence { + return <- self.handleCadenceNativeCrossVMNFTFromEVM( + owner: owner, + type: type, + id: id, + feeProvider: feeProvider, + protectedTransferCall: protectedTransferCall + ) + } else { // EVM-native case as there are only two possible VMs + return <- self.handleEVMNativeCrossVMNFTFromEVM( + owner: owner, + type: type, + id: id, + feeProvider: feeProvider, + protectedTransferCall: protectedTransferCall + ) + } + } + + /// Handler to move registered Cadence-native cross-VM NFTs from EVM + /// + access(self) + fun handleCadenceNativeCrossVMNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + pre { + FlowEVMBridgeUtils.isCadenceNative(type: type): + "Attempting to move bridge-defined NFT type \(type.identifier) from EVM as Cadence-native via handleCadenceNativeCrossVMNFTFromEVM" + } + let configInfo = FlowEVMBridgeCustomAssociations.getCustomConfigInfo(forType: type)! + let customERC721 = configInfo.evmPointer.evmContractAddress + let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) + let bridgedTokenExists = bridgedAssociation != nil ? FlowEVMBridgeUtils.erc721Exists(erc721Address: bridgedAssociation!, id: id) : false + if configInfo.updatedFromBridged && bridgedTokenExists { + let bridgedTokenOwner = FlowEVMBridgeUtils.ownerOf(id: id, evmContractAddress: bridgedAssociation!)! + if bridgedTokenOwner.equals(owner) { + FlowEVMBridgeUtils.mustEscrowERC721( + owner: owner, + id: id, + erc721Address: bridgedAssociation!, + protectedTransferCall: protectedTransferCall + ) + } else if bridgedTokenOwner.equals(customERC721) { + // Bridged token owned by custom ERC721 - treat as OpenZeppelin's ERC721Wrapper, escrow & unwrap + FlowEVMBridgeUtils.mustEscrowERC721( + owner: owner, + id: id, + erc721Address: customERC721, + protectedTransferCall: protectedTransferCall + ) + FlowEVMBridgeUtils.mustUnwrapERC721( + id: id, + erc721WrapperAddress: customERC721, + underlyingEVMAddress: bridgedAssociation! + ) + } else { + // Bridged token not wrapped nor owned by caller - could not determine owner + panic("Bridged ERC721 \(bridgedAssociation!.toString()) ID \(id) still exists after \(type.identifier) " + .concat("was updated to associate with ERC721 \(customERC721.toString()), but the bridged token is ") + .concat("neither wrapped nor owned by caller \(owner.toString()). Could not determine owner.")) + } + // Burn the bridged ERC721, taking the bridged representation out of circulation in favor of custom ERC721 + FlowEVMBridgeUtils.mustBurnERC721(erc721Address: bridgedAssociation!, id: id) + } else { + FlowEVMBridgeUtils.mustEscrowERC721( + owner: owner, + id: id, + erc721Address: customERC721, + protectedTransferCall: protectedTransferCall + ) + } + // Cadence-native NFTs must be in escrow, so unlock & return + return <-FlowEVMBridgeNFTEscrow.unlockNFT( + type: type, + id: FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id)! + ) + } + + /// Handler to move registered cross-VM EVM-native NFTs from EVM + /// + access(self) + fun handleEVMNativeCrossVMNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + pre { + id <= UInt256(UInt64.max): + "NFT ID \(id) is greater than the maximum Cadence ID \(UInt64.max) - cannot fulfill this NFT from EVM" + } + var _type = type + let erc721Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)! + + // Burn if NFT is found to be bridge-defined as it's to be replaced by the registered custom cross-VM NFT + if !FlowEVMBridgeUtils.isCadenceNative(type: type) { + // Find and assign the updated custom Cadence NFT Type associated with the EVM-native ERC721 + _type = FlowEVMBridgeConfig.getTypeAssociated(with: erc721Address)! + + // Burn the bridged NFT token if it's locked + if let cadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) { + let bridgedToken <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID) as! @{CrossVMNFT.EVMNFT} + emit BridgedNFTBurned( + type: bridgedToken.getType().identifier, + id: bridgedToken.id, + evmID: bridgedToken.evmID, + uuid: bridgedToken.uuid, + erc721Address: erc721Address.toString() + ) + Burner.burn(<-bridgedToken) + } + } + + FlowEVMBridgeUtils.mustEscrowERC721(owner: owner, id: id, erc721Address: erc721Address, protectedTransferCall: protectedTransferCall) + + if FlowEVMBridgeNFTEscrow.isLocked(type: type, id: UInt64(id)) { + // Unlock the NFT from escrow + return <-FlowEVMBridgeNFTEscrow.unlockNFT(type: _type, id: UInt64(id)) + } else { + // Otherwise, fulfill via configured NFTFulfillmentMinter + return <- FlowEVMBridgeCustomAssociations.fulfillNFTFromEVM(forType: _type, id: id) + } + } + + access(self) + fun handleUpdatedBridgedNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + pre { + !FlowEVMBridgeUtils.isCadenceNative(type: type): // expect this type to be bridge-defined + "Expected a bridge-defined NFT but was provided NFT of type \(type.identifier)" + id < UInt256(UInt64.max): + "Requested ID \(id) exceeds UIn64.max - Cross-VM NFT IDs must be within UInt64 range across Cadence & EVM implementations" + } + // Assign the legacy and custom associations + let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type)! + let updatedTypeAssoc = FlowEVMBridgeConfig.getTypeAssociated(with: bridgedAssoc)! + + // Confirm custom association is EVM-native + let configInfo = FlowEVMBridgeCustomAssociations.getCustomConfigInfo(forType: updatedTypeAssoc)! + assert(configInfo.evmPointer.nativeVM == CrossVMMetadataViews.VM.EVM, + message: "Expected native VM for ERC721 \(bridgedAssoc.toString()) associated with NFT type \(type.identifier) to be EVM-native") + + FlowEVMBridgeUtils.mustEscrowERC721(owner: owner, id: id, erc721Address: bridgedAssoc, protectedTransferCall: protectedTransferCall) + + // Check if originally associated bridged token is in escrow, burning if so + if let lockedCadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) { + let bridgedToken <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: lockedCadenceID) as! @{CrossVMNFT.EVMNFT} + emit BridgedNFTBurned( + type: bridgedToken.getType().identifier, + id: bridgedToken.id, + evmID: bridgedToken.evmID, + uuid: bridgedToken.uuid, + erc721Address: bridgedAssoc.toString() + ) + Burner.burn(<-bridgedToken) + } + // Either unlock if locked or fulfill via configured NFTFulfillmentMinter + if FlowEVMBridgeNFTEscrow.isLocked(type: updatedTypeAssoc, id: UInt64(id)) { + return <- FlowEVMBridgeNFTEscrow.unlockNFT(type: updatedTypeAssoc, id: UInt64(id)) + } else { + return <- FlowEVMBridgeCustomAssociations.fulfillNFTFromEVM(forType: updatedTypeAssoc, id: id) + } + } +} diff --git a/contracts/external/FlowEVMBridgeAccessor.cdc b/contracts/external/FlowEVMBridgeAccessor.cdc new file mode 100644 index 000000000..7532bd76d --- /dev/null +++ b/contracts/external/FlowEVMBridgeAccessor.cdc @@ -0,0 +1,202 @@ +import NonFungibleToken from 0x1d7e57aa55817448 +import FungibleToken from 0xf233dcee88fe0abe +import FlowToken from 0x1654653399040a61 + +import EVM from 0xe467b9dd11fa00df + +import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 +import FlowEVMBridge from 0x1e4aa0b87d10b141 + +/// This contract defines a mechanism for routing bridge requests from the EVM contract to the Flow-EVM bridge contract +/// +access(all) +contract FlowEVMBridgeAccessor { + + access(all) let StoragePath: StoragePath + + /// BridgeAccessor implementation used by the EVM contract to route bridge calls from COA resources + /// + access(all) + resource BridgeAccessor : EVM.BridgeAccessor { + + /// Passes along the bridge request to dedicated bridge contract + /// + /// @param nft: The NFT to be bridged to EVM + /// @param to: The address of the EVM account to receive the bridged NFT + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(EVM.Bridge) + fun depositNFT( + nft: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + FlowEVMBridge.bridgeNFTToEVM(token: <-nft, to: to, feeProvider: feeProvider) + } + + /// Passes along the bridge request to the dedicated bridge contract, returning the bridged NFT + /// + /// @param caller: A reference to the COA which currently owns the NFT in EVM + /// @param type: The Cadence type of the NFT to be bridged from EVM + /// @param id: The ID of the NFT to be bridged from EVM + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + /// @return The bridged NFT + /// + access(EVM.Bridge) + fun withdrawNFT( + caller: auth(EVM.Call) &EVM.CadenceOwnedAccount, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{NonFungibleToken.NFT} { + // Define a callback function, enabling the bridge to act on the ephemeral COA reference in scope + var executed = false + fun callback(target: EVM.EVMAddress): EVM.Result { + pre { + !executed: "Callback can only be executed once" + FlowEVMBridge.getAssociatedEVMAddress(with: type) ?? FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) != nil: + "Could not find EVM association for NFT Type \(type.identifier) - ensure the NFT has been onboarded to the bridge & try again" + } + post { + executed: "Callback must be executed" + } + // Ensure the call is to an EVM contract known to be associated with the NFT Type as registered with + // the VM Bridge + let callAllowed = FlowEVMBridgeAccessor.isValidEVMTarget(forType: type, target: target) + assert(callAllowed, + message: "Target EVM contract \(target.toString()) is not association with NFT Type \(type.identifier) - COA `safeTransferFrom` callback rejected") + + executed = true + return caller.call( + to: target, + data: EVM.encodeABIWithSignature( + "safeTransferFrom(address,address,uint256)", + [caller.address(), FlowEVMBridge.getBridgeCOAEVMAddress(), id] + ), + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: EVM.Balance(attoflow: 0) + ) + } + // Execute the bridge request + return <- FlowEVMBridge.bridgeNFTFromEVM( + owner: caller.address(), + type: type, + id: id, + feeProvider: feeProvider, + protectedTransferCall: callback + ) + } + + /// Passes along the bridge request to dedicated bridge contract + /// + /// @param vault: The fungible token vault to be bridged to EVM + /// @param to: The address of the EVM account to receive the bridged tokens + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(EVM.Bridge) + fun depositTokens( + vault: @{FungibleToken.Vault}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + FlowEVMBridge.bridgeTokensToEVM(vault: <-vault, to: to, feeProvider: feeProvider) + } + + /// Passes along the bridge request to the dedicated bridge contract, returning the bridged FungibleToken + /// + /// @param caller: A reference to the COA which currently owns the tokens in EVM + /// @param type: The Cadence type of the fungible token vault to be bridged from EVM + /// @param amount: The amount of tokens to be bridged + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + /// @return The bridged FungibleToken Vault + /// + access(EVM.Bridge) + fun withdrawTokens( + caller: auth(EVM.Call) &EVM.CadenceOwnedAccount, + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{FungibleToken.Vault} { + // Define a callback function, enabling the bridge to act on the ephemeral COA reference in scope + var executed = false + fun callback(): EVM.Result { + pre { + !executed: "Callback can only be executed once" + } + post { + executed: "Callback must be executed" + } + executed = true + return caller.call( + to: FlowEVMBridge.getAssociatedEVMAddress(with: type) + ?? panic("No EVM address associated with type"), + data: EVM.encodeABIWithSignature( + "transfer(address,uint256)", + [FlowEVMBridge.getBridgeCOAEVMAddress(), amount] + ), + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: EVM.Balance(attoflow: 0) + ) + } + // Execute the bridge request + return <- FlowEVMBridge.bridgeTokensFromEVM( + owner: caller.address(), + type: type, + amount: amount, + feeProvider: feeProvider, + protectedTransferCall: callback + ) + } + + /// Returns a BridgeRouter resource so a Capability on this BridgeAccessor can be stored in the BridgeRouter + /// + access(EVM.Bridge) fun createBridgeRouter(): @BridgeRouter { + return <-create BridgeRouter() + } + } + + /// BridgeRouter implementation used by the EVM contract to capture a BridgeAccessor Capability and route bridge + /// calls from COA resources to the FlowEVMBridge contract + /// + access(all) resource BridgeRouter : EVM.BridgeRouter { + /// Capability to the BridgeAccessor resource, initialized to nil + access(self) var bridgeAccessorCap: Capability? + + init() { + self.bridgeAccessorCap = nil + } + + /// Returns an EVM.Bridge entitled reference to the underlying BridgeAccessor resource + /// + access(EVM.Bridge) view fun borrowBridgeAccessor(): auth(EVM.Bridge) &{EVM.BridgeAccessor} { + let cap = self.bridgeAccessorCap ?? panic("BridgeAccessor Capabaility is not yet set") + return cap.borrow() ?? panic("Problem retrieving BridgeAccessor reference") + } + + /// Sets the BridgeAccessor Capability in the BridgeRouter + access(EVM.Bridge) fun setBridgeAccessor(_ accessorCap: Capability) { + self.bridgeAccessorCap = accessorCap + } + } + + /// Assesses whether the EVM contract address is associated with the provided type based on bridge associations + /// + access(self) + fun isValidEVMTarget(forType: Type, target: EVM.EVMAddress): Bool { + let currentAssociation = FlowEVMBridge.getAssociatedEVMAddress(with: forType) + let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: forType) + return currentAssociation?.equals(target) ?? false || bridgedAssociation?.equals(target) ?? false + } + + init(publishToEVMAccount: Address) { + self.StoragePath = /storage/flowEVMBridgeAccessor + self.account.storage.save( + <-create BridgeAccessor(), + to: self.StoragePath + ) + let cap = self.account.capabilities.storage.issue(self.StoragePath) + self.account.inbox.publish(cap, name: "FlowEVMBridgeAccessor", recipient: publishToEVMAccount) + } +} diff --git a/contracts/external/FlowEVMBridgeConfig.cdc b/contracts/external/FlowEVMBridgeConfig.cdc new file mode 100644 index 000000000..01eb72c85 --- /dev/null +++ b/contracts/external/FlowEVMBridgeConfig.cdc @@ -0,0 +1,683 @@ +import EVM from 0xe467b9dd11fa00df +import NonFungibleToken from 0x1d7e57aa55817448 + +import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141 +import FlowEVMBridgeCustomAssociations from 0x1e4aa0b87d10b141 + +/// This contract is used to store configuration information shared by FlowEVMBridge contracts +/// +access(all) +contract FlowEVMBridgeConfig { + + /****************** + Entitlements + *******************/ + + access(all) entitlement Gas + access(all) entitlement Fee + access(all) entitlement Pause + access(all) entitlement Blocklist + + /************* + Fields + **************/ + + /// Amount of FLOW paid to onboard a Type or EVMAddress to the bridge + access(all) + var onboardFee: UFix64 + /// Flat rate fee for all bridge requests + access(all) + var baseFee: UFix64 + /// Default ERC20.decimals() value + access(all) + let defaultDecimals: UInt8 + /// The gas limit for all EVM calls related to bridge operations + access(all) + var gasLimit: UInt64 + /// Flag enabling pausing of bridge operations + access(self) + var paused: Bool + /// Mapping of Type to its associated EVMAddress. The contained struct values also store the operational status of + /// the association, allowing for pausing of operations by Type + access(self) let registeredTypes: {Type: TypeEVMAssociation} + /// Reverse mapping of registeredTypes. Note the EVMAddress is stored as a hex string since the EVMAddress type + /// as of contract development is not a hashable or equatable type and making it so is not supported by Cadence + access(self) + let evmAddressHexToType: {String: Type} + /// Mapping of Type to its associated EVMAddress as relevant to the bridge + access(self) + let typeToTokenHandlers: @{Type: {FlowEVMBridgeHandlerInterfaces.TokenHandler}} + + /******************** + Path Constants + *********************/ + + /// StoragePath where bridge Cadence Owned Account is stored + access(all) + let coaStoragePath: StoragePath + /// StoragePath where bridge config Admin is stored + access(all) + let adminStoragePath: StoragePath + /// PublicPath where a public Capability on the bridge config Admin is exposed + access(all) + let adminPublicPath: PublicPath + /// StoragePath to store the Provider capability used as a bridge fee Provider + access(all) + let providerCapabilityStoragePath: StoragePath + + /************* + Events + **************/ + + /// Emitted whenever the onboarding fee is updated + /// + access(all) + event BridgeFeeUpdated(old: UFix64, new: UFix64, isOnboarding: Bool) + /// Emitted whenever a TokenHandler is configured + /// + access(all) + event HandlerConfigured(targetType: String, targetEVMAddress: String?, isEnabled: Bool) + /// Emitted whenever the bridge is paused or unpaused globally - true for paused, false for unpaused + /// + access(all) + event BridgePauseStatusUpdated(paused: Bool) + /// Emitted whenever a specific asset is paused or unpaused - true for paused, false for unpaused + /// + access(all) + event AssetPauseStatusUpdated(paused: Bool, type: String, evmAddress: String) + /// Emitted whenever an association is updated + /// + access(all) + event AssociationUpdated(type: String, evmAddress: String) + + /************* + Getters + *************/ + + /// Returns whether all bridge operations are currently paused or active + /// + access(all) + view fun isPaused(): Bool { + return self.paused + } + + /// Returns whether operations for a given Type are paused. A return value of nil indicates the Type is not yet + /// onboarded to the bridge. + /// + access(all) + view fun isTypePaused(_ type: Type): Bool? { + // Paused if the type has a token handler & it's disabled, a custom config has been paused or the bridge config has been paused + return !(self.borrowTokenHandler(type)?.isEnabled() ?? true) + || FlowEVMBridgeCustomAssociations.isCustomConfigPaused(forType: type) ?? false + || self.registeredTypes[type]?.isPaused == true + } + + /// Retrieves the EVMAddress associated with a given Type if it has been onboarded to the bridge + /// + access(all) + view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? { + if self.typeHasTokenHandler(type) { + return self.borrowTokenHandler(type)!.getTargetEVMAddress() + } + let customAssociation = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) + return customAssociation ?? self.registeredTypes[type]?.evmAddress + } + + /// Retrieves the type associated with a given EVMAddress if it has been onboarded to the bridge + /// + access(all) + view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { + let evmAddressHex = evmAddress.toString() + let customAssociation = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmAddress) + return customAssociation ?? self.evmAddressHexToType[evmAddressHex] + } + + /// Returns whether the given EVMAddress is currently blocked from onboarding to the bridge + /// + access(all) + view fun isEVMAddressBlocked(_ evmAddress: EVM.EVMAddress): Bool { + return self.borrowEVMBlocklist().isBlocked(evmAddress) + } + + /// Returns whether the given Cadence Type is currently blocked from onboarding to the bridge + /// + access(all) + view fun isCadenceTypeBlocked(_ type: Type): Bool { + return self.borrowCadenceBlocklist().isBlocked(type) + } + + /// Returns the project-defined Type has been registered as a replacement for the originally bridge-defined asset + /// type. This would arise in the event an EVM-native project onboarded to the bridge via permissionless onboarding + /// & later registered their own Cadence NFT contract as associated with their ERC721 per FLIP-318 mechanisms. + /// If there is not a related custom cross-VM Type registered with the bridge, `nil` is returned. + /// + access(all) + view fun getUpdatedCustomCrossVMTypeForLegacyType(_ type: Type): Type? { + if !type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) || type.address! != self.account.address { + // only bridge-defined NFT Types can have an updated custom cross-VM implementation + return nil + } + if let legacyEVMAssoc = self.getLegacyEVMAddressAssociated(with: type) { + // return the new Type associated with the originally associated EVM contract address + return FlowEVMBridgeCustomAssociations.getTypeAssociated(with: legacyEVMAssoc) + } + return nil + } + + /// Returns the bridge-defined Type that was originally associated with the related EVM contract given some + /// externally defined contract. This would arise in the event an EVM-native project onboarded to the bridge via + /// permissionless onboarding & later registered their own Cadence NFT contract as associated with their ERC721 per + /// FLIP-318 mechanisms. If there is not a related bridge-defined Type registered with the bridge, `nil` is returned. + /// + access(all) + view fun getLegacyTypeForCustomCrossVMType(_ type: Type): Type? { + if !type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) || type.address! == self.account.address { + // only externally-defined NFT Types can have an updated custom cross-VM implementation + return nil + } + if let customEVMAssoc = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type ) { + // return the original bridged NFT Type associated with the custom cross-VM EVM contract address + return self.evmAddressHexToType[customEVMAssoc.toString()] + } + return nil + } + + /// Returns the project-defined EVM contract address has been registered as a replacement for the originally bridge- + /// defined asset EVM contract. This would arise in the event an Cadence-native project onboarded to the bridge via + /// permissionless onboarding & later registered their own EVM contract as associated with their Cadence NFT per + /// FLIP-318 mechanisms. If there is not a related custom cross-VM EVM contract registered with the bridge, `nil` is + /// returned. + /// + access(all) + view fun getUpdatedCustomCrossVMEVMAddressForLegacyEVMAddress(_ evmAddress: EVM.EVMAddress): EVM.EVMAddress? { + if let legacyType = self.getLegacyTypeAssociated(with: evmAddress) { + // return the new EVM address associated with the originally associated Type + return FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: legacyType) + } + return nil + } + + /// Returns the bridge-defined EVM contract address that was originally associated with the related Cadence NFT + /// given some externally defined contract. This would arise in the event a Cadence-native project onboarded to the + /// bridge via permissionless onboarding & later registered their own EVM contract as associated with their + /// Cadence NFT per FLIP-318 mechanisms. If there is not a related bridge-defined EVM contract registered with the + /// bridge, `nil` is returned. + /// + access(all) + view fun getLegacyEVMAddressForCustomCrossVMAddress(_ evmAddress: EVM.EVMAddress): EVM.EVMAddress? { + if let customType = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmAddress) { + // return the original bridged NFT Type associated with the custom cross-VM EVM contract address + return self.registeredTypes[customType]?.evmAddress + } + return nil + } + + /**************************** + Bridge Account Methods + ****************************/ + + /// Returns whether the given Type has a TokenHandler configured + /// + access(account) + view fun typeHasTokenHandler(_ type: Type): Bool { + return self.typeToTokenHandlers[type] != nil + } + + /// Returns whether the given EVMAddress has a TokenHandler configured + /// + access(account) + view fun evmAddressHasTokenHandler(_ evmAddress: EVM.EVMAddress): Bool { + let associatedType = self.getTypeAssociated(with: evmAddress) + return associatedType != nil ? self.typeHasTokenHandler(associatedType!) : false + } + + /// Returns the Type associated with the provided EVM contract address if the association was established via + /// the permissionless onboarding path + /// + access(account) + view fun getLegacyTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { + return self.evmAddressHexToType[evmAddress.toString()] ?? nil + } + + /// Returns the EVM contract address associated with the provided Type if the association was established via + /// the permissionless onboarding path + /// + access(account) + view fun getLegacyEVMAddressAssociated(with type: Type): EVM.EVMAddress? { + if self.typeHasTokenHandler(type) { + return self.borrowTokenHandler(type)!.getTargetEVMAddress() + } + return self.registeredTypes[type]?.evmAddress ?? nil + } + + /// Enables bridge contracts to add new associations between types and EVM addresses + /// + access(account) + fun associateType(_ type: Type, with evmAddress: EVM.EVMAddress) { + pre { + self.getEVMAddressAssociated(with: type) == nil: + "Type ".concat(type.identifier).concat(" already associated with an EVMAddress ") + .concat(self.registeredTypes[type]!.evmAddress.toString()) + self.getTypeAssociated(with: evmAddress) == nil: + "EVMAddress ".concat(evmAddress.toString()).concat(" already associated with Type ") + .concat(self.evmAddressHexToType[evmAddress.toString()]!.identifier) + } + self.registeredTypes[type] = TypeEVMAssociation(associated: evmAddress) + let evmAddressHex = evmAddress.toString() + self.evmAddressHexToType[evmAddressHex] = type + + emit AssociationUpdated(type: type.identifier, evmAddress: evmAddressHex) + } + + /// Adds a TokenHandler to the bridge configuration + /// + access(account) + fun addTokenHandler(_ handler: @{FlowEVMBridgeHandlerInterfaces.TokenHandler}) { + pre { + handler.getTargetType() != nil: "Cannot configure Handler without a target Cadence Type set" + self.getEVMAddressAssociated(with: handler.getTargetType()!) == nil: + "Cannot configure Handler for Type that has already been onboarded to the bridge" + self.borrowTokenHandler(handler.getTargetType()!) == nil: + "Cannot configure Handler for Type that already has a Handler configured" + } + let type = handler.getTargetType()! + var targetEVMAddressHex: String? = nil + if let targetEVMAddress = handler.getTargetEVMAddress() { + targetEVMAddressHex = targetEVMAddress.toString() + + let associatedType = self.getTypeAssociated(with: targetEVMAddress) + assert( + associatedType == nil, + message: "Handler target EVMAddress is already associated with a different Type" + ) + self.associateType(type, with: targetEVMAddress) + } + + emit HandlerConfigured( + targetType: type.identifier, + targetEVMAddress: targetEVMAddressHex, + isEnabled: handler.isEnabled() + ) + + self.typeToTokenHandlers[type] <-! handler + } + + /// Returns an unentitled reference to the TokenHandler associated with the given Type + /// + access(account) + view fun borrowTokenHandler( + _ type: Type + ): &{FlowEVMBridgeHandlerInterfaces.TokenHandler}? { + return &self.typeToTokenHandlers[type] + } + + /// Returns an entitled reference to the TokenHandler associated with the given Type + /// + access(self) + view fun borrowTokenHandlerAdmin( + _ type: Type + ): auth(FlowEVMBridgeHandlerInterfaces.Admin) &{FlowEVMBridgeHandlerInterfaces.TokenHandler}? { + return &self.typeToTokenHandlers[type] + } + + /// Returns an entitled reference to the bridge EVMBlocklist + /// + access(self) + view fun borrowEVMBlocklist(): auth(Blocklist) &EVMBlocklist { + return self.account.storage.borrow(from: /storage/evmBlocklist) + ?? panic("Missing or mis-typed EVMBlocklist in storage") + } + + /// Returns an entitled reference to the bridge CadenceBlocklist + /// + access(self) + view fun borrowCadenceBlocklist(): auth(Blocklist) &CadenceBlocklist { + return self.account.storage.borrow(from: /storage/cadenceBlocklist) + ?? panic("Missing or mis-typed CadenceBlocklist in storage") + } + + /// Sets the pause status of a given type, reverting if the type has no associated EVM address as either bridge- + /// defined or registered as a custom cross-VM association + /// + access(self) + fun updatePauseStatus(_ type: Type, pause: Bool) { + var evmAddress = "" + var updated = false + if let customAssoc = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) { + updated = FlowEVMBridgeCustomAssociations.isCustomConfigPaused(forType: type)! != pause + // Called methods no-op internally, so check for status update is skipped here + pause ? FlowEVMBridgeCustomAssociations.pauseCustomConfig(forType: type) + : FlowEVMBridgeCustomAssociations.unpauseCustomConfig(forType: type) + // Assign the EVM address based on the CustomConfig value + evmAddress = customAssoc.toString() + } + if let bridgedAssoc = &FlowEVMBridgeConfig.registeredTypes[type] as &TypeEVMAssociation? { + if evmAddress.length == 0 { + // Assign as bridge association only if custom association does not exist + evmAddress = bridgedAssoc.evmAddress.toString() + } + // No-op if already meets pause status, otherwise update as specified + if (pause && !bridgedAssoc.isPaused) || (!pause && bridgedAssoc.isPaused) { + updated = true + pause ? bridgedAssoc.pause() : bridgedAssoc.unpause() + } + } + assert(evmAddress.length > 0, + message: "There was no association found for type \(type.identifier). To block the type from onboarding, use the CadenceBlocklist.") + if updated { emit AssetPauseStatusUpdated(paused: pause, type: type.identifier, evmAddress: evmAddress) } + } + + /***************** + Constructs + *****************/ + + /// Entry in the registeredTypes mapping, associating a Type with an EVMAddress and its operational status. Since + /// the registeredTypes mapping is indexed on Type, this struct does not additionally store the Type to reduce + /// redundant storage. + /// + access(all) struct TypeEVMAssociation { + /// The EVMAddress associated with the Type + access(all) let evmAddress: EVM.EVMAddress + /// Flag indicating whether operations for the associated Type are paused + access(all) var isPaused: Bool + + init(associated evmAddress: EVM.EVMAddress) { + self.evmAddress = evmAddress + self.isPaused = false + } + + /// Pauses operations for this association + /// + access(contract) fun pause() { + self.isPaused = true + } + + /// Unpauses operations for this association + /// + access(contract) fun unpause() { + self.isPaused = false + } + } + + /// EVMBlocklist resource stores a mapping of EVM addresses that are blocked from onboarding to the bridge + /// + access(all) resource EVMBlocklist { + /// Mapping of serialized EVM addresses to their blocked status + /// + access(all) let blocklist: {String: Bool} + + init() { + self.blocklist = {} + } + + /// Returns whether the given EVM address is blocked from onboarding to the bridge + /// + access(all) view fun isBlocked(_ evmAddress: EVM.EVMAddress): Bool { + return self.blocklist[evmAddress.toString()] ?? false + } + + /// Blocks the given EVM address from onboarding to the bridge + /// + access(Blocklist) fun block(_ evmAddress: EVM.EVMAddress) { + self.blocklist[evmAddress.toString()] = true + } + + /// Removes the given EVM address from the blocklist + /// + access(Blocklist) fun unblock(_ evmAddress: EVM.EVMAddress) { + self.blocklist.remove(key: evmAddress.toString()) + } + } + + /// CadenceBlocklist resource stores a mapping of Cadence Types that are blocked from onboarding to the bridge + /// + access(all) resource CadenceBlocklist { + /// Mapping of serialized Cadence Type to their blocked status + /// + access(all) let blocklist: {Type: Bool} + + init() { + self.blocklist = {} + } + + /// Returns whether the given Type is blocked from onboarding to the bridge + /// + access(all) view fun isBlocked(_ type: Type): Bool { + return self.blocklist[type] ?? false + } + + /// Blocks the given Type from onboarding to the bridge + /// + access(Blocklist) fun block(_ type: Type) { + self.blocklist[type] = true + } + + /// Removes the given type from the blocklist + /// + access(Blocklist) fun unblock(_ type: Type) { + self.blocklist.remove(key: type) + } + } + + /***************** + Config Admin + *****************/ + + /// Admin resource enables updates to the bridge fees + /// + access(all) + resource Admin { + + /// Sets the TokenMinter for the given Type. If a TokenHandler does not exist for the given Type, the operation + /// reverts. The provided minter must be of the expected type for the TokenHandler and the handler cannot have + /// a minter already set. + /// + /// @param targetType: Cadence type indexing the relevant TokenHandler + /// @param minter: TokenMinter minter to set for the TokenHandler + /// + access(all) + fun setTokenHandlerMinter(targetType: Type, minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { + pre { + FlowEVMBridgeConfig.typeHasTokenHandler(targetType): + "Cannot set minter for Type that does not have a TokenHandler configured" + FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) != nil: + "No handler found for target Type" + FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType)!.getExpectedMinterType() == minter.getType(): + "Invalid minter type" + } + FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType)!.setMinter(<-minter) + } + + /// Sets the gas limit for all EVM calls related to bridge operations + /// + /// @param lim the new gas limit + /// + access(Gas) + fun setGasLimit(_ limit: UInt64) { + FlowEVMBridgeConfig.gasLimit = limit + } + + /// Updates the onboarding fee + /// + /// @param new: UFix64 - new onboarding fee + /// + /// @emits BridgeFeeUpdated with the old and new rates and isOnboarding set to true + /// + access(Fee) + fun updateOnboardingFee(_ new: UFix64) { + emit BridgeFeeUpdated(old: FlowEVMBridgeConfig.onboardFee, new: new, isOnboarding: true) + FlowEVMBridgeConfig.onboardFee = new + } + + /// Updates the base fee + /// + /// @param new: UFix64 - new base fee + /// + /// @emits BridgeFeeUpdated with the old and new rates and isOnboarding set to false + /// + access(Fee) + fun updateBaseFee(_ new: UFix64) { + emit BridgeFeeUpdated(old: FlowEVMBridgeConfig.baseFee, new: new, isOnboarding: false) + FlowEVMBridgeConfig.baseFee = new + } + + /// Pauses the bridge, preventing all bridge operations + /// + /// @emits BridgePauseStatusUpdated with true + /// + access(Pause) + fun pauseBridge() { + if FlowEVMBridgeConfig.isPaused() { + return + } + FlowEVMBridgeConfig.paused = true + emit BridgePauseStatusUpdated(paused: true) + } + + /// Unpauses the bridge, allowing bridge operations to resume + /// + /// @emits BridgePauseStatusUpdated with true + /// + access(Pause) + fun unpauseBridge() { + if !FlowEVMBridgeConfig.isPaused() { + return + } + FlowEVMBridgeConfig.paused = false + emit BridgePauseStatusUpdated(paused: false) + } + + /// Pauses all operations for a given asset type + /// + /// @param type: The Type for which to pause bridge operations + /// + /// @emits AssetPauseStatusUpdated with the pause status and serialized type & associated EVM address + /// + access(Pause) + fun pauseType(_ type: Type) { + pre { + FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) != nil || FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) != nil: + "Could not find a bridged or custom association for type \(type.identifier) - cannot pause a type without an association" + } + FlowEVMBridgeConfig.updatePauseStatus(type, pause: true) + } + + /// Unpauses all operations for a given asset type + /// + /// @param type: The Type for which to unpause bridge operations + /// + /// @emits AssetPauseStatusUpdated with the pause status and serialized type & associated EVM address + /// + access(Pause) + fun unpauseType(_ type: Type) { + pre { + FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) != nil || FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) != nil: + "Could not find a bridged or custom association for type \(type.identifier) - cannot unpause a type without an association" + } + FlowEVMBridgeConfig.updatePauseStatus(type, pause: false) + } + + /// Sets the target EVM contract address on the handler for a given Type, associating the Cadence type with the + /// provided EVM address. If a TokenHandler does not exist for the given Type, the operation reverts. + /// + /// @param targetType: Cadence type to associate with the target EVM address + /// @param targetEVMAddress: target EVM address to associate with the Cadence type + /// + /// @emits HandlerConfigured with the target Type, target EVM address, and whether the handler is enabled + /// + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setHandlerTargetEVMAddress(targetType: Type, targetEVMAddress: EVM.EVMAddress) { + pre { + FlowEVMBridgeConfig.getEVMAddressAssociated(with: targetType) == nil: + "Type already associated with an EVM Address" + FlowEVMBridgeConfig.getTypeAssociated(with: targetEVMAddress) == nil: + "EVM Address already associated with another Type" + } + post { + FlowEVMBridgeConfig.getEVMAddressAssociated(with: targetType)!.equals(targetEVMAddress): + "Problem associating target Type and target EVM Address" + } + FlowEVMBridgeConfig.associateType(targetType, with: targetEVMAddress) + + let handler = FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) + ?? panic("No handler found for target Type") + handler.setTargetEVMAddress(targetEVMAddress) + + emit HandlerConfigured( + targetType: targetType.identifier, + targetEVMAddress: targetEVMAddress.toString(), + isEnabled: handler.isEnabled() + ) + } + + /// Enables the TokenHandler for the given Type. If a TokenHandler does not exist for the given Type, the + /// operation reverts. + /// + /// @param targetType: Cadence type indexing the relevant TokenHandler + /// + /// @emits HandlerConfigured with the target Type, target EVM address, and whether the handler is enabled + /// + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun enableHandler(targetType: Type) { + let handler = FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) + ?? panic("No handler found for target Type ".concat(targetType.identifier)) + handler.enableBridging() + + let targetEVMAddressHex = handler.getTargetEVMAddress()?.toString() + ?? panic("Handler cannot be enabled without a target EVM Address") + + emit HandlerConfigured( + targetType: handler.getTargetType()!.identifier, + targetEVMAddress: targetEVMAddressHex, + isEnabled: handler.isEnabled() + ) + } + + /// Disables the TokenHandler for the given Type. If a TokenHandler does not exist for the given Type, the + /// operation reverts. + /// + /// @param targetType: Cadence type indexing the relevant TokenHandler + /// + /// @emits HandlerConfigured with the target Type, target EVM address, and whether the handler is enabled + /// + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun disableHandler(targetType: Type) { + let handler = FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) + ?? panic("No handler found for target Type".concat(targetType.identifier)) + handler.disableBridging() + + emit HandlerConfigured( + targetType: handler.getTargetType()!.identifier, + targetEVMAddress: handler.getTargetEVMAddress()?.toString(), + isEnabled: handler.isEnabled() + ) + } + } + + init() { + self.onboardFee = 0.0 + self.baseFee = 0.0 + self.defaultDecimals = 18 + self.gasLimit = 15_000_000 + self.paused = true + + self.registeredTypes = {} + self.evmAddressHexToType = {} + + self.typeToTokenHandlers <- {} + + self.adminStoragePath = /storage/flowEVMBridgeConfigAdmin + self.adminPublicPath = /public/flowEVMBridgeConfigAdmin + self.coaStoragePath = /storage/evm + self.providerCapabilityStoragePath = /storage/bridgeFlowVaultProvider + + // Create & save Admin, issuing a public unentitled Admin Capability + self.account.storage.save(<-create Admin(), to: self.adminStoragePath) + let adminCap = self.account.capabilities.storage.issue<&Admin>(self.adminStoragePath) + self.account.capabilities.publish(adminCap, at: self.adminPublicPath) + + // Initialize the blocklists + self.account.storage.save(<-create EVMBlocklist(), to: /storage/evmBlocklist) + self.account.storage.save(<-create CadenceBlocklist(), to: /storage/cadenceBlocklist) + } +} diff --git a/contracts/external/FlowEVMBridgeCustomAssociations.cdc b/contracts/external/FlowEVMBridgeCustomAssociations.cdc new file mode 100644 index 000000000..9f735b1b3 --- /dev/null +++ b/contracts/external/FlowEVMBridgeCustomAssociations.cdc @@ -0,0 +1,212 @@ +import NonFungibleToken from 0x1d7e57aa55817448 +import CrossVMMetadataViews from 0x1d7e57aa55817448 +import EVM from 0xe467b9dd11fa00df + +import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141 + +/// The FlowEVMBridgeCustomAssociations is tasked with preserving custom associations between Cadence assets and their +/// EVM implementations. These associations should be validated before `saveCustomAssociation` is called by +/// leveraging the interfaces outlined in FLIP-318 (https://github.com/onflow/flips/issues/318) to ensure that the +/// declared association is valid and that neither implementation is bridge-defined. +/// +access(all) contract FlowEVMBridgeCustomAssociations { + + /// Stored associations indexed by Cadence Type + access(self) let associationsConfig: @{Type: {FlowEVMBridgeCustomAssociationTypes.CustomConfig}} + /// Reverse lookup indexed on serialized EVM contract address + access(self) let associationsByEVMAddress: {String: Type} + + /// Event emitted whenever a custom association is established + access(all) event CustomAssociationEstablished( + type: String, + evmContractAddress: String, + nativeVMRawValue: UInt8, + updatedFromBridged: Bool, + fulfillmentMinterType: String?, + fulfillmentMinterOrigin: Address?, + fulfillmentMinterCapID: UInt64?, + fulfillmentMinterUUID: UInt64?, + configUUID: UInt64 + ) + + /// Retrieves the EVM address associated with the given Cadence Type if it has been registered as a cross-VM asset + /// + /// @param with: The Cadence Type to query against + /// + /// @return The EVM address configured as associated with the provided Cadence Type + /// + access(all) + view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? { + return self.associationsConfig[type]?.getEVMContractAddress() ?? nil + } + + /// Retrieves the Cadence Type associated with the given EVM address if it has been registered as a cross-VM asset + /// + /// @param with: The EVM contract address to query against + /// + /// @return The Cadence Type configured as associated with the provided EVM address + /// + access(all) + view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { + return self.associationsByEVMAddress[evmAddress.toString()] + } + + /// Returns an EVMPointer containing the data at the time of registration + /// + /// @param forType: The Cadence Type to query against + /// + /// @return a copy of the EVMPointer view as registered with the bridge + /// + access(all) + fun getEVMPointerAsRegistered(forType: Type): CrossVMMetadataViews.EVMPointer? { + if let config = &self.associationsConfig[forType] as &{FlowEVMBridgeCustomAssociationTypes.CustomConfig}? { + return CrossVMMetadataViews.EVMPointer( + cadenceType: config.getCadenceType(), + cadenceContractAddress: config.getCadenceType().address!, + evmContractAddress: config.getEVMContractAddress(), + nativeVM: config.getNativeVM() + ) + } + return nil + } + + /// Returns whether the related CustomConfig is currently paused or not. `nil` is returned if a CustomConfig is not + /// found for the given Type + /// + /// @param forType: The Cadence Type for which to retrieve a registered CustomConfig + /// + /// @return true if the CustomConfig is paused, false if registered and unpaused, nil if unregistered as a custom + /// association + /// + access(all) + view fun isCustomConfigPaused(forType: Type): Bool? { + return self.borrowNFTCustomConfig(forType: forType)?.isPaused() ?? nil + } + + /// Returns metadata about a registered CustomConfig + /// + /// @param forType: The Cadence Type of the registered cross-VM asset + /// + /// @return The CustomConfigInfo struct if the type is registered, nil otherwise + /// + access(all) + fun getCustomConfigInfo(forType: Type): FlowEVMBridgeCustomAssociationTypes.CustomConfigInfo? { + if let config = self.borrowNFTCustomConfig(forType: forType) { + let fulfillmentMinterType = config.checkFulfillmentMinter() == true ? config.borrowFulfillmentMinter().getType() : nil + return FlowEVMBridgeCustomAssociationTypes.CustomConfigInfo( + updatedFromBridged: config.isUpdatedFromBridged(), + isPaused: config.isPaused(), + fulfillmentMinterType: fulfillmentMinterType, + evmPointer: self.getEVMPointerAsRegistered(forType: forType)! + ) + } + return nil + } + + /// Allows the bridge contracts to preserve a custom association. Will revert if a custom association already exists + /// + /// @param type: The Cadence Type of the associated asset. + /// @param evmContractAddress: The EVM address defining the EVM implementation of the associated asset. + /// @param nativeVM: The VM in which the asset is distributed by the project. The bridge will mint/escrow in the non-native + /// VM environment. + /// @param updatedFromBridged: Whether the asset was originally onboarded to the bridge via permissionless + /// onboarding. In other words, whether there was first a bridge-defined implementation of the underlying asset. + /// @param fulfillmentMinter: An authorized Capability allowing the bridge to fulfill bridge requests moving the + /// underlying asset from EVM. Required if the asset is EVM-native. + /// + access(account) + fun saveCustomAssociation( + type: Type, + evmContractAddress: EVM.EVMAddress, + nativeVM: CrossVMMetadataViews.VM, + updatedFromBridged: Bool, + fulfillmentMinter: Capability? + ) { + pre { + self.associationsConfig[type] == nil: + "Type \(type.identifier) already has a custom association with \(self.borrowNFTCustomConfig(forType: type)!.getEVMContractAddress().toString())" + type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): + "Only NFT cross-VM associations are currently supported but \(type.identifier) is not an NFT implementation" + self.associationsByEVMAddress[evmContractAddress.toString()] == nil: + "EVM Address \(evmContractAddress.toString()) already has a custom association with \(self.borrowNFTCustomConfig(forType: type)!.getCadenceType().identifier)" + fulfillmentMinter?.check() ?? true: + "The NFTFulfillmentMinter Capability issued from \(fulfillmentMinter!.address.toString()) is invalid. Ensure the Capability is properly issued and active." + } + let config <- FlowEVMBridgeCustomAssociationTypes.createNFTCustomConfig( + type: type, + evmContractAddress: evmContractAddress, + nativeVM: nativeVM, + updatedFromBridged: updatedFromBridged, + fulfillmentMinter: fulfillmentMinter + ) + emit CustomAssociationEstablished( + type: type.identifier, + evmContractAddress: evmContractAddress.toString(), + nativeVMRawValue: nativeVM.rawValue, + updatedFromBridged: updatedFromBridged, + fulfillmentMinterType: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().identifier : nil, + fulfillmentMinterOrigin: fulfillmentMinter?.address ?? nil, + fulfillmentMinterCapID: fulfillmentMinter?.id ?? nil, + fulfillmentMinterUUID: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.uuid : nil, + configUUID: config.uuid + ) + self.associationsByEVMAddress[config.evmContractAddress.toString()] = type + self.associationsConfig[type] <-! config + } + + /// Allows bridge contracts to fulfill NFT bridging requests for EVM-native NFTs, using the provided + /// NFTFulfillmentMinter Capability provided by the project on cross-VM registration to mint a new NFT. + /// **NOTE:** Given the bridge's mint/escrow pattern for the non-native VM, any calls should first check that the + /// requested NFT is not locked in escrow before minting. + /// + /// @param forType: The Cadence Type of the NFT being fulfilled + /// @param id: The ERC721 ID of the requested NFT + /// + /// @param + access(account) + fun fulfillNFTFromEVM(forType: Type, id: UInt256): @{NonFungibleToken.NFT} { + post { + result.getType() == forType: + "Requested \(forType.identifier) but got \(result.getType().identifier) on fulfillment from EVM" + } + let config = self.borrowNFTCustomConfig(forType: forType) + ?? panic("No CustomConfig found for type \(forType.identifier) - cannot fulfill NFT \(id) from EVM") + let minter = config.borrowFulfillmentMinter() + return <- minter.fulfillFromEVM(id: id) + } + + /// Sets the associated CustomConfig as paused, preventing bridging operations on the associated implementations. + /// Expect a no-op in the event the CustomConfig is already paused + /// + access(account) fun pauseCustomConfig(forType: Type) { + let config = self.borrowNFTCustomConfig(forType: forType) + ?? panic("No CustomConfig found for type \(forType.identifier) - cannot pause config that does not exist") + if !config.isPaused() { + config.setPauseStatus(true) + } + } + + /// Sets the associated CustomConfig as unpaused, preventing bridging operations on the associated implementations. + /// Expect a no-op in the event the CustomConfig is already paused + /// + access(account) fun unpauseCustomConfig(forType: Type) { + let config = self.borrowNFTCustomConfig(forType: forType) + ?? panic("No CustomConfig found for type \(forType.identifier) - cannot unpause config that does not exist") + if config.isPaused() { + config.setPauseStatus(false) + } + } + + /// Returns a reference to the NFTCustomConfig if it exists, nil otherwise + /// + access(self) view fun borrowNFTCustomConfig(forType: Type): &FlowEVMBridgeCustomAssociationTypes.NFTCustomConfig? { + let config = &self.associationsConfig[forType] as &{FlowEVMBridgeCustomAssociationTypes.CustomConfig}? + return config as? &FlowEVMBridgeCustomAssociationTypes.NFTCustomConfig + } + + + init() { + self.associationsConfig <- {} + self.associationsByEVMAddress = {} + } +} diff --git a/contracts/external/FlowEVMBridgeHandlerInterfaces.cdc b/contracts/external/FlowEVMBridgeHandlerInterfaces.cdc new file mode 100644 index 000000000..6c8c9dee0 --- /dev/null +++ b/contracts/external/FlowEVMBridgeHandlerInterfaces.cdc @@ -0,0 +1,207 @@ +import FungibleToken from 0xf233dcee88fe0abe +import NonFungibleToken from 0x1d7e57aa55817448 + +import EVM from 0xe467b9dd11fa00df + +/// FlowEVMBridgeHandlerInterfaces +/// +/// This contract defines the interfaces for the FlowEVM Bridge Handlers. These Handlers are intended to encapsulate +/// the logic for bridging edge case assets between Cadence and EVM and require configuration by the bridge account to +/// enable. Contracts implementing these resources should be deployed to the bridge account so that privileged methods, +/// particularly those related to fulfilling bridge requests remain in the closed loop of bridge contract logic and +/// defined assets in the custody of the bridge account. +/// +access(all) contract FlowEVMBridgeHandlerInterfaces { + + /****************** + Entitlements + *******************/ + + /// Entitlement related to administrative setters + access(all) entitlement Admin + /// Entitlement related to minting handled assets + access(all) entitlement Mint + + /************* + Events + **************/ + + /// Event emitted when a handler is enabled between a Cadence type and an EVM address + access(all) event HandlerEnabled( + handlerType: String, + handlerUUID: UInt64, + targetType: String, + targetEVMAddress: String + ) + /// Event emitted when a handler is disabled, pausing bridging between VMs + access(all) event HandlerDisabled( + handlerType: String, + handlerUUID: UInt64, + targetType: String?, + targetEVMAddress: String? + ) + /// Emitted when a minter resource is set in a handler + access(all) event MinterSet(handlerType: String, + handlerUUID: UInt64, + targetType: String?, + targetEVMAddress: String?, + minterType: String, + minterUUID: UInt64 + ) + + /**************** + Constructs + *****************/ + + /// Non-privileged interface for querying handler information + /// + access(all) resource interface HandlerInfo { + /// Returns whether the Handler is enabled + access(all) view fun isEnabled(): Bool + /// Returns the Cadence type handled by the Handler, nil if not set + access(all) view fun getTargetType(): Type? + /// Returns the EVM address handled by the Handler, nil if not set + access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? + /// Returns the Type of the expected minter if the handler utilizes one + access(all) view fun getExpectedMinterType(): Type? + } + + /// Administrative interface for Handler configuration + /// + access(all) resource interface HandlerAdmin : HandlerInfo { + /// Sets the target Cadence Type handled by this resource. Once the targe type is set - whether by this method + /// or on initialization - this setter will fail. + access(Admin) fun setTargetType(_ type: Type) { + pre { + self.getTargetType() == nil: "Target Type has already been set" + } + post { + self.getTargetType()! == type: "Problem setting target type" + } + } + /// Sets the target EVM address handled by this resource + access(Admin) fun setTargetEVMAddress(_ address: EVM.EVMAddress) { + pre { + self.getTargetEVMAddress() == nil: "Target EVM address has already been set" + } + post { + self.getTargetEVMAddress()!.equals(address!): "Problem setting target EVM address" + } + } + access(Admin) fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { + pre { + self.getExpectedMinterType() == minter.getType(): "Minter is not of the expected type" + minter.getMintedType() == self.getTargetType(): "Minter does not mint the target type" + emit MinterSet( + handlerType: self.getType().identifier, + handlerUUID: self.uuid, + targetType: self.getTargetType()?.identifier, + targetEVMAddress: self.getTargetEVMAddress()?.toString(), + minterType: minter.getType().identifier, + minterUUID: minter.uuid + ) + } + } + /// Enables the Handler to fulfill bridge requests for the configured targets. If implementers utilize a minter, + /// they should additionally ensure the minter is set before enabling. + access(Admin) fun enableBridging() { + pre { + self.getTargetType() != nil && self.getTargetEVMAddress() != nil: + "Cannot enable before setting bridge target Type and EVM Address" + !self.isEnabled(): "Handler has already been enabled" + } + post { + self.isEnabled(): "Problem enabling Handler" + emit HandlerEnabled( + handlerType: self.getType().identifier, + handlerUUID: self.uuid, + targetType: self.getTargetType()!.identifier, + targetEVMAddress: self.getTargetEVMAddress()!.toString() + ) + } + } + + /// Disables the Handler from fulfilling bridge requests. + access(Admin) fun disableBridging() { + pre { + self.isEnabled(): + "Cannot disable: ".concat(self.getType().identifier).concat(" is already disabled") + } + post { + !self.isEnabled(): + "Problem disabling ".concat(self.getType().identifier) + emit HandlerDisabled( + handlerType: self.getType().identifier, + handlerUUID: self.uuid, + targetType: self.getTargetType()?.identifier, + targetEVMAddress: self.getTargetEVMAddress()?.toString() + ) + } + } + } + + /// Minter interface for configurations requiring the minting of Cadence fungible tokens + /// + access(all) resource interface TokenMinter { + /// Returns the Cadence type minted by this resource + access(all) view fun getMintedType(): Type + /// Mints the specified amount of tokens + access(Mint) fun mint(amount: UFix64): @{FungibleToken.Vault} { + pre { + amount > 0.0: "Attempting to mint 0.0 - Amount minted must be greater than 0" + } + post { + result.getType() == self.getMintedType(): + "TokenMinter ".concat(self.getType().identifier).concat(" with uuid ").concat(self.uuid.toString()) + .concat(" expected to mint ").concat(self.getMintedType().identifier) + .concat(" but returned ").concat(result.getType().identifier) + result.balance == amount: + "Minted amount ".concat(result.balance.toString()) + .concat(" does not match requested amount ").concat(amount.toString()) + } + } + } + + /// Handler interface for bridging FungibleToken assets. Implementations should be stored within the bridge account + /// and called be the bridge contract for bridging operations on the Handler's target Type and EVM contract. + /// + access(all) resource interface TokenHandler : HandlerAdmin { + /// Fulfills a request to bridge tokens from the Cadence side to the EVM side + access(account) fun fulfillTokensToEVM( + tokens: @{FungibleToken.Vault}, + to: EVM.EVMAddress + ) { + pre { + self.isEnabled(): + "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ") + .concat(self.uuid.toString()).concat(" is not yet enabled") + tokens.getType() == self.getTargetType(): + "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ").concat(self.uuid.toString()) + .concat(" expects ").concat(self.getTargetType()?.identifier ?? "nil") + .concat(" but received ").concat(tokens.getType().identifier) + tokens.balance > 0.0: + "Attempting to bridge 0.0 tokens - zero amounts are unsupported" + } + } + /// Fulfills a request to bridge tokens from the EVM side to the Cadence side + access(account) fun fulfillTokensFromEVM( + owner: EVM.EVMAddress, + type: Type, + amount: UInt256, + protectedTransferCall: fun (): EVM.Result + ): @{FungibleToken.Vault} { + pre { + self.isEnabled(): + "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ") + .concat(self.uuid.toString()).concat(" is not yet enabled") + amount > UInt256(0): "Attempting to bridge 0 tokens from EVM - zero amounts are unsupported" + } + post { + result.getType() == self.getTargetType(): + "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ").concat(self.uuid.toString()) + .concat(" expected to return ").concat(self.getTargetType()?.identifier ?? "nil") + .concat(" but returned ").concat(result.getType().identifier) + } + } + } +} diff --git a/contracts/external/FlowEVMBridgeHandlers.cdc b/contracts/external/FlowEVMBridgeHandlers.cdc new file mode 100644 index 000000000..fb9225466 --- /dev/null +++ b/contracts/external/FlowEVMBridgeHandlers.cdc @@ -0,0 +1,468 @@ +import Burner from 0xf233dcee88fe0abe +import FungibleToken from 0xf233dcee88fe0abe +import NonFungibleToken from 0x1d7e57aa55817448 +import FlowToken from 0x1654653399040a61 + +import EVM from 0xe467b9dd11fa00df + +import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141 +import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 +import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141 + +/// FlowEVMBridgeHandlers +/// +/// This contract is responsible for defining and configuring bridge handlers for special cased assets. +/// +access(all) contract FlowEVMBridgeHandlers { + + /********************** + Contract Fields + ***********************/ + + /// The storage path for the HandlerConfigurator resource + access(all) let ConfiguratorStoragePath: StoragePath + + /**************** + Constructs + *****************/ + + /// Handler for bridging Cadence native fungible tokens to EVM. In the event a Cadence project migrates native + /// support to EVM, this Hander can be configured to facilitate bridging the Cadence tokens to EVM. This Handler + /// then effectively allows the bridge to treat such tokens as bridge-defined on the Cadence side and EVM-native on + /// the EVM side minting/burning in Cadence and escrowing in EVM. + /// In order for this to occur, neither the Cadence token nor the EVM contract can be onboarded to the bridge - in + /// essence, neither side of the asset can be onboarded to the bridge. + /// The Handler must be configured in the bridge via the HandlerConfigurator. Once added, the bridge will filter + /// requests to bridge the token Vault to EVM through this Handler which cannot be enabled until a target EVM + /// address is set. Once the corresponding EVM contract address is known, it can be set and the Handler. It's also + /// suggested that the Handler only be enabled once sufficient liquidity has been arranged in bridge escrow on the + /// EVM side. + /// + access(all) resource CadenceNativeTokenHandler : FlowEVMBridgeHandlerInterfaces.TokenHandler { + /// Flag determining if request handling is enabled + access(self) var enabled: Bool + /// The Cadence Type this handler fulfills requests for + access(self) var targetType: Type + /// The EVM contract address this handler fulfills requests for. This field is optional in the event the EVM + /// contract address is not yet known but the Cadence type must still be filtered via Handler to prevent the + /// type from being onboarded otherwise. + access(self) var targetEVMAddress: EVM.EVMAddress? + /// The expected minter type for minting tokens on fulfillment + access(self) let expectedMinterType: Type + /// The Minter enabling minting of Cadence tokens on fulfillment from EVM + access(self) var minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}? + + init(targetType: Type, targetEVMAddress: EVM.EVMAddress?, expectedMinterType: Type) { + pre { + expectedMinterType.isSubtype(of: Type<@{FlowEVMBridgeHandlerInterfaces.TokenMinter}>()): + "Invalid minter type" + } + self.enabled = false + self.targetType = targetType + self.targetEVMAddress = targetEVMAddress + self.expectedMinterType = expectedMinterType + self.minter <- nil + } + + /* --- HandlerInfo --- */ + + /// Returns the enabled status of the handler + access(all) view fun isEnabled(): Bool { + return self.enabled + } + + /// Returns the type of the asset the handler is configured to handle + access(all) view fun getTargetType(): Type? { + return self.targetType + } + + /// Returns the EVM contract address the handler is configured to handle + access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? { + return self.targetEVMAddress + } + + /// Returns the expected minter type for the handler + access(all) view fun getExpectedMinterType(): Type? { + return self.expectedMinterType + } + + /* --- TokenHandler --- */ + + /// Fulfill a request to bridge tokens from Cadence to EVM, burning the provided Vault and transferring from + /// EVM escrow to the named recipient. Assumes any fees are handled by the caller within the bridge contracts + /// + /// @param tokens: The Vault containing the tokens to bridge + /// @param to: The EVM address to transfer the tokens to + /// + access(account) + fun fulfillTokensToEVM( + tokens: @{FungibleToken.Vault}, + to: EVM.EVMAddress + ) { + let evmAddress = self.getTargetEVMAddress()! + + // Get values from vault and burn + let amount = tokens.balance + let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: evmAddress) + + assert(uintAmount > UInt256(0), message: "Amount to bridge must be greater than 0") + + Burner.burn(<-tokens) + + FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: uintAmount, erc20Address: evmAddress) + } + + /// Fulfill a request to bridge tokens from EVM to Cadence, minting the provided amount of tokens in Cadence + /// and transferring from the named owner to bridge escrow in EVM. + /// + /// @param owner: The EVM address of the owner of the tokens. Should also be the caller executing the protected + /// transfer call. + /// @param type: The type of the asset being bridged + /// @param amount: The amount of tokens to bridge + /// + /// @return The minted Vault containing the the requested amount of Cadence tokens + /// + access(account) + fun fulfillTokensFromEVM( + owner: EVM.EVMAddress, + type: Type, + amount: UInt256, + protectedTransferCall: fun (): EVM.Result + ): @{FungibleToken.Vault} { + let evmAddress = self.getTargetEVMAddress()! + + // Convert the amount to a UFix64 + let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount( + amount, + erc20Address: evmAddress + ) + assert(ufixAmount > 0.0, message: "Amount to bridge must be greater than 0") + + FlowEVMBridgeUtils.mustEscrowERC20( + owner: owner, + amount: amount, + erc20Address: evmAddress, + protectedTransferCall: protectedTransferCall + ) + + // After state confirmation, mint the tokens and return + let minter = self.borrowMinter() + ?? panic("Cannot bridge - Minter not set in ".concat(self.getType().identifier)) + let minted <- minter.mint(amount: ufixAmount) + return <-minted + } + + /* --- Admin --- */ + + /// Sets the target type for the handler + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setTargetType(_ type: Type) { + self.targetType = type + } + + /// Sets the target EVM address for the handler + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setTargetEVMAddress(_ address: EVM.EVMAddress) { + self.targetEVMAddress = address + } + + /// Sets the target type for the handler + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { + pre { + self.minter == nil: "Minter has already been set in ".concat(self.getType().identifier) + } + self.minter <-! minter + } + + /// Enables the handler for request handling. + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun enableBridging() { + pre { + self.minter != nil: "Cannot enable ".concat(self.getType().identifier).concat(" without a minter") + } + self.enabled = true + } + + /// Disables the handler for request handling. + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun disableBridging() { + self.enabled = false + } + + /* --- Internal --- */ + + /// Returns an entitled reference to the encapsulated minter resource + access(self) + view fun borrowMinter(): auth(FlowEVMBridgeHandlerInterfaces.Mint) &{FlowEVMBridgeHandlerInterfaces.TokenMinter}? { + return &self.minter + } + } + + /// Facilitates moving Flow between Cadence and EVM as WFLOW. Since WFLOW is an artifact of the EVM ecosystem, + /// wrapping the native token as an ERC20, it does not have a place in Cadence's fungible token ecosystem. + /// Given the native interface on EVM.CadenceOwnedAccount and EVM.EVMAddress to move FLOW between Cadence and EVM, + /// this handler treats requests to bridge FLOW as WFLOW as a special case. + /// + access(all) resource WFLOWTokenHandler : FlowEVMBridgeHandlerInterfaces.TokenHandler { + /// Flag determining if request handling is enabled + access(self) var enabled: Bool + /// The Cadence Type this handler fulfills requests for + access(self) var targetType: Type + /// The EVM contract address this handler fulfills requests for + access(self) var targetEVMAddress: EVM.EVMAddress + + init(wflowEVMAddress: EVM.EVMAddress) { + self.enabled = false + self.targetType = Type<@FlowToken.Vault>() + self.targetEVMAddress = wflowEVMAddress + } + + /// Returns whether the Handler is enabled + access(all) view fun isEnabled(): Bool { + return self.enabled + } + /// Returns the Cadence type handled by the Handler, nil if not set + access(all) view fun getTargetType(): Type? { + return self.targetType + } + /// Returns the EVM address handled by the Handler, nil if not set + access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? { + return self.targetEVMAddress + } + /// Returns nil as this handler simply unwraps WFLOW to FLOW + access(all) view fun getExpectedMinterType(): Type? { + return nil + } + + /* --- TokenHandler --- */ + + /// Fulfill a request to bridge tokens from Cadence to EVM, burning the provided Vault and transferring from + /// EVM escrow to the named recipient. Assumes any fees are handled by the caller within the bridge contracts + /// + /// @param tokens: The Vault containing the tokens to bridge + /// @param to: The EVM address to transfer the tokens to + /// + access(account) + fun fulfillTokensToEVM( + tokens: @{FungibleToken.Vault}, + to: EVM.EVMAddress + ) { + let flowVault <- tokens as! @FlowToken.Vault + let wflowAddress = self.getTargetEVMAddress()! + + // Get balance from vault + let balance = flowVault.balance + let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(balance, erc20Address: wflowAddress) + + // Deposit to bridge COA + let coa = FlowEVMBridgeUtils.borrowCOA() + coa.deposit(from: <-flowVault) + + let preBalance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: wflowAddress) + + // Wrap the deposited FLOW as WFLOW, giving the bridge COA the necessary WFLOW to transfer + let wrapResult = FlowEVMBridgeUtils.call( + signature: "deposit()", + targetEVMAddress: wflowAddress, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: balance + ) + assert(wrapResult.status == EVM.Status.successful, message: "Failed to wrap FLOW as WFLOW") + + let postBalance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: wflowAddress) + + // Cover underflow + assert( + postBalance > preBalance, + message: "Escrowed WFLOW balance did not increment after wrapping FLOW - pre: " + .concat(preBalance.toString()).concat(" | post: ").concat(postBalance.toString()) + ) + // Confirm bridge COA's WFLOW balance has incremented by the expected amount + assert( + postBalance - preBalance == uintAmount, + message: "Escrowed WFLOW balance after wrapping does not match requested amount - expected: " + .concat((preBalance + uintAmount).toString()) + .concat(" | actual: ") + .concat((postBalance - preBalance).toString()) + ) + + // Transfer WFLOW to recipient + FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: uintAmount, erc20Address: wflowAddress) + } + + /// Fulfill a request to bridge tokens from EVM to Cadence, minting the provided amount of tokens in Cadence + /// and transferring from the named owner to bridge escrow in EVM. + /// + /// @param owner: The EVM address of the owner of the tokens. Should also be the caller executing the protected + /// transfer call. + /// @param type: The type of the asset being bridged + /// @param amount: The amount of tokens to bridge + /// + /// @return The minted Vault containing the the requested amount of Cadence tokens + /// + access(account) + fun fulfillTokensFromEVM( + owner: EVM.EVMAddress, + type: Type, + amount: UInt256, + protectedTransferCall: fun (): EVM.Result + ): @{FungibleToken.Vault} { + let wflowAddress = self.getTargetEVMAddress()! + + // Convert the amount to a UFix64 + let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount( + amount, + erc20Address: wflowAddress + ) + assert( + ufixAmount > 0.0, + message: "Requested UInt256 amount ".concat(amount.toString()).concat(" converted to 0.0 ") + .concat(" - try bridging a larger amount to avoid UFix64 precision loss during conversion") + ) + + // Transfers WFLOW to bridge COA as escrow + FlowEVMBridgeUtils.mustEscrowERC20( + owner: owner, + amount: amount, + erc20Address: wflowAddress, + protectedTransferCall: protectedTransferCall + ) + + // Get the bridge COA's FLOW balance before unwrapping WFLOW + let coa = FlowEVMBridgeUtils.borrowCOA() + let preBalance = coa.balance().attoflow + + // Unwrap the transferred WFLOW to FLOW, giving the bridge COA the necessary FLOW to withdraw from EVM + let unwrapResult = FlowEVMBridgeUtils.call( + signature: "withdraw(uint256)", + targetEVMAddress: wflowAddress, + args: [amount], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(unwrapResult.status == EVM.Status.successful, message: "Failed to unwrap WFLOW as FLOW") + + let postBalance = coa.balance().attoflow + + // Cover underflow + assert( + postBalance > preBalance, + message: "Escrowed FLOW Balance did not increment after unwrapping WFLOW - pre: ".concat(preBalance.toString()) + .concat(" | post: ").concat(postBalance.toString()) + ) + // Confirm bridge COA's FLOW balance has incremented by the expected amount + assert( + UInt256(postBalance - preBalance) == amount, + message: "Escrowed WFLOW balance after unwrapping does not match requested amount - expected: " + .concat((UInt256(preBalance) + amount).toString()) + .concat(" | actual: ") + .concat((postBalance - preBalance).toString()) + ) + + // Withdraw escrowed FLOW from bridge COA + let withdrawBalance = EVM.Balance(attoflow: UInt(amount)) + assert( + UInt256(withdrawBalance.attoflow) == amount, + message: "Requested balance failed to convert to attoflow - expected: " + .concat(amount.toString()) + .concat(" | actual: ") + .concat(withdrawBalance.attoflow.toString()) + ) + let flowVault <- coa.withdraw(balance: withdrawBalance) + assert( + flowVault.balance == ufixAmount, + message: "Resulting FLOW Vault balance does not match requested amount - expected: " + .concat(ufixAmount.toString()) + .concat(" | actual: ") + .concat(flowVault.balance.toString()) + ) + return <-flowVault + } + + /* --- HandlerAdmin --- */ + // Conforms to HandlerAdmin for enableBridging, but most of the methods are unnecessary given the strict + // association between FLOW and WFLOW + + /// Sets the target type for the handler + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setTargetType(_ type: Type) { + panic("WFLOWTokenHandler has targetType set to " + .concat(self.targetType.identifier).concat(" at initialization")) + } + + /// Sets the target EVM address for the handler + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setTargetEVMAddress(_ address: EVM.EVMAddress) { + panic("WFLOWTokenHandler has EVMAddress set to " + .concat(self.targetEVMAddress.toString()).concat(" at initialization")) + } + + /// Sets the target type for the handler + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { + panic("WFLOWTokenHandler does not utilize a minter") + } + + /// Enables the handler for request handling. The + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun enableBridging() { + self.enabled = true + } + + /// Disables the handler for request handling. + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun disableBridging() { + self.enabled = false + } + } + + /// This resource enables the configuration of Handlers. These Handlers are stored in FlowEVMBridgeConfig from which + /// further setting and getting can be executed. + /// + access(all) resource HandlerConfigurator { + /// Creates a new Handler and adds it to the bridge configuration + /// + /// @param handlerType: The type of handler to create as defined in this contract + /// @param targetType: The type of the asset the handler will handle + /// @param targetEVMAddress: The EVM contract address the handler will handle, can be nil if still unknown + /// @param expectedMinterType: The Type of the expected minter to be set for the created TokenHandler + /// + access(FlowEVMBridgeHandlerInterfaces.Admin) + fun createTokenHandler( + handlerType: Type, + targetType: Type, + targetEVMAddress: EVM.EVMAddress?, + expectedMinterType: Type? + ) { + switch handlerType { + case Type<@CadenceNativeTokenHandler>(): + assert( + expectedMinterType != nil, + message: "CadenceNativeTokenHandler requires an expected minter type but received nil" + ) + let handler <-create CadenceNativeTokenHandler( + targetType: targetType, + targetEVMAddress: targetEVMAddress, + expectedMinterType: expectedMinterType! + ) + FlowEVMBridgeConfig.addTokenHandler(<-handler) + case Type<@WFLOWTokenHandler>(): + assert( + targetEVMAddress != nil, + message: "WFLOWTokenHandler requires a target EVM address but received nil" + ) + let handler <-create WFLOWTokenHandler(wflowEVMAddress: targetEVMAddress!) + FlowEVMBridgeConfig.addTokenHandler(<-handler) + default: + panic("Invalid Handler type requested") + } + } + } + + init() { + self.ConfiguratorStoragePath = /storage/BridgeHandlerConfigurator + self.account.storage.save(<-create HandlerConfigurator(), to: self.ConfiguratorStoragePath) + } +} diff --git a/contracts/external/FlowEVMBridgeUtils.cdc b/contracts/external/FlowEVMBridgeUtils.cdc new file mode 100644 index 000000000..571b860bf --- /dev/null +++ b/contracts/external/FlowEVMBridgeUtils.cdc @@ -0,0 +1,1600 @@ +import NonFungibleToken from 0x1d7e57aa55817448 +import FungibleToken from 0xf233dcee88fe0abe +import MetadataViews from 0x1d7e57aa55817448 +import CrossVMMetadataViews from 0x1d7e57aa55817448 +import FungibleTokenMetadataViews from 0xf233dcee88fe0abe +import ViewResolver from 0x1d7e57aa55817448 +import FlowToken from 0x1654653399040a61 +import FlowStorageFees from 0xe467b9dd11fa00df + +import EVM from 0xe467b9dd11fa00df + +import SerializeMetadata from 0x1e4aa0b87d10b141 +import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 +import CrossVMNFT from 0x1e4aa0b87d10b141 +import IBridgePermissions from 0x1e4aa0b87d10b141 + +/// This contract serves as a source of utility methods leveraged by FlowEVMBridge contracts +// +access(all) +contract FlowEVMBridgeUtils { + + /// Address of the bridge factory Solidity contract + access(self) + var bridgeFactoryEVMAddress: EVM.EVMAddress + /// Delimeter used to derive contract names + access(self) + let delimiter: String + /// Mapping containing contract name prefixes + access(self) + let contractNamePrefixes: {Type: {String: String}} + + /**************** + Constructs + *****************/ + + /// Struct used to preserve and pass around multiple values relating to Cadence asset onboarding + /// + access(all) struct CadenceOnboardingValues { + access(all) let contractAddress: Address + access(all) let name: String + access(all) let symbol: String + access(all) let identifier: String + access(all) let contractURI: String + + init( + contractAddress: Address, + name: String, + symbol: String, + identifier: String, + contractURI: String + ) { + self.contractAddress = contractAddress + self.name = name + self.symbol = symbol + self.identifier = identifier + self.contractURI = contractURI + } + } + + /// Struct used to preserve and pass around multiple values preventing the need to make multiple EVM calls + /// during EVM asset onboarding + /// + access(all) struct EVMOnboardingValues { + access(all) let evmContractAddress: EVM.EVMAddress + access(all) let name: String + access(all) let symbol: String + access(all) let decimals: UInt8? + access(all) let contractURI: String? + access(all) let cadenceContractName: String + access(all) let isERC721: Bool + + init( + evmContractAddress: EVM.EVMAddress, + name: String, + symbol: String, + decimals: UInt8?, + contractURI: String?, + cadenceContractName: String, + isERC721: Bool + ) { + self.evmContractAddress = evmContractAddress + self.name = name + self.symbol = symbol + self.decimals = decimals + self.contractURI = contractURI + self.cadenceContractName = cadenceContractName + self.isERC721 = isERC721 + } + } + + /************************** + Public Bridge Utils + **************************/ + + /// Retrieves the bridge factory contract address + /// + /// @returns The EVMAddress of the bridge factory contract in EVM + /// + access(all) + view fun getBridgeFactoryEVMAddress(): EVM.EVMAddress { + return self.bridgeFactoryEVMAddress + } + + /// Calculates the fee bridge fee based on the given storage usage + the current base fee. + /// + /// @param used: The amount of storage used by the asset + /// + /// @return The calculated fee amount + /// + access(all) + view fun calculateBridgeFee(bytes used: UInt64): UFix64 { + let megabytesUsed = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(used) + let storageFee = FlowStorageFees.storageCapacityToFlow(megabytesUsed) + return storageFee + FlowEVMBridgeConfig.baseFee + } + + /// Returns whether the given type is allowed to be bridged as defined by the IBridgePermissions contract interface. + /// If the type's defining contract does not implement IBridgePermissions, the method returns true as the bridge + /// operates permissionlessly by default. Otherwise, the result of {IBridgePermissions}.allowsBridging() is returned + /// + /// @param type: The Type of the asset to check + /// + /// @return true if the type is allowed to be bridged, false otherwise + /// + access(all) + view fun typeAllowsBridging(_ type: Type): Bool { + let contractAddress = self.getContractAddress(fromType: type) + ?? panic("Could not construct contract address from type identifier: ".concat(type.identifier)) + let contractName = self.getContractName(fromType: type) + ?? panic("Could not construct contract name from type identifier: ".concat(type.identifier)) + if let bridgePermissions = getAccount(contractAddress).contracts.borrow<&{IBridgePermissions}>(name: contractName) { + return bridgePermissions.allowsBridging() + } + return true + } + + /// Returns whether the given address has opted out of enabling bridging for its defined assets + /// + /// @param address: The EVM contract address to check + /// + /// @return false if the address has opted out of enabling bridging, true otherwise + /// + access(all) + fun evmAddressAllowsBridging(_ address: EVM.EVMAddress): Bool { + let callResult = self.dryCall( + signature: "allowsBridging()", + targetEVMAddress: address, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + // Contract doesn't support the method - proceed permissionlessly + if callResult.status != EVM.Status.successful { + return true + } + // Contract is IBridgePermissions - return the result + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + return (decodedResult.length == 1 && decodedResult[0] as! Bool) == true ? true : false + } + + /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge contract defines it or not + /// + /// @param type: The Type of the asset to check + /// + /// @return True if the asset is Cadence-native, false if it is EVM-native + /// + access(all) + view fun isCadenceNative(type: Type): Bool { + let definingAddress = self.getContractAddress(fromType: type) + ?? panic("Could not construct address from type identifier: ".concat(type.identifier)) + return definingAddress != self.account.address + } + + /// Identifies if an asset is a type that is defined by a bridge-owned Cadence contract. For NFTs, this would + /// indicate that the NFT is a bridged representation of a corresponding ERC721. For a Vault, this would + /// indicate that the Vault is a bridged representation of a corresponding ERC20. + /// + /// @param type: The Type of the asset to check + /// + /// @return True if the asset is bridge-defined, false if another Cadence contract defines the type. Reverts if the + /// type is a primitive type that is not defined by a Cadence contract. + /// + access(all) + view fun isBridgeDefined(type: Type): Bool { + let definingAddress = self.getContractAddress(fromType: type) + ?? panic("Could not construct address from type identifier: ".concat(type.identifier)) + return definingAddress == self.account.address + } + + /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge-owned contract defines it or not. + /// Reverts on EVM call failure. + /// + /// @param type: The Type of the asset to check + /// + /// @return True if the asset is EVM-native, false if it is Cadence-native + /// + access(all) + fun isEVMNative(evmContractAddress: EVM.EVMAddress): Bool { + return self.isEVMContractBridgeOwned(evmContractAddress: evmContractAddress) == false + } + + /// Determines if the given EVM contract address was deployed by the bridge by querying the factory contract + /// Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to check + /// + /// @return True if the contract was deployed by the bridge, false otherwise + /// + access(all) + fun isEVMContractBridgeOwned(evmContractAddress: EVM.EVMAddress): Bool { + // Ask the bridge factory if the given contract address was deployed by the bridge + let callResult = self.dryCall( + signature: "isBridgeDeployed(address)", + targetEVMAddress: self.bridgeFactoryEVMAddress, + args: [evmContractAddress], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + + assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) + assert(decodedResult.length == 1, message: "Invalid response length") + + return decodedResult[0] as! Bool + } + + /// Identifies if an asset is ERC721. Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to check + /// + /// @return True if the asset is an ERC721, false otherwise + /// + access(all) + fun isERC721(evmContractAddress: EVM.EVMAddress): Bool { + let callResult = self.dryCall( + signature: "isERC721(address)", + targetEVMAddress: self.bridgeFactoryEVMAddress, + args: [evmContractAddress], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + + assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) + assert(decodedResult.length == 1, message: "Invalid response length") + + return decodedResult[0] as! Bool + } + + /// Identifies if an asset is ERC20 as far as is possible without true EVM type introspection. Reverts on EVM call + /// failure. + /// + /// @param evmContractAddress: The EVM contract address to check + /// + /// @return true if the asset is an ERC20, false otherwise + /// + access(all) + fun isERC20(evmContractAddress: EVM.EVMAddress): Bool { + let callResult = self.dryCall( + signature: "isERC20(address)", + targetEVMAddress: self.bridgeFactoryEVMAddress, + args: [evmContractAddress], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + + assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) + assert(decodedResult.length == 1, message: "Invalid response length") + + return decodedResult[0] as! Bool + } + + /// Returns whether the contract address is either an ERC721 or ERC20 exclusively. Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to check + /// + /// @return True if the contract is either an ERC721 or ERC20, false otherwise + /// + access(all) + fun isValidEVMAsset(evmContractAddress: EVM.EVMAddress): Bool { + let callResult = self.dryCall( + signature: "isValidAsset(address)", + targetEVMAddress: self.bridgeFactoryEVMAddress, + args: [evmContractAddress], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) + assert(decodedResult.length == 1, message: "Invalid response length") + return decodedResult[0] as! Bool + } + + /// Returns whether the given type is either an NFT or FT exclusively + /// + /// @param type: The Type of the asset to check + /// + /// @return True if the type is either an NFT or FT, false otherwise + /// + access(all) + view fun isValidCadenceAsset(type: Type): Bool { + let isCadenceNFT = type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) + let isCadenceFungibleToken = type.isSubtype(of: Type<@{FungibleToken.Vault}>()) + return isCadenceNFT != isCadenceFungibleToken + } + + /// Retrieves the bridge contract's COA EVMAddress + /// + /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM + /// + access(all) + view fun getBridgeCOAEVMAddress(): EVM.EVMAddress { + return self.borrowCOA().address() + } + + /// Retrieves the relevant information for onboarding a Cadence asset to the bridge. This method is used to + /// retrieve the name, symbol, contract address, and contract URI for a given Cadence asset type. These values + /// are used to then deploy a corresponding EVM contract. If EVMBridgedMetadata is supported by the asset's + /// defining contract, the values are retrieved from that view. Otherwise, the values are derived from other + /// common metadata views. + /// + /// @param forAssetType: The Type of the asset to retrieve onboarding values for + /// + /// @return The CadenceOnboardingValues struct containing the asset's name, symbol, identifier, contract address, + /// and contract URI + /// + access(all) + fun getCadenceOnboardingValues(forAssetType: Type): CadenceOnboardingValues { + pre { + self.isValidCadenceAsset(type: forAssetType): "This type is not a supported Flow asset type." + } + // If not an NFT, assumed to be fungible token. + let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) + + // Retrieve the Cadence type's defining contract name, address, & its identifier + var name = self.getContractName(fromType: forAssetType) + ?? panic("Could not contract name from type: ".concat(forAssetType.identifier)) + let identifier = forAssetType.identifier + let cadenceAddress = self.getContractAddress(fromType: forAssetType) + ?? panic("Could not derive contract address for token type: ".concat(identifier)) + // Initialize asset symbol which will be assigned later + // based on presence of asset-defined metadata + var symbol: String? = nil + // Borrow the ViewResolver to attempt to resolve the EVMBridgedMetadata view + let viewResolver = getAccount(cadenceAddress).contracts.borrow<&{ViewResolver}>(name: name)! + var contractURI = "" + + // Try to resolve the EVMBridgedMetadata + let bridgedMetadata = viewResolver.resolveContractView( + resourceType: forAssetType, + viewType: Type() + ) as! MetadataViews.EVMBridgedMetadata? + // Default to project-defined URI if available + if bridgedMetadata != nil { + name = bridgedMetadata!.name + symbol = bridgedMetadata!.symbol + contractURI = bridgedMetadata!.uri.uri() + } else { + if isNFT { + // Otherwise, serialize collection-level NFTCollectionDisplay + if let collectionDisplay = viewResolver.resolveContractView( + resourceType: forAssetType, + viewType: Type() + ) as! MetadataViews.NFTCollectionDisplay? { + name = collectionDisplay.name + let serializedDisplay = SerializeMetadata.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)! + contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}") + } + if symbol == nil { + symbol = SerializeMetadata.deriveSymbol(fromString: name) + } + } else { + let ftDisplay = viewResolver.resolveContractView( + resourceType: forAssetType, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTDisplay? + if ftDisplay != nil { + name = ftDisplay!.name + symbol = ftDisplay!.symbol + } + if contractURI.length == 0 && ftDisplay != nil { + let serializedDisplay = SerializeMetadata.serializeFTDisplay(ftDisplay!) + contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}") + } + } + } + + return CadenceOnboardingValues( + contractAddress: cadenceAddress, + name: name, + symbol: symbol!, + identifier: identifier, + contractURI: contractURI + ) + } + + /// Retrieves identifying information about an EVM contract related to bridge onboarding. + /// + /// @param evmContractAddress: The EVM contract address to retrieve onboarding values for + /// + /// @return The EVMOnboardingValues struct containing the asset's name, symbol, decimals, contractURI, and + /// Cadence contract name as well as whether the asset is an ERC721 + /// + access(all) + fun getEVMOnboardingValues(evmContractAddress: EVM.EVMAddress): EVMOnboardingValues { + // Retrieve the EVM contract's name, symbol, and contractURI + let name: String = self.getName(evmContractAddress: evmContractAddress) + let symbol: String = self.getSymbol(evmContractAddress: evmContractAddress) + let contractURI = self.getContractURI(evmContractAddress: evmContractAddress) + // Default to 18 decimals for ERC20s + var decimals: UInt8 = FlowEVMBridgeConfig.defaultDecimals + + // Derive Cadence contract name + let isERC721: Bool = self.isERC721(evmContractAddress: evmContractAddress) + var cadenceContractName: String = "" + if isERC721 { + // Assert the contract is not mixed asset + let isERC20 = self.isERC20(evmContractAddress: evmContractAddress) + assert(!isERC20, message: "Contract is mixed asset and is not currently supported by the bridge") + // Derive the contract name from the ERC721 contract + cadenceContractName = self.deriveBridgedNFTContractName(from: evmContractAddress) + } else { + // Otherwise, treat as ERC20 + let isERC20 = self.isERC20(evmContractAddress: evmContractAddress) + assert( + isERC20, + message: "Contract ".concat(evmContractAddress.toString()).concat("defines an asset that is not currently supported by the bridge") + ) + cadenceContractName = self.deriveBridgedTokenContractName(from: evmContractAddress) + decimals = self.getTokenDecimals(evmContractAddress: evmContractAddress) + } + + return EVMOnboardingValues( + evmContractAddress: evmContractAddress, + name: name, + symbol: symbol, + decimals: decimals, + contractURI: contractURI, + cadenceContractName: cadenceContractName, + isERC721: isERC721 + ) + } + + /// Retrieves the EVMPointer view from a given type's defining contract if the view is supported. + /// NOTE: This does not guarantee the association is valid, only that the defining Cadence contract declares + /// the association. + /// + /// @param from: The type for which to retrieve the EVMPointer view + /// + /// @return The resolved EVMPointer view for the given type or nil if the view is unsupported + /// + access(all) + fun getEVMPointerView(forType: Type): CrossVMMetadataViews.EVMPointer? { + let contractAddress = forType.address! + let contractName = forType.contractName! + if let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) { + return viewResolver.resolveContractView( + resourceType: forType, + viewType: Type() + ) as? CrossVMMetadataViews.EVMPointer? ?? nil + } + return nil + } + + /************************ + EVM Call Wrappers + ************************/ + + /// Retrieves the NFT/FT name from the given EVM contract address - applies for both ERC20 & ERC721. + /// Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to retrieve the name from + /// + /// @return the name of the asset + /// + access(all) + fun getName(evmContractAddress: EVM.EVMAddress): String { + let callResult = self.dryCall( + signature: "name()", + targetEVMAddress: evmContractAddress, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + + assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset name failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + assert(decodedResult.length == 1, message: "Invalid response length") + + return decodedResult[0] as! String + } + + /// Retrieves the NFT/FT symbol from the given EVM contract address - applies for both ERC20 & ERC721 + /// Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to retrieve the symbol from + /// + /// @return the symbol of the asset + /// + access(all) + fun getSymbol(evmContractAddress: EVM.EVMAddress): String { + let callResult = self.dryCall( + signature: "symbol()", + targetEVMAddress: evmContractAddress, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset symbol failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + assert(decodedResult.length == 1, message: "Invalid response length") + return decodedResult[0] as! String + } + + /// Retrieves the tokenURI for the given NFT ID from the given EVM contract address. Reverts on EVM call failure. + /// Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to retrieve the tokenURI from + /// @param id: The ID of the NFT for which to retrieve the tokenURI value + /// + /// @return the tokenURI of the ERC721 + /// + access(all) + fun getTokenURI(evmContractAddress: EVM.EVMAddress, id: UInt256): String { + let callResult = self.dryCall( + signature: "tokenURI(uint256)", + targetEVMAddress: evmContractAddress, + args: [id], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + + assert(callResult.status == EVM.Status.successful, message: "Call to EVM for tokenURI failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + assert(decodedResult.length == 1, message: "Invalid response length") + + return decodedResult[0] as! String + } + + /// Retrieves the contract URI from the given EVM contract address. Returns nil on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to retrieve the contractURI from + /// + /// @return the contract's contractURI + /// + access(all) + fun getContractURI(evmContractAddress: EVM.EVMAddress): String? { + let callResult = self.dryCall( + signature: "contractURI()", + targetEVMAddress: evmContractAddress, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if callResult.status != EVM.Status.successful { + return nil + } + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + return decodedResult.length == 1 ? decodedResult[0] as! String : nil + } + + /// Retrieves the number of decimals for a given ERC20 contract address. Reverts on EVM call failure. + /// + /// @param evmContractAddress: The ERC20 contract address to retrieve the token decimals from + /// + /// @return the token decimals of the ERC20 + /// + access(all) + fun getTokenDecimals(evmContractAddress: EVM.EVMAddress): UInt8 { + let callResult = self.dryCall( + signature: "decimals()", + targetEVMAddress: evmContractAddress, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + + assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset decimals failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + assert(decodedResult.length == 1, message: "Invalid response length") + + return decodedResult[0] as! UInt8 + } + + /// Determines if the provided owner address is either the owner or approved for the NFT in the ERC721 contract + /// Reverts on EVM call failure. + /// + /// @param ofNFT: The ID of the NFT to query + /// @param owner: The owner address to query + /// @param evmContractAddress: The ERC721 contract address to query + /// + /// @return true if the owner is either the owner or approved for the NFT, false otherwise + /// + access(all) + fun isOwnerOrApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { + return self.isOwner(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress) || + self.isApproved(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress) + } + + /// Returns whether the given owner is the owner of the given NFT. Reverts on EVM call failure. + /// + /// @param ofNFT: The ID of the NFT to query + /// @param owner: The owner address to query + /// @param evmContractAddress: The ERC721 contract address to query + /// + /// @return true if the owner is in fact the owner of the NFT, false otherwise + /// + access(all) + fun isOwner(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { + return self.ownerOf(id: ofNFT, evmContractAddress: evmContractAddress)?.equals(owner) ?? false + } + + /// Returns the owner of a given ERC721 token + /// + /// @param id: The ID of the NFT to query + /// @param evmContractAddress: The ERC721 contract address to query + /// + /// @return The current owner's EVM address or nil if the `ownerOf` call is unsuccessful + /// + access(all) + fun ownerOf(id: UInt256, evmContractAddress: EVM.EVMAddress): EVM.EVMAddress? { + let callResult = self.dryCall( + signature: "ownerOf(uint256)", + targetEVMAddress: evmContractAddress, + args: [id], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if callResult.status == EVM.Status.failed { + return nil + } + let decodedCallResult = EVM.decodeABI(types: [Type()], data: callResult.data) + return decodedCallResult.length == 1 ? decodedCallResult[0] as! EVM.EVMAddress : nil + } + + /// Returns whether the given owner is approved for the given NFT. Reverts on EVM call failure. + /// + /// @param ofNFT: The ID of the NFT to query + /// @param owner: The owner address to query + /// @param evmContractAddress: The ERC721 contract address to query + /// + /// @return true if the owner is in fact approved for the NFT, false otherwise + /// + access(all) + fun isApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { + let callResult = self.dryCall( + signature: "getApproved(uint256)", + targetEVMAddress: evmContractAddress, + args: [ofNFT], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(callResult.status == EVM.Status.successful, message: "Call to ERC721.getApproved(uint256) failed") + let decodedCallResult = EVM.decodeABI(types: [Type()], data: callResult.data) + if decodedCallResult.length == 1 { + let actualApproved = decodedCallResult[0] as! EVM.EVMAddress + return actualApproved.equals(owner) + } + 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()], + data: self.dryCall( + signature: "exists(uint256)", + targetEVMAddress: erc721Address, + args: [id], + gasLimit: FlowEVMBridgeConfig.gasLimit, + 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 owner: The owner address to query + /// @param evmContractAddress: The ERC20 contract address to query + /// + /// @return The UInt256 balance of the owner at the ERC20 contract address. Callers may wish to convert the return + /// value to a UFix64 via convertERC20AmountToCadenceAmount, though note there may be a loss of precision. + /// + access(all) + fun balanceOf(owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): UInt256 { + let callResult = self.dryCall( + signature: "balanceOf(address)", + targetEVMAddress: evmContractAddress, + args: [owner], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.balanceOf(address) failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + assert(decodedResult.length == 1, message: "Invalid response length") + return decodedResult[0] as! UInt256 + } + + /// Determines if the owner has sufficient funds to bridge the given amount at the ERC20 contract address + /// Reverts on EVM call failure. + /// + /// @param amount: The amount to check if the owner has enough balance to cover + /// @param owner: The owner address to query + /// @param evmContractAddress: The ERC20 contract address to query + /// + /// @return true if the owner's balance >= amount, false otherwise + /// + access(all) + fun hasSufficientBalance(amount: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { + return self.balanceOf(owner: owner, evmContractAddress: evmContractAddress) >= amount + } + + /// Retrieves the total supply of the ERC20 contract at the given EVM contract address. Reverts on EVM call failure. + /// + /// @param evmContractAddress: The EVM contract address to retrieve the total supply from + /// + /// @return the total supply of the ERC20 + /// + access(all) + fun totalSupply(evmContractAddress: EVM.EVMAddress): UInt256 { + let callResult = self.dryCall( + signature: "totalSupply()", + targetEVMAddress: evmContractAddress, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.totalSupply() failed") + let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] + assert(decodedResult.length == 1, message: "Invalid response length") + return decodedResult[0] as! UInt256 + } + + /// Converts the given amount of ERC20 tokens to the equivalent amount in FLOW tokens based on the ERC20s decimals + /// value. Note that may be some loss of decimal precision as UFix64 supports precision for 8 decimal places. + /// Reverts on EVM call failure. + /// + /// @param amount: The amount of ERC20 tokens to convert + /// @param erc20Address: The EVM contract address of the ERC20 token + /// + /// @return the equivalent amount in FLOW tokens as a UFix64 + /// + access(all) + fun convertERC20AmountToCadenceAmount(_ amount: UInt256, erc20Address: EVM.EVMAddress): UFix64 { + return self.uint256ToUFix64( + value: amount, + decimals: self.getTokenDecimals(evmContractAddress: erc20Address) + ) + } + + /// Converts the given amount of Cadence fungible tokens to the equivalent amount in ERC20 tokens based on the + /// ERC20s decimals. Note that there may be some loss of decimal precision as UFix64 supports precision for 8 + /// decimal places. Reverts on EVM call failure. + /// + /// @param amount: The amount of Cadence fungible tokens to convert + /// @param erc20Address: The EVM contract address of the ERC20 token + /// + /// @return the equivalent amount in ERC20 tokens as a UInt256 + /// + access(all) + fun convertCadenceAmountToERC20Amount(_ amount: UFix64, erc20Address: EVM.EVMAddress): UInt256 { + return self.ufix64ToUInt256(value: amount, decimals: self.getTokenDecimals(evmContractAddress: erc20Address)) + } + + /// Gets the declared Cadence contract address declared by an EVM contract in conformance to the ICrossVM.sol + /// contract interface. Reverts if the EVM call is unsuccessful. + /// NOTE: Just because an EVM contract declares an association does not mean it it is valid! + /// + /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the declared Cadence + /// contract address + /// + /// @return The resulting Cadence Address as declared associated by the provided EVM contract or nil if the call fails + /// + access(all) + fun getDeclaredCadenceAddressFromCrossVM(evmContract: EVM.EVMAddress): Address? { + let cadenceAddrRes = self.dryCall( + signature: "getCadenceAddress()", + targetEVMAddress: evmContract, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if cadenceAddrRes.status != EVM.Status.successful { + return nil + } + let decodedCadenceAddr = EVM.decodeABI(types: [Type()], data: cadenceAddrRes.data) + assert(decodedCadenceAddr.length == 1) + var cadenceAddrStr = decodedCadenceAddr[0] as! String + if cadenceAddrStr[1] != "x" { + cadenceAddrStr = "0x".concat(cadenceAddrStr) + } + return Address.fromString(cadenceAddrStr) ?? nil + } + + /// Gets the declared Cadence Type declared by an EVM contract in conformance to the ICrossVM.sol contract + /// interface. Reverts if the EVM call is unsuccessful. + /// NOTE: Just because an EVM contract declares an association does not mean it it is valid! + /// + /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the declared Cadence + /// Type + /// + /// @return The resulting Cadence Type as declared associated by the provided EVM contract or nil if the call fails + /// + /// + access(all) + fun getDeclaredCadenceTypeFromCrossVM(evmContract: EVM.EVMAddress): Type? { + let cadenceIdentifierRes = self.dryCall( + signature: "getCadenceIdentifier()", + targetEVMAddress: evmContract, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if cadenceIdentifierRes.status != EVM.Status.successful { + return nil + } + let decodedCadenceIdentifier = EVM.decodeABI(types: [Type()], data: cadenceIdentifierRes.data) + assert(decodedCadenceIdentifier.length == 1) + let cadenceIdentifier = decodedCadenceIdentifier[0] as! String + return CompositeType(cadenceIdentifier) ?? nil + } + + /// Returns whether the provided EVM contract conforms to ICrossVMBridgeERC721Fulfillment.sol contract interface. + /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully + /// registered + /// + /// @param evmContract: The EVM contract to check for ICrossVMBridgeERC721 conformance + /// + /// @return True if conformance is found, false otherwise + /// + access(all) + fun supportsICrossVMBridgeERC721Fulfillment(evmContract: EVM.EVMAddress): Bool { + let interfaceID = EVM.EVMBytes4(value: "2e608d70".decodeHex().toConstantSized<[UInt8; 4]>()!) + let supportsRes = self.dryCall( + signature: "supportsInterface(bytes4)", + targetEVMAddress: evmContract, + args: [interfaceID], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if supportsRes.status != EVM.Status.successful { + return false + } + let decodedSupports = EVM.decodeABI(types: [Type()], data: supportsRes.data) + if decodedSupports.length != 1 { + return false + } + return decodedSupports[0] as! Bool + } + + /// Returns whether the provided EVM contract conforms to ICrossVMBridgeCallable.sol contract interface. + /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully + /// registered + /// + /// @param evmContract: The EVM contract to check for ICrossVMBridgeCallable conformance + /// + /// @return True if conformance is found, false otherwise + /// + access(all) + fun supportsICrossVMBridgeCallable(evmContract: EVM.EVMAddress): Bool { + let interfaceID = EVM.EVMBytes4(value: "b7f9a9ec".decodeHex().toConstantSized<[UInt8; 4]>()!) + let supportsRes = self.dryCall( + signature: "supportsInterface(bytes4)", + targetEVMAddress: evmContract, + args: [interfaceID], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if supportsRes.status != EVM.Status.successful { + return false + } + let decodedSupports = EVM.decodeABI(types: [Type()], data: supportsRes.data) + if decodedSupports.length != 1 { + return false + } + return decodedSupports[0] as! Bool + } + + /// Returns whether the provided EVM contract conforms to both ICrossVMBridgeERC721Fulfillment and + /// ICrossVMBridgeCallable Solidity contract interfaces + /// + /// @param evmContract: The EVM contract to check for conformance + /// + /// @return True if conformance is found, false otherwise + /// + access(all) + fun supportsCadenceNativeNFTEVMInterfaces(evmContract: EVM.EVMAddress): Bool { + return self.supportsICrossVMBridgeCallable(evmContract: evmContract) + && self.supportsICrossVMBridgeERC721Fulfillment(evmContract: evmContract) + } + + /// Returns the VM Bridge address designated by the ICrossVMBridgeCallable conforming EVM contract. Reverts on call + /// failure. + /// + /// @param evmContract: The ICrossVMBridgeCallable EVM contract from which to retrieve the value + /// + /// @return The EVM address designated as the VM bridge address in the provided contract + /// + access(all) + fun getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: EVM.EVMAddress): EVM.EVMAddress? { + let cadenceIdentifierRes = self.dryCall( + signature: "vmBridgeAddress()", + targetEVMAddress: evmContract, + args: [], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + if cadenceIdentifierRes.status != EVM.Status.successful { + return nil + } + let decodedCadenceIdentifier = EVM.decodeABI(types: [Type()], data: cadenceIdentifierRes.data) + return decodedCadenceIdentifier.length == 1 ? decodedCadenceIdentifier[0] as! EVM.EVMAddress : nil + } + + /************************ + Derivation Utils + ************************/ + + /// Derives the StoragePath where the escrow locker is stored for a given Type of asset & returns. The given type + /// must be of an asset supported by the bridge. + /// + /// @param fromType: The type of the asset the escrow locker is being derived for + /// + /// @return The StoragePath associated with the type's escrow Locker, or nil if the type is not supported + /// + access(all) + view fun deriveEscrowStoragePath(fromType: Type): StoragePath? { + if !self.isValidCadenceAsset(type: fromType) { + return nil + } + var prefix = "" + if fromType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) { + prefix = "flowEVMBridgeNFTEscrow" + } else if fromType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + prefix = "flowEVMBridgeTokenEscrow" + } + assert(prefix.length > 1, message: "Invalid prefix") + if let splitIdentifier = self.splitObjectIdentifier(identifier: fromType.identifier) { + let sourceContractAddress = Address.fromString("0x".concat(splitIdentifier[1]))! + let sourceContractName = splitIdentifier[2] + let resourceName = splitIdentifier[3] + return StoragePath( + identifier: prefix.concat(self.delimiter) + .concat(sourceContractAddress.toString()).concat(self.delimiter) + .concat(sourceContractName).concat(self.delimiter) + .concat(resourceName) + ) ?? nil + } + return nil + } + + /// Derives the Cadence contract name for a given EVM NFT of the form + /// EVMVMBridgedNFT_<0xCONTRACT_ADDRESS> + /// + /// @param from evmContract: The EVM contract address to derive the Cadence NFT contract name for + /// + /// @return The derived Cadence FT contract name + /// + access(all) + view fun deriveBridgedNFTContractName(from evmContract: EVM.EVMAddress): String { + return self.contractNamePrefixes[Type<@{NonFungibleToken.NFT}>()]!["bridged"]! + .concat(self.delimiter) + .concat(evmContract.toString()) + } + + /// Derives the Cadence contract name for a given EVM fungible token of the form + /// EVMVMBridgedToken_<0xCONTRACT_ADDRESS> + /// + /// @param from evmContract: The EVM contract address to derive the Cadence FT contract name for + /// + /// @return The derived Cadence FT contract name + /// + access(all) + view fun deriveBridgedTokenContractName(from evmContract: EVM.EVMAddress): String { + return self.contractNamePrefixes[Type<@{FungibleToken.Vault}>()]!["bridged"]! + .concat(self.delimiter) + .concat(evmContract.toString()) + } + + /**************** + Math Utils + ****************/ + + /// Raises the base to the power of the exponent + /// + access(all) + view fun pow(base: UInt256, exponent: UInt8): UInt256 { + if exponent == 0 { + return 1 + } + + var r = base + var exp: UInt8 = 1 + while exp < exponent { + r = r * base + exp = exp + 1 + } + + return r + } + + /// Raises the fixed point base to the power of the exponent + /// + access(all) + view fun ufixPow(base: UFix64, exponent: UInt8): UFix64 { + if exponent == 0 { + return 1.0 + } + + var r = base + var exp: UInt8 = 1 + while exp < exponent { + r = r * base + exp = exp + 1 + } + + return r + } + + /// Converts a UFix64 to a UInt256 + // + access(all) + view fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 { + // Default to 10e8 scale, catching instances where decimals are less than default and scale appropriately + let ufixScaleExp: UInt8 = decimals < 8 ? decimals : 8 + var ufixScale = self.ufixPow(base: 10.0, exponent: ufixScaleExp) + + // Separate the fractional and integer parts of the UFix64 + let integer = UInt256(value) + var fractional = (value % 1.0) * ufixScale + + // Calculate the multiplier for integer and fractional parts + var integerMultiplier: UInt256 = self.pow(base:10, exponent: decimals) + let fractionalMultiplierExp: UInt8 = decimals < 8 ? 0 : decimals - 8 + var fractionalMultiplier: UInt256 = self.pow(base:10, exponent: fractionalMultiplierExp) + + // Scale and sum the parts + return integer * integerMultiplier + UInt256(fractional) * fractionalMultiplier + } + + /// Converts a UInt256 to a UFix64 + /// + access(all) + view fun uint256ToUFix64(value: UInt256, decimals: UInt8): UFix64 { + // Calculate scale factors for the integer and fractional parts + let absoluteScaleFactor = self.pow(base: 10, exponent: decimals) + + // Separate the integer and fractional parts of the value + let scaledValue = value / absoluteScaleFactor + var fractional = value % absoluteScaleFactor + // Scale the fractional part + let scaledFractional = self.uint256FractionalToScaledUFix64Decimals(value: fractional, decimals: decimals) + + // Ensure the parts do not exceed the max UFix64 value before conversion + assert( + scaledValue <= UInt256(UFix64.max), + message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value") + ) + /// Check for the max value that can be converted to a UFix64 without overflowing + assert( + scaledValue == UInt256(UFix64.max) ? scaledFractional < 0.09551616 : true, + message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value") + ) + + return UFix64(scaledValue) + scaledFractional + } + + /// Converts a UInt256 fractional value with the given decimal places to a scaled UFix64. Note that UFix64 has + /// decimal precision of 8 places so converted values may lose precision and be rounded down. + /// + access(all) + view fun uint256FractionalToScaledUFix64Decimals(value: UInt256, decimals: UInt8): UFix64 { + pre { + self.getNumberOfDigits(value) <= decimals: "Fractional digits exceed the defined decimal places" + } + post { + result < 1.0: "Resulting scaled fractional exceeds 1.0" + } + + var fractional = value + // Truncate fractional to the first 8 decimal places which is the max precision for UFix64 + if decimals >= 8 { + fractional = fractional / self.pow(base: 10, exponent: decimals - 8) + } + // Return early if the truncated fractional part is now 0 + if fractional == 0 { + return 0.0 + } + + // Scale the fractional part + let fractionalMultiplier = self.ufixPow(base: 0.1, exponent: decimals < 8 ? decimals : 8) + return UFix64(fractional) * fractionalMultiplier + } + + /// Returns the value as a UInt64 if it fits, otherwise panics + /// + access(all) + view fun uint256ToUInt64(value: UInt256): UInt64 { + return value <= UInt256(UInt64.max) ? UInt64(value) : panic("Value too large to fit into UInt64") + } + + /// Returns the number of digits in the given UInt256 + /// + access(all) + view fun getNumberOfDigits(_ value: UInt256): UInt8 { + var tmp = value + var digits: UInt8 = 0 + while tmp > 0 { + tmp = tmp / 10 + digits = digits + 1 + } + return digits + } + + /*************************** + Type Identifier Utils + ***************************/ + + /// Returns the contract address from the given Type + /// + /// @param fromType: The Type to extract the contract address from + /// + /// @return The defining contract's Address, or nil if the identifier does not have an associated Address + /// + access(all) + view fun getContractAddress(fromType: Type): Address? { + return fromType.address + } + + /// Returns the defining contract name from the given Type + /// + /// @param fromType: The Type to extract the contract name from + /// + /// @return The defining contract's name, or nil if the identifier does not have an associated contract name + /// + access(all) + view fun getContractName(fromType: Type): String? { + return fromType.contractName + } + + /// Returns the object's name from the given Type's identifier where the identifier is in the format + /// of: A... + /// + /// @param fromType: The Type to extract the object name from + /// + /// @return The object's name, or nil if the identifier does identify an object + /// + access(all) + view fun getObjectName(fromType: Type): String? { + if let identifierSplit = self.splitObjectIdentifier(identifier: fromType.identifier) { + return identifierSplit[3] + } + return nil + } + + /// Splits the given identifier into its constituent parts defined by a delimiter of '".'" + /// + /// @param identifier: The identifier to split + /// + /// @return An array of the identifier's constituent parts, or nil if the identifier does not have 4 parts + /// + access(all) + view fun splitObjectIdentifier(identifier: String): [String]? { + let identifierSplit = identifier.split(separator: ".") + return identifierSplit.length != 4 ? nil : identifierSplit + } + + /// Builds a composite type from the given identifier parts + /// + /// @param address: The defining contract address + /// @param contractName: The defining contract name + /// @param resourceName: The resource name + /// + access(all) + view fun buildCompositeType(address: Address, contractName: String, resourceName: String): Type? { + let addressStr = address.toString() + let subtract0x = addressStr.slice(from: 2, upTo: addressStr.length) + let identifier = "A".concat(".").concat(subtract0x).concat(".").concat(contractName).concat(".").concat(resourceName) + return CompositeType(identifier) + } + + /************************** + FungibleToken Utils + **************************/ + + /// Returns the `createEmptyVault()` function from a Vault Type's defining contract or nil if either the Type is not + access(all) fun getCreateEmptyVaultFunction(forType: Type): (fun (Type): @{FungibleToken.Vault})? { + // We can only reasonably assume that the requested function is accessible from a FungibleToken contract + if !forType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + return nil + } + // Vault Types should guarantee that the following forced optionals are safe + let contractAddress = self.getContractAddress(fromType: forType)! + let contractName = self.getContractName(fromType: forType)! + let tokenContract: &{FungibleToken} = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>( + name: contractName + )! + return tokenContract.createEmptyVault + } + + /****************************** + Bridge-Access Only Utils + ******************************/ + + /// Deposits fees to the bridge account's FlowToken Vault - helps fund asset storage + /// + access(account) + fun depositFee(_ feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, feeAmount: UFix64) { + let vault = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault) + ?? panic("Could not borrow FlowToken.Vault reference") + + let feeVault <-feeProvider.withdraw(amount: feeAmount) as! @FlowToken.Vault + assert(feeVault.balance == feeAmount, message: "Fee provider did not return the requested fee") + + vault.deposit(from: <-feeVault) + } + + /// Enables other bridge contracts to orchestrate bridge operations from contract-owned COA + /// + access(account) + view fun borrowCOA(): auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount { + return self.account.storage.borrow( + from: FlowEVMBridgeConfig.coaStoragePath + ) ?? panic("Could not borrow COA reference") + } + + /// Shared helper simplifying calls using the bridge account's COA + /// + access(account) + fun call( + signature: String, + targetEVMAddress: EVM.EVMAddress, + args: [AnyStruct], + gasLimit: UInt64, + value: UFix64 + ): EVM.Result { + let calldata = EVM.encodeABIWithSignature(signature, args) + let valueBalance = EVM.Balance(attoflow: 0) + valueBalance.setFLOW(flow: value) + return self.borrowCOA().call( + to: targetEVMAddress, + data: calldata, + gasLimit: gasLimit, + value: valueBalance + ) + } + + /// Shared helper simplifying dryCalls using the bridge account's COA. Note that `COA.dryCall` does not execute the + /// call within EVM, serving solely as a mechanism for retrieving data from Flow-EVM environment. + /// + access(account) + fun dryCall( + signature: String, + targetEVMAddress: EVM.EVMAddress, + args: [AnyStruct], + gasLimit: UInt64, + value: UFix64 + ): EVM.Result { + let calldata = EVM.encodeABIWithSignature(signature, args) + let valueBalance = EVM.Balance(attoflow: 0) + valueBalance.setFLOW(flow: value) + return self.borrowCOA().dryCall( + to: targetEVMAddress, + data: calldata, + gasLimit: gasLimit, + value: valueBalance + ) + } + + /// 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 ERC721 requesting to be transferred") + assert(!toPreStatus, message: "Recipient already owns ERC721 attempting to be transferred") + + let transferResult: EVM.Result = self.call( + signature: "safeTransferFrom(address,address,uint256)", + targetEVMAddress: erc721Address, + args: [bridgeCOAAddress, to, id], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert( + transferResult.status == EVM.Status.successful, + message: "safeTransferFrom call to ERC721 transferring NFT from escrow 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: "ERC721 is still in escrow after transfer") + assert(toPostStatus, message: "ERC721 was not successfully transferred to recipient from escrow") + } + + /// Executes a safeMint call on the given ERC721 contract address, minting an ERC721 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 = self.call( + signature: "safeMint(address,uint256,string)", + targetEVMAddress: erc721Address, + args: [to, id, uri], + gasLimit: FlowEVMBridgeConfig.gasLimit, + 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 a safeMint call on the given ERC721 contract address, minting an ERC721 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 mustFulfillNFTToEVM(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, maybeBytes: EVM.EVMBytes?) { + let fulfillResult = self.call( + signature: "fulfillToEVM(address,uint256,bytes)", + targetEVMAddress: erc721Address, + args: [to, id, maybeBytes ?? EVM.EVMBytes(value: [])], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert( + fulfillResult.status == EVM.Status.successful, + message: "Fulfill ERC721 \(erc721Address.toString()) with id \(id) to \(to.toString()) failed with error code \(fulfillResult.errorCode): \(fulfillResult.errorMessage)" + ) + + 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 = self.call( + signature: "updateTokenURI(uint256,string)", + targetEVMAddress: erc721Address, + args: [id, uri], + gasLimit: FlowEVMBridgeConfig.gasLimit, + 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 mustEscrowERC721( + owner: EVM.EVMAddress, + id: UInt256, + erc721Address: EVM.EVMAddress, + protectedTransferCall: fun (EVM.EVMAddress): 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 ERC721") + + // Call the protected transfer function which should execute a transfer call from the owner to escrow + let transferResult = protectedTransferCall(erc721Address) + assert(transferResult.status == EVM.Status.successful, message: "Transfer ERC721 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: "ERC721 was not successfully escrowed") + } + + /// Unwraps an ERC721 token, calling `ERC721Wrapper.withdrawTo(address,uint256[])` on the provided wrapper address + /// and ensuring that the underlying ERC721 is owned by the bridge COA before returning. + /// NOTE: This method relies on implementation of OpenZeppelin's `ERC721Wrapper` contract interface, reverting if + /// the unwrap operation is unsuccessful. + /// + access(account) + fun mustUnwrapERC721( + id: UInt256, + erc721WrapperAddress: EVM.EVMAddress, + underlyingEVMAddress: EVM.EVMAddress + ) { + assert( + self.isOwner(ofNFT: id, owner: erc721WrapperAddress, evmContractAddress: underlyingEVMAddress), + message: "Attempting to unwrap \(underlyingEVMAddress.toString()) ID \(id), but token is not wrapped by \(erc721WrapperAddress.toString())" + ) + let bridgeCOA = self.getBridgeCOAEVMAddress() + + let unwrapResult: EVM.Result = self.call( + signature: "withdrawTo(address,uint256[])", + targetEVMAddress: erc721WrapperAddress, + args: [bridgeCOA, [id]], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert( + unwrapResult.status == EVM.Status.successful, + message: "Call to \(erc721WrapperAddress.toString()) ERC721Wrapper.withdrawTo(address,uint256[]) failed" + ) + + assert( + self.isOwner(ofNFT: id, owner: bridgeCOA, evmContractAddress: underlyingEVMAddress), + message: "Unsuccessful escrow of wrapped ERC721 \(erc721WrapperAddress.toString()) wrapping underlying \(underlyingEVMAddress.toString()) ID \(id)" + ) + } + + /// Mints ERC20 tokens to the recipient and confirms that the recipient's balance was updated + /// + access(account) + fun mustMintERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) { + let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) + // Mint tokens to the recipient + let mintResult: EVM.Result = self.call( + signature: "mint(address,uint256)", + targetEVMAddress: erc20Address, + args: [to, amount], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge ERC20 contract failed") + // Ensure bridge to recipient was succcessful + let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) + assert( + toPostBalance == toPreBalance + amount, + message: "Recipient didn't receive minted ERC20 tokens during bridging" + ) + } + + /// Transfers ERC20 tokens to the recipient and confirms that the recipient's balance was incremented and the escrow + /// balance was decremented by the requested amount. + /// + access(account) + fun mustTransferERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) { + let bridgeCOAAddress = self.getBridgeCOAEVMAddress() + + let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) + let escrowPreBalance = self.balanceOf( + owner: bridgeCOAAddress, + evmContractAddress: erc20Address + ) + + // Transfer tokens to the recipient + let transferResult: EVM.Result = self.call( + signature: "transfer(address,uint256)", + targetEVMAddress: erc20Address, + args: [to, amount], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(transferResult.status == EVM.Status.successful, message: "transfer call to ERC20 contract failed") + + // Ensure bridge to recipient was succcessful + let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) + let escrowPostBalance = self.balanceOf( + owner: bridgeCOAAddress, + evmContractAddress: erc20Address + ) + assert( + toPostBalance == toPreBalance + amount, + message: "Recipient's ERC20 balance did not increment by the requested amount after transfer from escrow" + ) + assert( + escrowPostBalance == escrowPreBalance - amount, + message: "Escrow ERC20 balance did not decrement by the requested amount after transfer from escrow" + ) + } + + /// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was + /// successful by validating that the named owner's balance was decremented by the requested amount and the bridge + /// escrow balance was incremented by the same amount. + /// + access(account) + fun mustEscrowERC20( + owner: EVM.EVMAddress, + amount: UInt256, + erc20Address: EVM.EVMAddress, + protectedTransferCall: fun (): EVM.Result + ) { + // Ensure the caller is has sufficient balance to bridge the requested amount + let hasSufficientBalance = self.hasSufficientBalance( + amount: amount, + owner: owner, + evmContractAddress: erc20Address + ) + assert(hasSufficientBalance, message: "Caller does not have sufficient balance to bridge requested tokens") + + // Get the owner and escrow balances before transfer + let ownerPreBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) + let bridgePreBalance = self.balanceOf( + owner: self.getBridgeCOAEVMAddress(), + evmContractAddress: erc20Address + ) + + // 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 via callback failed") + + // Get the resulting balances after transfer + let ownerPostBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) + let bridgePostBalance = self.balanceOf( + owner: self.getBridgeCOAEVMAddress(), + evmContractAddress: erc20Address + ) + + // Confirm the transfer of the expected was successful in both sending owner and recipient escrow + assert(ownerPostBalance == ownerPreBalance - amount, message: "Transfer to owner failed") + assert(bridgePostBalance == bridgePreBalance + amount, message: "Transfer to bridge escrow failed") + } + + /// Executes a `burn(uint256)` call targeting the provided ERC721 contract address. Reverts if the call is + /// unsuccessful + /// + access(account) + fun mustBurnERC721(erc721Address: EVM.EVMAddress, id: UInt256) { + let burnResult = FlowEVMBridgeUtils.call( + signature: "burn(uint256)", + targetEVMAddress: erc721Address, + args: [id], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(burnResult.status == EVM.Status.successful, + message: "0x\(erc721Address.toString()).burn(\(id)) failed with error code \(burnResult.errorCode) and message: \(burnResult.errorMessage)") + } + + /// Calls to the bridge factory to deploy an ERC721/ERC20 contract and returns the deployed contract address + /// + access(account) + fun mustDeployEVMContract( + name: String, + symbol: String, + cadenceAddress: Address, + flowIdentifier: String, + contractURI: String, + isERC721: Bool + ): EVM.EVMAddress { + let deployerTag = isERC721 ? "ERC721" : "ERC20" + let deployResult: EVM.Result = self.call( + signature: "deploy(string,string,string,string,string,string)", + targetEVMAddress: self.bridgeFactoryEVMAddress, + args: [deployerTag, name, symbol, cadenceAddress.toString(), flowIdentifier, contractURI], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ) + assert(deployResult.status == EVM.Status.successful, message: "EVM Token contract deployment failed") + let decodedResult: [AnyStruct] = EVM.decodeABI(types: [Type()], data: deployResult.data) + assert(decodedResult.length == 1, message: "Invalid response length") + return decodedResult[0] as! EVM.EVMAddress + } + + /// Calls `setSymbol(string)` on the EVM contract as exposed on FlowEVMBridgedERC721 contracts, enabling Cadence + /// NFTs to update their EVM symbol via EVMBridgedMetadata.symbol. The call's status is returned so conditional + /// execution can be handled on the caller's end. + /// + access(account) + fun tryUpdateSymbol(_ evmContractAddress: EVM.EVMAddress, symbol: String): Bool { + return self.call( + signature: "setSymbol(string)", + targetEVMAddress: evmContractAddress, + args: [symbol], + gasLimit: FlowEVMBridgeConfig.gasLimit, + value: 0.0 + ).status == EVM.Status.successful + } + + init(bridgeFactoryAddressHex: String) { + self.delimiter = "_" + self.contractNamePrefixes = { + Type<@{NonFungibleToken.NFT}>(): { + "bridged": "EVMVMBridgedNFT" + }, + Type<@{FungibleToken.Vault}>(): { + "bridged": "EVMVMBridgedToken" + } + } + self.bridgeFactoryEVMAddress = EVM.addressFromString(bridgeFactoryAddressHex.toLower()) + } +} diff --git a/contracts/external/FungibleToken.cdc b/contracts/external/FungibleToken.cdc new file mode 100644 index 000000000..9b634118c --- /dev/null +++ b/contracts/external/FungibleToken.cdc @@ -0,0 +1,332 @@ +/** + +# The Flow Fungible Token standard + +## `FungibleToken` contract + +If a users wants to deploy a new token contract, their contract +needs to implement the FungibleToken interface and their tokens +need to implement the interfaces defined in this contract. + +/// Contributors (please add to this list if you contribute!): +/// - Joshua Hannan - https://github.com/joshuahannan +/// - Bastian Müller - https://twitter.com/turbolent +/// - Dete Shirley - https://twitter.com/dete73 +/// - Bjarte Karlsen - https://twitter.com/0xBjartek +/// - Austin Kline - https://twitter.com/austin_flowty +/// - Giovanni Sanchez - https://twitter.com/gio_incognito +/// - Deniz Edincik - https://twitter.com/bluesign +/// - Jonny - https://github.com/dryruner +/// +/// Repo reference: https://github.com/onflow/flow-ft + +## `Vault` resource interface + +Each fungible token resource type needs to implement the `Vault` resource interface. + +## `Provider`, `Receiver`, and `Balance` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +It gives users the ability to make custom resources that implement +these interfaces to do various things with the tokens. +For example, a faucet can be implemented by conforming +to the Provider interface. + +*/ + +import ViewResolver from 0x1d7e57aa55817448 +import Burner from 0xf233dcee88fe0abe + +/// FungibleToken +/// +/// Fungible Token implementations should implement the fungible token +/// interface. +access(all) contract interface FungibleToken: ViewResolver { + + // An entitlement for allowing the withdrawal of tokens from a Vault + access(all) entitlement Withdraw + + /// The event that is emitted when tokens are withdrawn + /// from any Vault that implements the `Vault` interface + access(all) event Withdrawn(type: String, + amount: UFix64, + from: Address?, + fromUUID: UInt64, + withdrawnUUID: UInt64, + balanceAfter: UFix64) + + /// The event that is emitted when tokens are deposited to + /// any Vault that implements the `Vault` interface + access(all) event Deposited(type: String, + amount: UFix64, + to: Address?, + toUUID: UInt64, + depositedUUID: UInt64, + balanceAfter: UFix64) + + /// Event that is emitted when the global `Burner.burn()` method + /// is called with a non-zero balance + access(all) event Burned(type: String, amount: UFix64, fromUUID: UInt64) + + /// Balance + /// + /// The interface that provides a standard field + /// for representing balance + /// + access(all) resource interface Balance: Burner.Burnable { + access(all) var balance: UFix64 + + // This default implementation needs to be in a separate interface + // from the one in `Vault` so that the conditions get enforced + // in the correct one + access(contract) fun burnCallback() { + self.balance = 0.0 + } + } + + /// Provider + /// + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on `balance` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + access(all) resource interface Provider { + + /// Function to ask a provider if a specific amount of tokens + /// is available to be withdrawn + /// This could be useful to avoid panicing when calling withdraw + /// when the balance is unknown + /// Additionally, if the provider is pulling from multiple vaults + /// it only needs to check some of the vaults until the desired amount + /// is reached, potentially helping with performance. + /// + /// @param amount the amount of tokens requested to potentially withdraw + /// @return Bool Whether or not this amount is available to withdraw + /// + access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool + + /// withdraw subtracts tokens from the implementing resource + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is `access(Withdraw)` + /// So in order to access it, one would either need the object itself + /// or an entitled reference with `Withdraw`. + /// + /// @param amount the amount of tokens to withdraw from the resource + /// @return The Vault with the withdrawn tokens + /// + access(Withdraw) fun withdraw(amount: UFix64): @{Vault} { + post { + // `result` refers to the return value + result.balance == amount: + "FungibleToken.Provider.withdraw: Cannot withdraw tokens!" + .concat("The balance of the withdrawn tokens (").concat(result.balance.toString()) + .concat(") is not equal to the amount requested to be withdrawn (") + .concat(amount.toString()).concat(")") + } + } + } + + /// Receiver + /// + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + access(all) resource interface Receiver { + + /// deposit takes a Vault and deposits it into the implementing resource type + /// + /// @param from the Vault that contains the tokens to deposit + /// + access(all) fun deposit(from: @{Vault}) + + /// getSupportedVaultTypes returns a dictionary of Vault types + /// and whether the type is currently supported by this Receiver + /// + /// @return {Type: Bool} A dictionary that indicates the supported types + /// If a type is not supported, it should be `nil`, not false + /// + access(all) view fun getSupportedVaultTypes(): {Type: Bool} + + /// Returns whether or not the given type is accepted by the Receiver + /// A vault that can accept any type should just return true by default + /// + /// @param type The type to query about + /// @return Bool Whether or not the vault type is supported + /// + access(all) view fun isSupportedVaultType(type: Type): Bool + } + + /// Vault + /// Conforms to all other interfaces so that implementations + /// only have to conform to `Vault` + /// + access(all) resource interface Vault: Receiver, Provider, Balance, ViewResolver.Resolver, Burner.Burnable { + + /// Field that tracks the balance of a vault + access(all) var balance: UFix64 + + /// Called when a fungible token is burned via the `Burner.burn()` method + /// Implementations can do any bookkeeping or emit any events + /// that should be emitted when a vault is destroyed. + /// Many implementations will want to update the token's total supply + /// to reflect that the tokens have been burned and removed from the supply. + /// Implementations also need to set the balance to zero before the end of the function + /// This is to prevent vault owners from spamming fake Burned events. + access(contract) fun burnCallback() { + pre { + emit Burned(type: self.getType().identifier, amount: self.balance, fromUUID: self.uuid) + } + post { + self.balance == 0.0: + "FungibleToken.Vault.burnCallback: Cannot burn this Vault with Burner.burn(). " + .concat("The balance must be set to zero during the burnCallback method so that it cannot be spammed.") + } + } + + /// getSupportedVaultTypes + /// The default implementation is included here because vaults are expected + /// to only accepted their own type, so they have no need to provide an implementation + /// for this function + /// + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + // Below check is implemented to make sure that run-time type would + // only get returned when the parent resource conforms with `FungibleToken.Vault`. + if self.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()) { + return {self.getType(): true} + } else { + // Return an empty dictionary as the default value for resource who don't + // implement `FungibleToken.Vault`, such as `FungibleTokenSwitchboard`, `TokenForwarder` etc. + return {} + } + } + + /// Checks if the given type is supported by this Vault + access(all) view fun isSupportedVaultType(type: Type): Bool { + return self.getSupportedVaultTypes()[type] ?? false + } + + /// withdraw subtracts `amount` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + access(Withdraw) fun withdraw(amount: UFix64): @{Vault} { + pre { + self.balance >= amount: + "FungibleToken.Vault.withdraw: Cannot withdraw tokens! " + .concat("The amount requested to be withdrawn (").concat(amount.toString()) + .concat(") is greater than the balance of the Vault (") + .concat(self.balance.toString()).concat(").") + } + post { + result.getType() == self.getType(): + "FungibleToken.Vault.withdraw: Cannot withdraw tokens! " + .concat("The withdraw method tried to return an incompatible Vault type <") + .concat(result.getType().identifier).concat(">. ") + .concat("It must return a Vault with the same type as self <") + .concat(self.getType().identifier).concat(">.") + + // use the special function `before` to get the value of the `balance` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "FungibleToken.Vault.withdraw: Cannot withdraw tokens! " + .concat("The sender's balance after the withdrawal (") + .concat(self.balance.toString()) + .concat(") must be the difference of the previous balance (").concat(before(self.balance.toString())) + .concat(") and the amount withdrawn (").concat(amount.toString()).concat(")") + + emit Withdrawn( + type: result.getType().identifier, + amount: amount, + from: self.owner?.address, + fromUUID: self.uuid, + withdrawnUUID: result.uuid, + balanceAfter: self.balance + ) + } + } + + /// deposit takes a Vault and adds its balance to the balance of this Vault + /// + access(all) fun deposit(from: @{FungibleToken.Vault}) { + // Assert that the concrete type of the deposited vault is the same + // as the vault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "FungibleToken.Vault.deposit: Cannot deposit tokens! " + .concat("The type of the deposited tokens <") + .concat(from.getType().identifier) + .concat("> has to be the same type as the Vault being deposited into <") + .concat(self.getType().identifier) + .concat(">. Check that you are withdrawing and depositing to the correct paths in the sender and receiver accounts ") + .concat("and that those paths hold the same Vault types.") + } + post { + emit Deposited( + type: before(from.getType().identifier), + amount: before(from.balance), + to: self.owner?.address, + toUUID: self.uuid, + depositedUUID: before(from.uuid), + balanceAfter: self.balance + ) + self.balance == before(self.balance) + before(from.balance): + "FungibleToken.Vault.deposit: Cannot deposit tokens! " + .concat("The receiver's balance after the deposit (") + .concat(self.balance.toString()) + .concat(") must be the sum of the previous balance (").concat(before(self.balance.toString())) + .concat(") and the amount deposited (").concat(before(from.balance).toString()).concat(")") + } + } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + /// @return A Vault of the same type that has a balance of zero + access(all) fun createEmptyVault(): @{Vault} { + post { + result.balance == 0.0: + "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! " + .concat("The newly created Vault must have zero balance but it has a balance of ") + .concat(result.balance.toString()) + + result.getType() == self.getType(): + "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! " + .concat("The type of the new Vault <") + .concat(result.getType().identifier) + .concat("> has to be the same type as the Vault that created it <") + .concat(self.getType().identifier) + .concat(">.") + } + } + } + + /// createEmptyVault allows any user to create a new Vault that has a zero balance + /// + /// @return A Vault of the requested type that has a balance of zero + access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { + post { + result.balance == 0.0: + "FungibleToken.createEmptyVault: Empty Vault creation failed! " + .concat("The newly created Vault must have zero balance but it has a balance of (") + .concat(result.balance.toString()).concat(")") + + result.getType() == vaultType: + "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! " + .concat("The type of the new Vault <") + .concat(result.getType().identifier) + .concat("> has to be the same as the type that was requested <") + .concat(vaultType.identifier) + .concat(">.") + } + } +} \ No newline at end of file diff --git a/contracts/external/FungibleTokenMetadataViews.cdc b/contracts/external/FungibleTokenMetadataViews.cdc new file mode 100644 index 000000000..eb57f6b75 --- /dev/null +++ b/contracts/external/FungibleTokenMetadataViews.cdc @@ -0,0 +1,186 @@ +import FungibleToken from 0xf233dcee88fe0abe +import MetadataViews from 0x1d7e57aa55817448 +import ViewResolver from 0x1d7e57aa55817448 + +/// This contract implements the metadata standard proposed +/// in FLIP-1087. +/// +/// Ref: https://github.com/onflow/flips/blob/main/application/20220811-fungible-tokens-metadata.md +/// +/// Structs and resources can implement one or more +/// metadata types, called views. Each view type represents +/// a different kind of metadata. +/// +access(all) contract FungibleTokenMetadataViews { + + /// FTView wraps FTDisplay and FTVaultData, and is used to give a complete + /// picture of a Fungible Token. Most Fungible Token contracts should + /// implement this view. + /// + access(all) struct FTView { + access(all) let ftDisplay: FTDisplay? + access(all) let ftVaultData: FTVaultData? + view init( + ftDisplay: FTDisplay?, + ftVaultData: FTVaultData? + ) { + self.ftDisplay = ftDisplay + self.ftVaultData = ftVaultData + } + } + + /// Helper to get a FT view. + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A FTView struct + /// + access(all) fun getFTView(viewResolver: &{ViewResolver.Resolver}): FTView { + let maybeFTView = viewResolver.resolveView(Type()) + if let ftView = maybeFTView { + return ftView as! FTView + } + return FTView( + ftDisplay: self.getFTDisplay(viewResolver), + ftVaultData: self.getFTVaultData(viewResolver) + ) + } + + /// View to expose the information needed to showcase this FT. + /// This can be used by applications to give an overview and + /// graphics of the FT. + /// + access(all) struct FTDisplay { + /// The display name for this token. + /// + /// Example: "Flow" + /// + access(all) let name: String + + /// The abbreviated symbol for this token. + /// + /// Example: "FLOW" + access(all) let symbol: String + + /// A description the provides an overview of this token. + /// + /// Example: "The FLOW token is the native currency of the Flow network." + access(all) let description: String + + /// External link to a URL to view more information about the fungible token. + access(all) let externalURL: MetadataViews.ExternalURL + + /// One or more versions of the fungible token logo. + access(all) let logos: MetadataViews.Medias + + /// Social links to reach the fungible token's social homepages. + /// Possible keys may be "instagram", "twitter", "discord", etc. + access(all) let socials: {String: MetadataViews.ExternalURL} + + view init( + name: String, + symbol: String, + description: String, + externalURL: MetadataViews.ExternalURL, + logos: MetadataViews.Medias, + socials: {String: MetadataViews.ExternalURL} + ) { + self.name = name + self.symbol = symbol + self.description = description + self.externalURL = externalURL + self.logos = logos + self.socials = socials + } + } + + /// Helper to get FTDisplay in a way that will return a typed optional. + /// + /// @param viewResolver: A reference to the resolver resource + /// @return An optional FTDisplay struct + /// + access(all) fun getFTDisplay(_ viewResolver: &{ViewResolver.Resolver}): FTDisplay? { + if let maybeDisplayView = viewResolver.resolveView(Type()) { + if let displayView = maybeDisplayView as? FTDisplay { + return displayView + } + } + return nil + } + + /// View to expose the information needed store and interact with a FT vault. + /// This can be used by applications to setup a FT vault with proper + /// storage and public capabilities. + /// + access(all) struct FTVaultData { + /// Path in storage where this FT vault is recommended to be stored. + access(all) let storagePath: StoragePath + + /// Public path which must be linked to expose the public receiver capability. + access(all) let receiverPath: PublicPath + + /// Public path which must be linked to expose the balance and resolver public capabilities. + access(all) let metadataPath: PublicPath + + /// Type that should be linked at the `receiverPath`. This is a restricted type requiring + /// the `FungibleToken.Receiver` interface. + access(all) let receiverLinkedType: Type + + /// Type that should be linked at the `receiverPath`. This is a restricted type requiring + /// the `ViewResolver.Resolver` interfaces. + access(all) let metadataLinkedType: Type + + /// Function that allows creation of an empty FT vault that is intended + /// to store the funds. + access(all) let createEmptyVault: fun(): @{FungibleToken.Vault} + + view init( + storagePath: StoragePath, + receiverPath: PublicPath, + metadataPath: PublicPath, + receiverLinkedType: Type, + metadataLinkedType: Type, + createEmptyVaultFunction: fun(): @{FungibleToken.Vault} + ) { + pre { + receiverLinkedType.isSubtype(of: Type<&{FungibleToken.Receiver}>()): + "Receiver public type <".concat(receiverLinkedType.identifier) + .concat("> must be a subtype of <").concat(Type<&{FungibleToken.Receiver}>().identifier) + .concat(">.") + metadataLinkedType.isSubtype(of: Type<&{FungibleToken.Vault}>()): + "Metadata linked type <".concat(metadataLinkedType.identifier) + .concat("> must be a subtype of <").concat(Type<&{FungibleToken.Vault}>().identifier) + .concat(">.") + } + self.storagePath = storagePath + self.receiverPath = receiverPath + self.metadataPath = metadataPath + self.receiverLinkedType = receiverLinkedType + self.metadataLinkedType = metadataLinkedType + self.createEmptyVault = createEmptyVaultFunction + } + } + + /// Helper to get FTVaultData in a way that will return a typed Optional. + /// + /// @param viewResolver: A reference to the resolver resource + /// @return A optional FTVaultData struct + /// + access(all) fun getFTVaultData(_ viewResolver: &{ViewResolver.Resolver}): FTVaultData? { + if let view = viewResolver.resolveView(Type()) { + if let v = view as? FTVaultData { + return v + } + } + return nil + } + + /// View to expose the total supply of the Vault's token + access(all) struct TotalSupply { + access(all) let supply: UFix64 + + view init(totalSupply: UFix64) { + self.supply = totalSupply + } + } +} + \ No newline at end of file diff --git a/contracts/external/FungibleTokenSwitchboard.cdc b/contracts/external/FungibleTokenSwitchboard.cdc new file mode 100644 index 000000000..10d8ae820 --- /dev/null +++ b/contracts/external/FungibleTokenSwitchboard.cdc @@ -0,0 +1,376 @@ +import FungibleToken from 0xf233dcee88fe0abe + +/// The contract that allows an account to receive payments in multiple fungible +/// tokens using a single `{FungibleToken.Receiver}` capability. +/// This capability should ideally be stored at the +/// `FungibleTokenSwitchboard.ReceiverPublicPath = /public/GenericFTReceiver` +/// but it can be stored anywhere. +/// +access(all) contract FungibleTokenSwitchboard { + + // Storage and Public Paths + access(all) let StoragePath: StoragePath + access(all) let PublicPath: PublicPath + access(all) let ReceiverPublicPath: PublicPath + + access(all) entitlement Owner + + /// The event that is emitted when a new vault capability is added to a + /// switchboard resource. + /// + access(all) event VaultCapabilityAdded(type: Type, switchboardOwner: Address?, + capabilityOwner: Address?) + + /// The event that is emitted when a vault capability is removed from a + /// switchboard resource. + /// + access(all) event VaultCapabilityRemoved(type: Type, switchboardOwner: Address?, + capabilityOwner: Address?) + + /// The event that is emitted when a deposit can not be completed. + /// + access(all) event NotCompletedDeposit(type: Type, amount: UFix64, + switchboardOwner: Address?) + + /// The interface that enforces the method to allow anyone to check on the + /// available capabilities of a switchboard resource and also exposes the + /// deposit methods to deposit funds on it. + /// + access(all) resource interface SwitchboardPublic { + access(all) view fun getVaultTypesWithAddress(): {Type: Address} + access(all) view fun getSupportedVaultTypes(): {Type: Bool} + access(all) view fun isSupportedVaultType(type: Type): Bool + access(all) fun deposit(from: @{FungibleToken.Vault}) + access(all) fun safeDeposit(from: @{FungibleToken.Vault}): @{FungibleToken.Vault}? + access(all) view fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? + } + + /// The resource that stores the multiple fungible token receiver + /// capabilities, allowing the owner to add and remove them and anyone to + /// deposit any fungible token among the available types. + /// + access(all) resource Switchboard: FungibleToken.Receiver, SwitchboardPublic { + + /// Dictionary holding the fungible token receiver capabilities, + /// indexed by the fungible token vault type. + /// + access(contract) var receiverCapabilities: {Type: Capability<&{FungibleToken.Receiver}>} + + /// Adds a new fungible token receiver capability to the switchboard + /// resource. + /// + /// @param capability: The capability to expose a certain fungible + /// token vault deposit function through `{FungibleToken.Receiver}` that + /// will be added to the switchboard. + /// + access(Owner) fun addNewVault(capability: Capability<&{FungibleToken.Receiver}>) { + // Borrow a reference to the vault pointed to by the capability we + // want to store inside the switchboard + let vaultRef = capability.borrow() + ?? panic("FungibleTokenSwitchboard.Switchboard.addNewVault: Cannot borrow reference to vault from capability! " + .concat("Make sure that the capability path points to a Vault that has been properly initialized. ")) + + // Check if there is a previous capability for this token + if (self.receiverCapabilities[vaultRef.getType()] == nil) { + // use the vault reference type as key for storing the + // capability and then + self.receiverCapabilities[vaultRef.getType()] = capability + // emit the event that indicates that a new capability has been + // added + emit VaultCapabilityAdded(type: vaultRef.getType(), + switchboardOwner: self.owner?.address, + capabilityOwner: capability.address) + } else { + // If there was already a capability for that token, panic + panic("FungibleTokenSwitchboard.Switchboard.addNewVault: Cannot add new Vault capability! " + .concat("There is already a vault in the Switchboard for this type <") + .concat(vaultRef.getType().identifier).concat(">.")) + } + } + + /// Adds a number of new fungible token receiver capabilities by using + /// the paths where they are stored. + /// + /// @param paths: The paths where the public capabilities are stored. + /// @param address: The address of the owner of the capabilities. + /// + access(Owner) fun addNewVaultsByPath(paths: [PublicPath], address: Address) { + // Get the account where the public capabilities are stored + let owner = getAccount(address) + // For each path, get the saved capability and store it + // into the switchboard's receiver capabilities dictionary + for path in paths { + let capability = owner.capabilities.get<&{FungibleToken.Receiver}>(path) + // Borrow a reference to the vault pointed to by the capability + // we want to store inside the switchboard + // If the vault was borrowed successfully... + if let vaultRef = capability.borrow() { + // ...and if there is no previous capability added for that token + if (self.receiverCapabilities[vaultRef!.getType()] == nil) { + // Use the vault reference type as key for storing the + // capability + self.receiverCapabilities[vaultRef!.getType()] = capability + // and emit the event that indicates that a new + // capability has been added + emit VaultCapabilityAdded(type: vaultRef.getType(), + switchboardOwner: self.owner?.address, + capabilityOwner: address, + ) + } + } + } + } + + /// Adds a new fungible token receiver capability to the switchboard + /// resource specifying which `Type` of `@{FungibleToken.Vault}` can be + /// deposited to it. Use it to include in your switchboard "wrapper" + /// receivers such as a `@TokenForwarding.Forwarder`. It can also be + /// used to overwrite the type attached to a certain capability without + /// having to remove that capability first. + /// + /// @param capability: The capability to expose a certain fungible + /// token vault deposit function through `{FungibleToken.Receiver}` that + /// will be added to the switchboard. + /// + /// @param type: The type of fungible token that can be deposited to that + /// capability, rather than the `Type` from the reference borrowed from + /// said capability + /// + access(Owner) fun addNewVaultWrapper(capability: Capability<&{FungibleToken.Receiver}>, + type: Type) { + // Check if the capability is working + assert ( + capability.check(), + message: + "FungibleTokenSwitchboard.Switchboard.addNewVaultWrapper: Cannot borrow reference to a vault from the provided capability! " + .concat("Make sure that the capability path points to a Vault that has been properly initialized.") + ) + // Use the type parameter as key for the capability + self.receiverCapabilities[type] = capability + // emit the event that indicates that a new capability has been + // added + emit VaultCapabilityAdded( + type: type, + switchboardOwner: self.owner?.address, + capabilityOwner: capability.address, + ) + } + + /// Adds zero or more new fungible token receiver capabilities to the + /// switchboard resource specifying which `Type`s of `@{FungibleToken.Vault}`s + /// can be deposited to it. Use it to include in your switchboard "wrapper" + /// receivers such as a `@TokenForwarding.Forwarder`. It can also be + /// used to overwrite the types attached to certain capabilities without + /// having to remove those capabilities first. + /// + /// @param paths: The paths where the public capabilities are stored. + /// @param types: The types of the fungible token to be deposited on each path. + /// @param address: The address of the owner of the capabilities. + /// + access(Owner) fun addNewVaultWrappersByPath(paths: [PublicPath], types: [Type], + address: Address) { + // Get the account where the public capabilities are stored + let owner = getAccount(address) + // For each path, get the saved capability and store it + // into the switchboard's receiver capabilities dictionary + for i, path in paths { + let capability = owner.capabilities.get<&{FungibleToken.Receiver}>(path) + // Borrow a reference to the vault pointed to by the capability + // we want to store inside the switchboard + // If the vault was borrowed successfully... + if let vaultRef = capability.borrow() { + // Use the vault reference type as key for storing the capability + self.receiverCapabilities[types[i]] = capability + // and emit the event that indicates that a new capability has been added + emit VaultCapabilityAdded( + type: types[i], + switchboardOwner: self.owner?.address, + capabilityOwner: address, + ) + } + } + } + + /// Removes a fungible token receiver capability from the switchboard + /// resource. + /// + /// @param capability: The capability to a fungible token vault to be + /// removed from the switchboard. + /// + access(Owner) fun removeVault(capability: Capability<&{FungibleToken.Receiver}>) { + // Borrow a reference to the vault pointed to by the capability we + // want to remove from the switchboard + let vaultRef = capability.borrow() + ?? panic ("FungibleTokenSwitchboard.Switchboard.addNewVaultWrapper: Cannot borrow reference to a vault from the provided capability! " + .concat("Make sure that the capability path points to a Vault that has been properly initialized.")) + + // Use the vault reference to find the capability to remove + self.receiverCapabilities.remove(key: vaultRef.getType()) + // Emit the event that indicates that a new capability has been + // removed + emit VaultCapabilityRemoved( + type: vaultRef.getType(), + switchboardOwner: self.owner?.address, + capabilityOwner: capability.address, + ) + } + + /// Takes a fungible token vault and routes it to the proper fungible + /// token receiver capability for depositing it. + /// + /// @param from: The deposited fungible token vault resource. + /// + access(all) fun deposit(from: @{FungibleToken.Vault}) { + // Get the capability from the ones stored at the switchboard + let depositedVaultCapability = self.receiverCapabilities[from.getType()] + ?? panic ("FungibleTokenSwitchboard.Switchboard.deposit: Cannot deposit Vault! " + .concat("The deposited vault of type <").concat(from.getType().identifier) + .concat("> is not available on this Fungible Token switchboard. ") + .concat("The recipient needs to initialize their account and switchboard to hold and receive the deposited vault type.")) + + // Borrow the reference to the desired vault + let vaultRef = depositedVaultCapability.borrow() + ?? panic ("FungibleTokenSwitchboard.Switchboard.deposit: Cannot borrow reference to a vault " + .concat("from the type of the deposited Vault <").concat(from.getType().identifier) + .concat(">. Make sure that the capability path points to a Vault that has been properly initialized.")) + + vaultRef.deposit(from: <-from) + } + + /// Takes a fungible token vault and tries to route it to the proper + /// fungible token receiver capability for depositing the funds, + /// avoiding panicking if the vault is not available. + /// + /// @param vaultType: The type of the ft vault that wants to be + /// deposited. + /// + /// @return The deposited fungible token vault resource, without the + /// funds if the deposit was successful, or still containing the funds + /// if the reference to the needed vault was not found. + /// + access(all) fun safeDeposit(from: @{FungibleToken.Vault}): @{FungibleToken.Vault}? { + // Try to get the proper vault capability from the switchboard + // If the desired vault is present on the switchboard... + if let depositedVaultCapability = self.receiverCapabilities[from.getType()] { + // We try to borrow a reference to the vault from the capability + // If we can borrow a reference to the vault... + if let vaultRef = depositedVaultCapability.borrow() { + // We deposit the funds on said vault + vaultRef.deposit(from: <-from.withdraw(amount: from.balance)) + } + } + // if deposit failed for some reason + if from.balance > 0.0 { + emit NotCompletedDeposit( + type: from.getType(), + amount: from.balance, + switchboardOwner: self.owner?.address, + ) + return <-from + } + destroy from + return nil + } + + /// Checks that the capability tied to a type is valid + /// + /// @param vaultType: The type of the ft vault whose capability needs to be checked + /// + /// @return a boolean marking the capability for a type as valid or not + access(all) view fun checkReceiverByType(type: Type): Bool { + if self.receiverCapabilities[type] == nil { + return false + } + + return self.receiverCapabilities[type]!.check() + } + + /// Gets the receiver assigned to a provided vault type. + /// This is necessary because without it, it is not possible to look under the hood and see if a capability + /// is of an expected type or not. This helps guard against infinitely chained TokenForwarding or other invalid + /// malicious kinds of updates that could prevent listings from being made that are valid on storefronts. + /// + /// @param vaultType: The type of the ft vault whose capability needs to be checked + /// + /// @return an optional receiver capability for consumers of the switchboard to check/validate on their own + access(all) view fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? { + if !self.checkReceiverByType(type: type) { + return nil + } + + return self.receiverCapabilities[type]!.borrow() + } + + /// A getter function to know which tokens a certain switchboard + /// resource is prepared to receive along with the address where + /// those tokens will be deposited. + /// + /// @return A dictionary mapping the `{FungibleToken.Receiver}` + /// type to the receiver owner's address + /// + access(all) view fun getVaultTypesWithAddress(): {Type: Address} { + let effectiveTypesWithAddress: {Type: Address} = {} + // Check if each capability is live + for vaultType in self.receiverCapabilities.keys { + if self.receiverCapabilities[vaultType]!.check() { + // and attach it to the owner's address + effectiveTypesWithAddress[vaultType] = self.receiverCapabilities[vaultType]!.address + } + } + return effectiveTypesWithAddress + } + + /// A getter function that returns the token types supported by this resource, + /// which can be deposited using the 'deposit' function. + /// + /// @return Dictionary of FT types that can be deposited. + access(all) view fun getSupportedVaultTypes(): {Type: Bool} { + let supportedVaults: {Type: Bool} = {} + for receiverType in self.receiverCapabilities.keys { + if self.receiverCapabilities[receiverType]!.check() { + if receiverType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + supportedVaults[receiverType] = true + } + if receiverType.isSubtype(of: Type<@{FungibleToken.Receiver}>()) { + let receiverRef = self.receiverCapabilities[receiverType]!.borrow()! + let subReceiverSupportedTypes = receiverRef.getSupportedVaultTypes() + for subReceiverType in subReceiverSupportedTypes.keys { + if subReceiverType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { + supportedVaults[subReceiverType] = true + } + } + } + } + } + return supportedVaults + } + + /// Returns whether or not the given type is accepted by the Receiver + /// A vault that can accept any type should just return true by default + access(all) view fun isSupportedVaultType(type: Type): Bool { + let supportedVaults = self.getSupportedVaultTypes() + if let supported = supportedVaults[type] { + return supported + } else { return false } + } + + init() { + // Initialize the capabilities dictionary + self.receiverCapabilities = {} + } + + } + + /// Function that allows to create a new blank switchboard. A user must call + /// this function and store the returned resource in their storage. + /// + access(all) fun createSwitchboard(): @Switchboard { + return <-create Switchboard() + } + + init() { + self.StoragePath = /storage/fungibleTokenSwitchboard + self.PublicPath = /public/fungibleTokenSwitchboardPublic + self.ReceiverPublicPath = /public/GenericFTReceiver + } +} diff --git a/contracts/external/IFlowEVMNFTBridge.cdc b/contracts/external/IFlowEVMNFTBridge.cdc new file mode 100644 index 000000000..0dbcfd04e --- /dev/null +++ b/contracts/external/IFlowEVMNFTBridge.cdc @@ -0,0 +1,119 @@ +import FungibleToken from 0xf233dcee88fe0abe +import NonFungibleToken from 0x1d7e57aa55817448 + +import EVM from 0xe467b9dd11fa00df + +import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 +import CrossVMNFT from 0x1e4aa0b87d10b141 + +access(all) contract interface IFlowEVMNFTBridge { + + /************* + Events + **************/ + + /// Broadcasts an NFT was bridged from Cadence to EVM + access(all) + event BridgedNFTToEVM( + type: String, + id: UInt64, + uuid: UInt64, + evmID: UInt256, + to: String, + evmContractAddress: String, + bridgeAddress: Address + ) + /// Broadcasts an NFT was bridged from EVM to Cadence + access(all) + event BridgedNFTFromEVM( + type: String, + id: UInt64, + uuid: UInt64, + evmID: UInt256, + caller: String, + evmContractAddress: String, + bridgeAddress: Address + ) + + /************** + Getters + ***************/ + + /// Returns the EVM address associated with the provided type + /// + access(all) + view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? + + /// Returns the EVM address of the bridge coordinating COA + /// + access(all) + view fun getBridgeCOAEVMAddress(): EVM.EVMAddress + + /******************************** + Public Bridge Entrypoints + *********************************/ + + /// Public entrypoint to bridge NFTs from Cadence to EVM. + /// + /// @param token: The NFT to be bridged + /// @param to: The NFT recipient in FlowEVM + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun bridgeNFTToEVM( + token: @{NonFungibleToken.NFT}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + emit BridgedNFTToEVM( + type: token.getType().identifier, + id: token.id, + uuid: token.uuid, + evmID: CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id), + to: to.toString(), + evmContractAddress: self.getAssociatedEVMAddress(with: token.getType())?.toString() + ?? panic( + "Could not find EVM Contract address associated with provided NFT identifier=" + .concat(token.getType().identifier) + ), + bridgeAddress: self.account.address + ) + } + } + + /// Public entrypoint to bridge NFTs from EVM to Cadence + /// + /// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via + /// `protectedTransferCall`) is validated before the bridge request is executed. + /// @param type: The Cadence Type of the NFT to be bridged. If EVM-native, this would be the Cadence Type associated + /// with the EVM contract on the Flow side at onboarding. + /// @param id: The NFT ID to bridged + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the + /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. + /// + /// @returns The bridged NFT + /// + access(account) + fun bridgeNFTFromEVM( + owner: EVM.EVMAddress, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (EVM.EVMAddress): EVM.Result + ): @{NonFungibleToken.NFT} { + post { + emit BridgedNFTFromEVM( + type: result.getType().identifier, + id: result.id, + uuid: result.uuid, + evmID: id, + caller: owner.toString(), + evmContractAddress: self.getAssociatedEVMAddress(with: result.getType())?.toString() + ?? panic("Could not find EVM Contract address associated with provided NFT"), + bridgeAddress: self.account.address + ) + } + } +} \ No newline at end of file diff --git a/contracts/external/IFlowEVMTokenBridge.cdc b/contracts/external/IFlowEVMTokenBridge.cdc new file mode 100644 index 000000000..942eb8fea --- /dev/null +++ b/contracts/external/IFlowEVMTokenBridge.cdc @@ -0,0 +1,112 @@ +import FungibleToken from 0xf233dcee88fe0abe +import NonFungibleToken from 0x1d7e57aa55817448 + +import EVM from 0xe467b9dd11fa00df + +access(all) contract interface IFlowEVMTokenBridge { + + /************* + Events + **************/ + + /// Broadcasts fungible tokens were bridged from Cadence to EVM + access(all) + event BridgedTokensToEVM( + type: String, + amount: UFix64, + bridgedUUID: UInt64, + to: String, + evmContractAddress: String, + bridgeAddress: Address + ) + /// Broadcasts fungible tokens were bridged from EVM to Cadence + access(all) + event BridgedTokensFromEVM( + type: String, + amount: UInt256, + bridgedUUID: UInt64, + caller: String, + evmContractAddress: String, + bridgeAddress: Address + ) + + /************** + Getters + ***************/ + + /// Returns the EVM address associated with the provided type + /// + access(all) + view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? + + /// Returns the EVM address of the bridge coordinating COA + /// + access(all) + view fun getBridgeCOAEVMAddress(): EVM.EVMAddress + + /******************************** + Public Bridge Entrypoints + *********************************/ + + /// Public entrypoint to bridge fungible tokens from Cadence to EVM. + /// + /// @param token: The token Vault to be bridged + /// @param to: The token recipient in EVM + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// + access(all) + fun bridgeTokensToEVM( + vault: @{FungibleToken.Vault}, + to: EVM.EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) { + pre { + emit BridgedTokensToEVM( + type: vault.getType().identifier, + amount: vault.balance, + bridgedUUID: vault.uuid, + to: to.toString(), + evmContractAddress: self.getAssociatedEVMAddress(with: vault.getType())?.toString() + ?? panic( + "Could not find EVM Contract address associated with provided Token identifier=" + .concat(vault.getType().identifier) + ), + bridgeAddress: self.account.address + ) + } + } + + /// Public entrypoint to bridge fungible tokens from EVM to Cadence + /// + /// @param owner: The EVM address of the token owner. Current ownership and successful transfer (via + /// `protectedTransferCall`) is validated before the bridge request is executed. + /// @param type: The Cadence Type of the fungible token to be bridged. If EVM-native, this would be the Cadence + /// Type associated with the EVM contract on the Flow side at onboarding. + /// @param amount: The amount of tokens to bridge from EVM to Cadence + /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW + /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the + /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. + /// + /// @returns The bridged NFT + /// + access(account) + fun bridgeTokensFromEVM( + owner: EVM.EVMAddress, + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, + protectedTransferCall: fun (): EVM.Result + ): @{FungibleToken.Vault} { + post { + emit BridgedTokensFromEVM( + type: result.getType().identifier, + amount: amount, + bridgedUUID: result.uuid, + caller: owner.toString(), + evmContractAddress: self.getAssociatedEVMAddress(with: result.getType())?.toString() + ?? panic("Could not find EVM Contract address associated with provided Vault"), + bridgeAddress: self.account.address + ) + } + } +} \ No newline at end of file From 199572b8808a1a3b7da1139e8672950569519e41 Mon Sep 17 00:00:00 2001 From: vishal <1117327+vishalchangrani@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:02:49 -0400 Subject: [PATCH 2/2] chore: remove external contracts folder from review branch --- contracts/external/FlowEVMBridge.cdc | 1186 ------------ contracts/external/FlowEVMBridgeAccessor.cdc | 202 --- contracts/external/FlowEVMBridgeConfig.cdc | 683 ------- .../FlowEVMBridgeCustomAssociations.cdc | 212 --- .../FlowEVMBridgeHandlerInterfaces.cdc | 207 --- contracts/external/FlowEVMBridgeHandlers.cdc | 468 ----- contracts/external/FlowEVMBridgeUtils.cdc | 1600 ----------------- contracts/external/FungibleToken.cdc | 332 ---- .../external/FungibleTokenMetadataViews.cdc | 186 -- .../external/FungibleTokenSwitchboard.cdc | 376 ---- contracts/external/IFlowEVMNFTBridge.cdc | 119 -- contracts/external/IFlowEVMTokenBridge.cdc | 112 -- 12 files changed, 5683 deletions(-) delete mode 100644 contracts/external/FlowEVMBridge.cdc delete mode 100644 contracts/external/FlowEVMBridgeAccessor.cdc delete mode 100644 contracts/external/FlowEVMBridgeConfig.cdc delete mode 100644 contracts/external/FlowEVMBridgeCustomAssociations.cdc delete mode 100644 contracts/external/FlowEVMBridgeHandlerInterfaces.cdc delete mode 100644 contracts/external/FlowEVMBridgeHandlers.cdc delete mode 100644 contracts/external/FlowEVMBridgeUtils.cdc delete mode 100644 contracts/external/FungibleToken.cdc delete mode 100644 contracts/external/FungibleTokenMetadataViews.cdc delete mode 100644 contracts/external/FungibleTokenSwitchboard.cdc delete mode 100644 contracts/external/IFlowEVMNFTBridge.cdc delete mode 100644 contracts/external/IFlowEVMTokenBridge.cdc diff --git a/contracts/external/FlowEVMBridge.cdc b/contracts/external/FlowEVMBridge.cdc deleted file mode 100644 index 7a8b8287d..000000000 --- a/contracts/external/FlowEVMBridge.cdc +++ /dev/null @@ -1,1186 +0,0 @@ -import Burner from 0xf233dcee88fe0abe -import FungibleToken from 0xf233dcee88fe0abe -import FungibleTokenMetadataViews from 0xf233dcee88fe0abe -import NonFungibleToken from 0x1d7e57aa55817448 -import MetadataViews from 0x1d7e57aa55817448 -import CrossVMMetadataViews from 0x1d7e57aa55817448 -import ViewResolver from 0x1d7e57aa55817448 - -import EVM from 0xe467b9dd11fa00df - -import IBridgePermissions from 0x1e4aa0b87d10b141 -import ICrossVM from 0x1e4aa0b87d10b141 -import IEVMBridgeNFTMinter from 0x1e4aa0b87d10b141 -import IEVMBridgeTokenMinter from 0x1e4aa0b87d10b141 -import IFlowEVMNFTBridge from 0x1e4aa0b87d10b141 -import IFlowEVMTokenBridge from 0x1e4aa0b87d10b141 -import CrossVMNFT from 0x1e4aa0b87d10b141 -import CrossVMToken from 0x1e4aa0b87d10b141 -import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141 -import FlowEVMBridgeCustomAssociations from 0x1e4aa0b87d10b141 -import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 -import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141 -import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141 -import FlowEVMBridgeNFTEscrow from 0x1e4aa0b87d10b141 -import FlowEVMBridgeTokenEscrow from 0x1e4aa0b87d10b141 -import FlowEVMBridgeTemplates from 0x1e4aa0b87d10b141 -import SerializeMetadata from 0x1e4aa0b87d10b141 - -/// The FlowEVMBridge contract is the main entrypoint for bridging NFT & FT assets between Flow & FlowEVM. -/// -/// Before bridging, be sure to onboard the asset type which will configure the bridge to handle the asset. From there, -/// the asset can be bridged between VMs via the COA as the entrypoint. -/// -/// See also: -/// - Code in context: https://github.com/onflow/flow-evm-bridge -/// - FLIP #237: https://github.com/onflow/flips/pull/233 -/// -access(all) -contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge { - - /************* - Events - **************/ - - /// Emitted any time a new asset type is onboarded to the bridge - access(all) - event Onboarded(type: String, cadenceContractAddress: Address, evmContractAddress: String) - /// Denotes a defining contract was deployed to the bridge account - access(all) - event BridgeDefiningContractDeployed( - contractName: String, - assetName: String, - symbol: String, - isERC721: Bool, - evmContractAddress: String - ) - /// Emitted whenever a bridged NFT is burned as a part of the bridging process. In the context of this contract, - /// this only occurs when an EVM-native ERC721 updates from a bridged NFT to their own custom Cadence NFT. - access(all) - event BridgedNFTBurned(type: String, id: UInt64, evmID: UInt256, uuid: UInt64, erc721Address: String) - - /************************** - Public Onboarding - **************************/ - - /// Onboards a given asset by type to the bridge. Since we're onboarding by Cadence Type, the asset must be defined - /// in a third-party contract. Attempting to onboard a bridge-defined asset will result in an error as the asset has - /// already been onboarded to the bridge. - /// - /// @param type: The Cadence Type of the NFT to be onboarded - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun onboardByType(_ type: Type, feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) { - pre { - !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" - !FlowEVMBridgeConfig.isCadenceTypeBlocked(type): - "This Cadence Type ".concat(type.identifier).concat(" is currently blocked from being onboarded") - self.typeRequiresOnboarding(type) == true: "Onboarding is not needed for this type" - FlowEVMBridgeUtils.typeAllowsBridging(type): - "This Cadence Type ".concat(type.identifier).concat(" is currently opted-out of bridge onboarding") - FlowEVMBridgeUtils.isCadenceNative(type: type): "Only Cadence-native assets can be onboarded by Type" - } - /* Custom cross-VM Implementation check */ - // - // Register as a custom cross-VM implementation if detected - if FlowEVMBridgeUtils.getEVMPointerView(forType: type) != nil { - self.registerCrossVMNFT(type: type, fulfillmentMinter: nil, feeProvider: feeProvider) - return - } - - /* Provision fees */ - // - // Withdraw from feeProvider and deposit to self - FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee) - - /* EVM setup */ - // - // Deploy an EVM defining contract via the FlowBridgeFactory.sol contract - let onboardingValues = self.deployEVMContract(forAssetType: type) - - /* Cadence escrow setup */ - // - // Initialize bridge escrow for the asset based on its type - if type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) { - FlowEVMBridgeNFTEscrow.initializeEscrow( - forType: type, - name: onboardingValues.name, - symbol: onboardingValues.symbol, - erc721Address: onboardingValues.evmContractAddress - ) - } else if type.isSubtype(of: Type<@{FungibleToken.Vault}>()) { - let createVaultFunction = FlowEVMBridgeUtils.getCreateEmptyVaultFunction(forType: type) - ?? panic("Could not retrieve createEmptyVault function for the given type") - let vault <-createVaultFunction(type) - assert( - vault.getType() == type, - message: "Requested to onboard type=".concat(type.identifier).concat( "but contract returned type=").concat(vault.getType().identifier) - ) - FlowEVMBridgeTokenEscrow.initializeEscrow( - with: <-vault, - name: onboardingValues.name, - symbol: onboardingValues.symbol, - decimals: onboardingValues.decimals!, - evmTokenAddress: onboardingValues.evmContractAddress - ) - } else { - panic("Attempted to onboard unsupported type: ".concat(type.identifier)) - } - - /* Confirmation */ - // - assert( - FlowEVMBridgeNFTEscrow.isInitialized(forType: type) || FlowEVMBridgeTokenEscrow.isInitialized(forType: type), - message: "Failed to initialize escrow for given type" - ) - - emit Onboarded( - type: type.identifier, - cadenceContractAddress: FlowEVMBridgeUtils.getContractAddress(fromType: type)!, - evmContractAddress: onboardingValues.evmContractAddress.toString() - ) - } - - /// Onboards a given EVM contract to the bridge. Since we're onboarding by EVM Address, the asset must be defined in - /// a third-party EVM contract. Attempting to onboard a bridge-defined asset will result in an error as onboarding - /// is not required. - /// - /// @param address: The EVMAddress of the ERC721 or ERC20 to be onboarded - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun onboardByEVMAddress( - _ address: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" - !FlowEVMBridgeConfig.isEVMAddressBlocked(address): - "This EVM contract ".concat(address.toString()).concat(" is currently blocked from being onboarded") - } - /* Custom cross-VM Implementation check */ - // - let cadenceAddr = FlowEVMBridgeUtils.getDeclaredCadenceAddressFromCrossVM(evmContract: address) - let cadenceType = FlowEVMBridgeUtils.getDeclaredCadenceTypeFromCrossVM(evmContract: address) - // Register as a custom cross-VM implementation if detected - if cadenceAddr != nil && cadenceType != nil { - self.registerCrossVMNFT(type: cadenceType!, fulfillmentMinter: nil, feeProvider: feeProvider) - return - } - - /* Validate the EVM contract */ - // - // Ensure the project has not opted out of bridge support - assert( - FlowEVMBridgeUtils.evmAddressAllowsBridging(address), - message: "This contract is not supported as defined by the project's development team" - ) - assert( - self.evmAddressRequiresOnboarding(address) == true, - message: "Onboarding is not needed for this contract" - ) - - /* Provision fees */ - // - // Withdraw fee from feeProvider and deposit - FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee) - - /* Setup Cadence-defining contract */ - // - // Deploy a defining Cadence contract to the bridge account - self.deployDefiningContract(evmContractAddress: address) - } - - /// Registers a custom cross-VM NFT implementation, allowing projects to integrate their Cadence & EVM contracts - /// such that the VM bridge facilitates movement between VMs as the integrated implementations. - /// - /// @param type: The NFT Type to register as cross-VM NFT - /// @param fulfillmentMinter: The optional NFTFulfillmentMinter Capability. This parameter is required for - /// EVM-native NFTs - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun registerCrossVMNFT( - type: Type, - fulfillmentMinter: Capability?, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - FlowEVMBridgeUtils.typeAllowsBridging(type): - "This Cadence Type \(type.identifier) is currently opted-out of bridge onboarding" - type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): - "The provided Type \(type.identifier) is not an NFT - only NFTs can register as cross-VM" - !type.isSubtype(of: Type<@{FungibleToken.Vault}>()): - "The provided Type \(type.identifier) is also a FungibleToken Vault - only NFTs can register as cross-VM" - !FlowEVMBridgeConfig.isCadenceTypeBlocked(type): - "Type \(type.identifier) has been blocked from onboarding" - FlowEVMBridgeUtils.isCadenceNative(type: type): - "Attempting to register a bridge-deployed NFT - cannot update a bridge-defined asset. If updating your EVM " - .concat("contract's Cadence association, deploy your Cadence NFT contract and register using the newly defined Cadence type") - FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) == nil: - "A custom association has already been declared for type \(type.identifier) with EVM address " - .concat(FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)!.toString()) - .concat(". Custom associations can only be declared once for any given Cadence Type or EVM contract") - fulfillmentMinter?.check() ?? true: - "NFTFulfillmentMinter Capability is invalid - Issue a new " - .concat("Capability and try again") - fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().address! == type.address! : true: - "NFTFulfillmentMinter must be defined by a contract deployed to the registered type address \(type.address!) " - .concat(" but found defining address of \(fulfillmentMinter!.borrow()!.getType().address!)") - } - /* Provision fees */ - // - // Withdraw fee from feeProvider and deposit - FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: FlowEVMBridgeConfig.onboardFee) - - /* Get pointers from both contracts */ - // - // Get the Cadence side EVMPointer - let evmPointer = FlowEVMBridgeUtils.getEVMPointerView(forType: type) - ?? panic("The CrossVMMetadataViews.EVMPointer is not supported by the type \(type.identifier).") - // EVM contract checks - assert(!FlowEVMBridgeConfig.isEVMAddressBlocked(evmPointer.evmContractAddress), - message: "Type \(type.identifier) has been blocked from onboarding.") - assert( - FlowEVMBridgeUtils.evmAddressAllowsBridging(evmPointer.evmContractAddress), - message: "The EVM contract \(evmPointer.evmContractAddress.toString()) developers have opted out of VM bridge integration." - ) - assert( - FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmPointer.evmContractAddress) == nil, - message: "A custom association has already been declared for EVM address \(evmPointer.evmContractAddress.toString()) with Cadence Type " - .concat(FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmPointer.evmContractAddress)?.identifier ?? "") - .concat(". Custom associations can only be declared once for any given Cadence Type or EVM contract") - ) - assert( - FlowEVMBridgeUtils.isERC721(evmContractAddress: evmPointer.evmContractAddress) - && !FlowEVMBridgeUtils.isERC20(evmContractAddress: evmPointer.evmContractAddress), - message: "Cross-VM NFTs must be implemented as ERC721 exclusively, but detected an invalid EVM interface " - .concat("at EVM contract \(evmPointer.evmContractAddress.toString())") - ) - - // Get pointer on EVM side - let cadenceAddr = FlowEVMBridgeUtils.getDeclaredCadenceAddressFromCrossVM(evmContract: evmPointer.evmContractAddress) - ?? panic("Could not retrieve a Cadence address declaration from the EVM contract \(evmPointer.evmContractAddress.toString())") - let cadenceType = FlowEVMBridgeUtils.getDeclaredCadenceTypeFromCrossVM(evmContract: evmPointer.evmContractAddress) - ?? panic("Could not retrieve a Cadence Type declaration from the EVM contract \(evmPointer.evmContractAddress.toString())") - - /* Pointer validation */ - // - // Assert both point to each other - assert( - type.address == cadenceAddr, - message: "Mismatched Cadence Address pointers: \(type.address!.toString()) and \(cadenceAddr.toString())" - ) - assert( - type == cadenceType, - message: "Mismatched type pointers: \(type.identifier) and \(cadenceType.identifier)" - ) - - /* Cross-VM conformance check */ - // - // Check supportsInterface() for CrossVMBridgeERC721Fulfillment if NFT is Cadence-native - if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence { - assert(FlowEVMBridgeUtils.supportsCadenceNativeNFTEVMInterfaces(evmContract: evmPointer.evmContractAddress), - message: "Corresponding EVM contract does not implement necessary EVM interfaces ICrossVMBridgeERC721Fulfillment " - .concat("and/or ICrossVMBridgeCallable. All Cadence-native cross-VM NFTs must implement these interfaces and ") - .concat("grant the bridge COA the ability to fulfill bridge requests moving NFTs into EVM.")) - let designatedVMBridgeAddress = FlowEVMBridgeUtils.getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: evmPointer.evmContractAddress) - ?? panic("Could not recover declared VM bridge address from EVM contract \(evmPointer.evmContractAddress.toString()). " - .concat("Ensure the contract conforms to ICrossVMBridgeCallable and declare the vmBridgeAddress as \(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString())")) - assert(designatedVMBridgeAddress.equals(FlowEVMBridgeUtils.getBridgeCOAEVMAddress()), - message: "ICrossVMBridgeCallable declared \(designatedVMBridgeAddress.toString())" - .concat(" as vmBridgeAddress which must be declared as \(FlowEVMBridgeUtils.getBridgeCOAEVMAddress().toString())")) - } - - /* Native VM consistency check */ - // - // Assess if the NFT has been previously onboarded to the bridge - let legacyEVMAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) - let legacyCadenceAssoc = FlowEVMBridgeConfig.getLegacyTypeAssociated(with: evmPointer.evmContractAddress) - assert(legacyEVMAssoc == nil || legacyCadenceAssoc == nil, - message: "Both the EVM contract \(evmPointer.evmContractAddress.toString()) and the Cadence Type \(type.identifier) " - .concat("have already been onboarded to the VM bridge - one side of this association will have to be redeployed ") - .concat("and the declared association updated to a non-onboarded target in order to register as a custom cross-VM asset.")) - // Ensure the native VM is consistent if the NFT has been previously onboarded via the permissionless path - if legacyEVMAssoc != nil { - assert(evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence, - message: "Attempting to register NFT \(type.identifier) as EVM-native after it has already been " - .concat("onboarded as Cadence-native. This NFT must be configured as Cadence-native with an ERC721 ") - .concat("implementing CrossVMBridgeERC721Fulfillment base contract allowing the bridge to fulfill ") - .concat("NFTs moving into EVM")) - } else if legacyCadenceAssoc != nil { - assert(evmPointer.nativeVM == CrossVMMetadataViews.VM.EVM, - message: "Attempting to register NFT \(type.identifier) as Cadence-native after it has already been " - .concat("onboarded as EVM-native. This NFT must be configured as EVM-native and provide an NFTFulfillmentMinter ") - .concat("Capability so the bridge may fulfill NFTs moving into Cadence.")) - } - - FlowEVMBridgeCustomAssociations.saveCustomAssociation( - type: type, - evmContractAddress: evmPointer.evmContractAddress, - nativeVM: evmPointer.nativeVM, - updatedFromBridged: legacyEVMAssoc != nil || legacyCadenceAssoc != nil, - fulfillmentMinter: fulfillmentMinter - ) - - if !FlowEVMBridgeNFTEscrow.isInitialized(forType: type) { - let name = FlowEVMBridgeUtils.getName(evmContractAddress: evmPointer.evmContractAddress) - let symbol = FlowEVMBridgeUtils.getSymbol(evmContractAddress: evmPointer.evmContractAddress) - FlowEVMBridgeNFTEscrow.initializeEscrow( - forType: type, - name: name, - symbol: symbol, - erc721Address: evmPointer.evmContractAddress - ) - } - } - - /************************* - NFT Handling - **************************/ - - /// Public entrypoint to bridge NFTs from Cadence to EVM as ERC721. - /// - /// @param token: The NFT to be bridged - /// @param to: The NFT recipient in FlowEVM - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun bridgeNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" - !token.isInstance(Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported" - self.typeRequiresOnboarding(token.getType()) == false: "NFT must first be onboarded" - FlowEVMBridgeConfig.isTypePaused(token.getType()) == false: "Bridging is currently paused for this NFT" - } - let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: token.getType()) - let customAssocByType = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: token.getType()) - let customAssocByEVMAddr = bridgedAssoc != nil ? FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssoc!) : nil - if bridgedAssoc != nil && customAssocByType == nil && customAssocByEVMAddr == nil { - // Common case - bridge-defined counterpart in non-native VM - return self.handleDefaultNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) - } else if customAssocByType != nil && customAssocByEVMAddr == nil { - // NFT is registered as cross-VM - return self.handleCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) - } else if customAssocByType == nil && customAssocByEVMAddr != nil { - // Dealing with a bridge-defined NFT after a custom association has been configured - return self.handleUpdatedBridgedNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) - } - // customAssocByType != nil && customAssocByEVMAddr != nil - panic("Unknown error encountered bridging NFT \(token.getType().identifier) with ID \(token.id) to EVM recipient \(to.toString())") - } - - /// Entrypoint to bridge ERC721 from EVM to Cadence as NonFungibleToken.NFT - /// - /// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via - /// `protectedTransferCall`) is validated before the bridge request is executed. - /// @param calldata: Caller-provided approve() call, enabling contract COA to operate on NFT in EVM contract - /// @param id: The NFT ID to bridged - /// @param evmContractAddress: Address of the EVM address defining the NFT being bridged - also call target - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the - /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. - /// - /// @returns The bridged NFT - /// - access(account) - fun bridgeNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - pre { - !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" - !type.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Mixed asset types are not yet supported" - self.typeRequiresOnboarding(type) == false: "NFT must first be onboarded" - FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this NFT" - } - let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) - let customAssocByType = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) - let customAssocByEVMAddr = bridgedAssoc != nil ? FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssoc!) : nil - // Initialize the internal handler method that will be used to move the NFT from EVM - var handler: (fun (EVM.EVMAddress, Type, UInt256, auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, fun (EVM.EVMAddress): EVM.Result): @{NonFungibleToken.NFT})? = nil - if bridgedAssoc != nil && customAssocByType == nil && customAssocByEVMAddr == nil { - // Common case - bridge-defined counterpart in non-native VM - handler = self.handleDefaultNFTFromEVM - } else if customAssocByType != nil && customAssocByEVMAddr == nil { - // NFT is registered as cross-VM - handler = self.handleCrossVMNFTFromEVM - } else if customAssocByType == nil && customAssocByEVMAddr != nil { - // Dealing with a bridge-defined NFT after a custom association has been configured - handler = self.handleUpdatedBridgedNFTFromEVM - } else { - // customAssocByType != nil && customAssocByEVMAddr != nil - panic("Unknown error encountered bridging NFT \(type.identifier) with ID \(id) from EVM owner \(owner.toString())") - } - // Return the bridged NFT, using the appropriate handler - return <- handler!(owner: owner, type: type, id: id, feeProvider: feeProvider, protectedTransferCall: protectedTransferCall) - } - - /************************** - FT Handling - ***************************/ - - /// Public entrypoint to bridge FTs from Cadence to EVM as ERC20 tokens. - /// - /// @param vault: The fungible token Vault to be bridged - /// @param to: The fungible token recipient in EVM - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun bridgeTokensToEVM( - vault: @{FungibleToken.Vault}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" - !vault.isInstance(Type<@{NonFungibleToken.NFT}>()): "Mixed asset types are not yet supported" - self.typeRequiresOnboarding(vault.getType()) == false: "FungibleToken must first be onboarded" - FlowEVMBridgeConfig.isTypePaused(vault.getType()) == false: "Bridging is currently paused for this token" - } - /* Handle $FLOW requests via EVM interface & return */ - // - let vaultType = vault.getType() - - // Gather the vault balance before acting on the resource - let vaultBalance = vault.balance - // Initialize fee amount to 0.0 and assign as appropriate for how the token is handled - var feeAmount = 0.0 - - /* TokenHandler coverage */ - // - // Some tokens pre-dating bridge require special case handling - borrow handler and passthrough to fulfill - if FlowEVMBridgeConfig.typeHasTokenHandler(vaultType) { - let handler = FlowEVMBridgeConfig.borrowTokenHandler(vaultType) - ?? panic("Could not retrieve handler for the given type") - handler.fulfillTokensToEVM(tokens: <-vault, to: to) - - // Here we assume burning Vault in Cadence which doesn't require storage consumption - feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) - FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount) - return - } - - /* Escrow or burn tokens depending on native environment */ - // - // In most all other cases, if Cadence-native then tokens must be escrowed - if FlowEVMBridgeUtils.isCadenceNative(type: vaultType) { - // Lock the FT balance & calculate the extra used by the FT if any - let storageUsed = FlowEVMBridgeTokenEscrow.lockTokens(<-vault) - // Calculate the bridge fee on current rates - feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: storageUsed) - } else { - // Since not Cadence-native, bridge defines the token - burn the vault and calculate the fee - Burner.burn(<-vault) - feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) - } - - /* Provision fees */ - // - // Withdraw fee amount from feeProvider and deposit - FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount) - - /* Gather identifying information */ - // - // Does the bridge control the EVM contract associated with this type? - let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: vaultType) - ?? panic("No EVMAddress found for vault type") - // Convert the vault balance to a UInt256 - let bridgeAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount( - vaultBalance, - erc20Address: associatedAddress - ) - assert(bridgeAmount > UInt256(0), message: "Amount to bridge must be greater than 0") - - // Determine if the EVM contract is bridge-owned - affects how tokens are transmitted to recipient - let isFactoryDeployed = FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress) - - /* Transmit tokens to recipient */ - // - // Mint or transfer based on the bridge's EVM contract authority, making needed state assertions to confirm - if isFactoryDeployed { - FlowEVMBridgeUtils.mustMintERC20(to: to, amount: bridgeAmount, erc20Address: associatedAddress) - } else { - FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: bridgeAmount, erc20Address: associatedAddress) - } - } - - /// Entrypoint to bridge ERC20 tokens from EVM to Cadence as FungibleToken Vaults - /// - /// @param owner: The EVM address of the FT owner. Current ownership and successful transfer (via - /// `protectedTransferCall`) is validated before the bridge request is executed. - /// @param calldata: Caller-provided approve() call, enabling contract COA to operate on FT in EVM contract - /// @param amount: The amount of tokens to be bridged - /// @param evmContractAddress: Address of the EVM address defining the FT being bridged - also call target - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// @param protectedTransferCall: A function that executes the transfer of the FT from the named owner to the - /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. - /// - /// @returns The bridged fungible token Vault - /// - access(account) - fun bridgeTokensFromEVM( - owner: EVM.EVMAddress, - type: Type, - amount: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (): EVM.Result - ): @{FungibleToken.Vault} { - pre { - !FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused" - !type.isSubtype(of: Type<@{NonFungibleToken.Collection}>()): "Mixed asset types are not yet supported" - self.typeRequiresOnboarding(type) == false: "FungibleToken must first be onboarded" - FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this token" - } - /* Provision fees */ - // - // Withdraw from feeProvider and deposit to self - let feeAmount = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 0) - FlowEVMBridgeUtils.depositFee(feeProvider, feeAmount: feeAmount) - - /* TokenHandler case coverage */ - // - // Some tokens pre-dating bridge require special case handling. If such a case, fulfill via the related handler - if FlowEVMBridgeConfig.typeHasTokenHandler(type) { - // - borrow handler and passthrough to fulfill - let handler = FlowEVMBridgeConfig.borrowTokenHandler(type) - ?? panic("Could not retrieve handler for the given type") - return <-handler.fulfillTokensFromEVM( - owner: owner, - type: type, - amount: amount, - protectedTransferCall: protectedTransferCall - ) - } - - /* Gather identifying information */ - // - // Get the EVMAddress of the ERC20 contract associated with the type - let associatedAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) - ?? panic("No EVMAddress found for token type") - // Find the Cadence defining address and contract name - let definingAddress = FlowEVMBridgeUtils.getContractAddress(fromType: type)! - let definingContractName = FlowEVMBridgeUtils.getContractName(fromType: type)! - // Convert the amount to a ufix64 so the amount can be settled on the Cadence side - let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount(amount, erc20Address: associatedAddress) - assert(ufixAmount > 0.0, message: "Amount to bridge must be greater than 0") - - /* Execute the transfer call and make needed state assertions */ - // - FlowEVMBridgeUtils.mustEscrowERC20( - owner: owner, - amount: amount, - erc20Address: associatedAddress, - protectedTransferCall: protectedTransferCall - ) - - /* Bridge-defined tokens are minted in Cadence */ - // - // If the Cadence Vault is bridge-defined, mint the tokens - if definingAddress == self.account.address { - let minter = getAccount(definingAddress).contracts.borrow<&{IEVMBridgeTokenMinter}>(name: definingContractName)! - return <- minter.mintTokens(amount: ufixAmount) - } - - /* Cadence-native tokens are withdrawn from escrow */ - // - // Confirm the EVM defining contract is bridge-owned before burning tokens - assert( - FlowEVMBridgeUtils.isEVMContractBridgeOwned(evmContractAddress: associatedAddress), - message: "Unexpected error bridging FT from EVM" - ) - // Burn the EVM tokens that have now been transferred to the bridge in EVM - let burnResult: EVM.Result = FlowEVMBridgeUtils.call( - signature: "burn(uint256)", - targetEVMAddress: associatedAddress, - args: [amount], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(burnResult.status == EVM.Status.successful, message: "Burn of EVM tokens failed") - - // Unlock from escrow and return - return <-FlowEVMBridgeTokenEscrow.unlockTokens(type: type, amount: ufixAmount) - } - - /************************** - Public Getters - **************************/ - - /// Returns the EVM address associated with the provided type - /// - access(all) - view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? { - return FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) - } - - /// Retrieves the bridge contract's COA EVMAddress - /// - /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM - /// - access(all) - view fun getBridgeCOAEVMAddress(): EVM.EVMAddress { - return FlowEVMBridgeUtils.borrowCOA().address() - } - - /// Returns whether an asset needs to be onboarded to the bridge - /// - /// @param type: The Cadence Type of the asset - /// - /// @returns Whether the asset needs to be onboarded - /// - access(all) - view fun typeRequiresOnboarding(_ type: Type): Bool? { - if !FlowEVMBridgeUtils.isValidCadenceAsset(type: type) { - return nil - } - return FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) == nil && - !FlowEVMBridgeConfig.typeHasTokenHandler(type) - } - - /// Returns whether an EVM-native asset needs to be onboarded to the bridge - /// - /// @param address: The EVMAddress of the asset - /// - /// @returns Whether the asset needs to be onboarded, nil if the defined asset is not supported by this bridge - /// - access(all) - fun evmAddressRequiresOnboarding(_ address: EVM.EVMAddress): Bool? { - // See if the bridge already has a known type associated with the given address - if FlowEVMBridgeConfig.getTypeAssociated(with: address) != nil { - return false - } - // Dealing with EVM-native asset, check if it's NFT or FT exclusively - if FlowEVMBridgeUtils.isValidEVMAsset(evmContractAddress: address) { - return true - } - // Not onboarded and not a valid asset, so return nil - return nil - } - - /************************** - Internal Helpers - ***************************/ - - /// Deploys templated EVM contract via Solidity Factory contract supporting bridging of a given asset type - /// - /// @param forAssetType: The Cadence Type of the asset - /// - /// @returns The EVMAddress of the deployed contract - /// - access(self) - fun deployEVMContract(forAssetType: Type): FlowEVMBridgeUtils.EVMOnboardingValues { - pre { - FlowEVMBridgeUtils.isValidCadenceAsset(type: forAssetType): - "Asset type is not supported by the bridge" - } - let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) - - let onboardingValues = FlowEVMBridgeUtils.getCadenceOnboardingValues(forAssetType: forAssetType) - - let deployedContractAddress = FlowEVMBridgeUtils.mustDeployEVMContract( - name: onboardingValues.name, - symbol: onboardingValues.symbol, - cadenceAddress: onboardingValues.contractAddress, - flowIdentifier: onboardingValues.identifier, - contractURI: onboardingValues.contractURI, - isERC721: isNFT - ) - - // Associate the deployed contract with the given type & return the deployed address - FlowEVMBridgeConfig.associateType(forAssetType, with: deployedContractAddress) - return FlowEVMBridgeUtils.EVMOnboardingValues( - evmContractAddress: deployedContractAddress, - name: onboardingValues.name, - symbol: onboardingValues.symbol, - decimals: isNFT ? nil : FlowEVMBridgeConfig.defaultDecimals, - contractURI: onboardingValues.contractURI, - cadenceContractName: FlowEVMBridgeUtils.getContractName(fromType: forAssetType)!, - isERC721: isNFT - ) - } - - /// Helper for deploying templated defining contract supporting EVM-native asset bridging to Cadence - /// Deploys either NFT or FT contract depending on the provided type - /// - /// @param evmContractAddress: The EVMAddress currently defining the asset to be bridged - /// - access(self) - fun deployDefiningContract(evmContractAddress: EVM.EVMAddress) { - // Gather identifying information about the EVM contract - let evmOnboardingValues = FlowEVMBridgeUtils.getEVMOnboardingValues(evmContractAddress: evmContractAddress) - - // Get Cadence code from template & deploy to the bridge account - let cadenceCode: [UInt8] = FlowEVMBridgeTemplates.getBridgedAssetContractCode( - evmOnboardingValues.cadenceContractName, - isERC721: evmOnboardingValues.isERC721 - ) ?? panic("Problem retrieving code for Cadence-defining contract") - if evmOnboardingValues.isERC721 { - self.account.contracts.add( - name: evmOnboardingValues.cadenceContractName, - code: cadenceCode, - evmOnboardingValues.name, - evmOnboardingValues.symbol, - evmContractAddress, - evmOnboardingValues.contractURI - ) - } else { - self.account.contracts.add( - name: evmOnboardingValues.cadenceContractName, - code: cadenceCode, - evmOnboardingValues.name, - evmOnboardingValues.symbol, - evmOnboardingValues.decimals!, - evmContractAddress, evmOnboardingValues.contractURI - ) - } - - emit BridgeDefiningContractDeployed( - contractName: evmOnboardingValues.cadenceContractName, - assetName: evmOnboardingValues.name, - symbol: evmOnboardingValues.symbol, - isERC721: evmOnboardingValues.isERC721, - evmContractAddress: evmContractAddress.toString() - ) - } - - /// Escrows the provided NFT and withdraws the bridging fee on the basis of a base fee + storage fee - /// - access(self) - fun escrowNFTAndWithdrawFee( - token: @{NonFungibleToken.NFT}, - from: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - // 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(from, feeAmount: feeAmount) - } - - /// Handle permissionlessly onboarded NFTs where the bridge deployed and manages the non-native contract - /// - access(self) - fun handleDefaultNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - /* Gather identifying information */ - // - let tokenType = token.getType() - let tokenID = token.id - let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id) - - /* Metadata assignment */ - // - // Grab the URI from the NFT if available - var uri: String = "" - var symbol: String = "" - // Default to project-specified URI - if let metadata = token.resolveView(Type()) as! MetadataViews.EVMBridgedMetadata? { - uri = metadata.uri.uri() - symbol = metadata.symbol - } else { - // Otherwise, serialize the NFT - uri = SerializeMetadata.serializeNFTMetadataAsURI(&token as &{NonFungibleToken.NFT}) - } - - /* Secure NFT in escrow & deposit calculated fees */ - // - // Withdraw fee from feeProvider and deposit - self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider) - - /* 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) - - /* Third-party controlled ERC721 handling */ - // - // Not bridge-controlled, transfer existing ownership - if !isFactoryDeployed { - FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID) - return - } - - /* 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 & update the URI to reflect current metadata - FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: associatedAddress, to: to, id: evmID) - FlowEVMBridgeUtils.mustUpdateTokenURI(erc721Address: associatedAddress, id: evmID, uri: uri) - } else { - // Otherwise mint with current URI - FlowEVMBridgeUtils.mustSafeMintERC721(erc721Address: associatedAddress, to: to, id: evmID, uri: uri) - } - // Update the bridged ERC721 symbol if different than the Cadence-defined EVMBridgedMetadata.symbol - if symbol.length > 0 && symbol != FlowEVMBridgeUtils.getSymbol(evmContractAddress: associatedAddress) { - FlowEVMBridgeUtils.tryUpdateSymbol(associatedAddress, symbol: symbol) - } - - } - - /// Handler to move registered cross-VM NFTs to EVM - /// - access(self) - fun handleCrossVMNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) { - let evmPointer = FlowEVMBridgeCustomAssociations.getEVMPointerAsRegistered(forType: token.getType()) - ?? panic("Could not find custom association for cross-VM NFT \(token.getType().identifier) with id \(token.id). " - .concat("Ensure this NFT has been registered as a cross-VM.")) - return evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence ? - self.handleCadenceNativeCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) : - self.handleEVMNativeCrossVMNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) - } - - /// Handler to move registered cross-VM Cadence-native NFTs to EVM - /// - access(self) - fun handleCadenceNativeCrossVMNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - let type = token.getType() - let id = UInt256(token.id) - - // Check on permissionlessly onboarded association & bridged token existence - if let bridgedERC721 = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) { - // Burn bridged ERC721 if exists - will be replaced by custom ERC721 implementation - if FlowEVMBridgeUtils.erc721Exists(erc721Address: bridgedERC721, id: id) { - FlowEVMBridgeUtils.mustBurnERC721(erc721Address: bridgedERC721, id: id) - } - } - // Make ICrossVMBridgeERC721Fulfillment.fulfillToEVM call, passing any metadata resolved by the NFT allowing - // the ERC721 implementation to update metadata if needed. The base CrossVMBridgeERC721Fulfillment contract - // checks for existence and mints if needed or transfers from vm bridge escrow, following a mint/escrow - // pattern. - let customERC721 = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type)! - let data = CrossVMMetadataViews.getEVMBytesMetadata(&token as &{ViewResolver.Resolver}) - FlowEVMBridgeUtils.mustFulfillNFTToEVM(erc721Address: customERC721, to: to, id: id, maybeBytes: data?.bytes) - - // Escrow the NFT & charge the bridge fee - self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider) - } - - /// Handler to move cross-VM EVM-native NFTs to EVM - /// - access(self) - fun handleEVMNativeCrossVMNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - if !FlowEVMBridgeUtils.isCadenceNative(type: token.getType()) { - // Bridge-defined token means this is a bridged token - passthrough to appropriate handler method - return self.handleUpdatedBridgedNFTToEVM(token: <-token, to: to, feeProvider: feeProvider) - } - let type = token.getType() - let id = UInt256(token.id) - let customERC721 = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: token.getType())! - - // Escrow the NFT & charge the bridge fee - self.escrowNFTAndWithdrawFee(token: <-token, from: feeProvider) - - // Transfer the ERC721 from escrow to the named recipient - FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: customERC721, to: to, id: id) - } - - /// Handler to move NFTs to EVM that were once bridge-defined but were later updated to a registered custom - /// cross-VM implementation - /// - access(self) - fun handleUpdatedBridgedNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - !FlowEVMBridgeUtils.isCadenceNative(type: token.getType()): - "Expected a bridge-defined NFT but was provided NFT of type \(token.getType().identifier)" - } - let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: token.getType())! - let updatedCadenceAssociation = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: bridgedAssociation) - ?? panic("Could not find a custom cross-VM association for NFT \(token.getType().identifier) #\(token.id). " - .concat("The handleUpdatedBridgedNFTToEVM route is intended for bridged Cadence NFTs associated with ") - .concat(" ERC721 contracts that have registered as a custom cross-VM NFT collection.")) - let tokenRef = (&token as &{NonFungibleToken.NFT}) as! &{CrossVMNFT.EVMNFT} - let evmID = tokenRef.evmID - let bridgedToken <- token as! @{CrossVMNFT.EVMNFT} - emit BridgedNFTBurned( - type: bridgedToken.getType().identifier, - id: bridgedToken.id, - evmID: bridgedToken.evmID, - uuid: bridgedToken.uuid, - erc721Address: bridgedAssociation.toString() - ) - Burner.burn(<-bridgedToken) - // Transfer the ERC721 from escrow to the named recipient - FlowEVMBridgeUtils.mustSafeTransferERC721(erc721Address: bridgedAssociation, to: to, id: evmID) - } - - /// Handle permissionlessly onboarded NFTs where the bridge deployed and manages the non-native contract - /// - access(self) - fun handleDefaultNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - /* 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") - // Execute the transfer call and make needed state assertions to confirm escrow from named owner - FlowEVMBridgeUtils.mustEscrowERC721( - owner: owner, - id: id, - erc721Address: associatedAddress, - protectedTransferCall: protectedTransferCall - ) - - /* 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) - - // If the NFT is bridge-defined, update the URI from the source ERC721 contract - if self.account.address == FlowEVMBridgeUtils.getContractAddress(fromType: type) { - nftContract!.updateTokenURI(evmID: id, newURI: uri) - } - - return <-nft - } - - /* 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 - } - - /// Handler to move registered cross-VM NFTs from EVM - /// - access(self) - fun handleCrossVMNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - let evmPointer = FlowEVMBridgeCustomAssociations.getEVMPointerAsRegistered(forType: type) - ?? panic("Could not find custom association for cross-VM NFT \(type.identifier) with id \(id). " - .concat("Ensure this NFT has been registered as a cross-VM.")) - if evmPointer.nativeVM == CrossVMMetadataViews.VM.Cadence { - return <- self.handleCadenceNativeCrossVMNFTFromEVM( - owner: owner, - type: type, - id: id, - feeProvider: feeProvider, - protectedTransferCall: protectedTransferCall - ) - } else { // EVM-native case as there are only two possible VMs - return <- self.handleEVMNativeCrossVMNFTFromEVM( - owner: owner, - type: type, - id: id, - feeProvider: feeProvider, - protectedTransferCall: protectedTransferCall - ) - } - } - - /// Handler to move registered Cadence-native cross-VM NFTs from EVM - /// - access(self) - fun handleCadenceNativeCrossVMNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - pre { - FlowEVMBridgeUtils.isCadenceNative(type: type): - "Attempting to move bridge-defined NFT type \(type.identifier) from EVM as Cadence-native via handleCadenceNativeCrossVMNFTFromEVM" - } - let configInfo = FlowEVMBridgeCustomAssociations.getCustomConfigInfo(forType: type)! - let customERC721 = configInfo.evmPointer.evmContractAddress - let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) - let bridgedTokenExists = bridgedAssociation != nil ? FlowEVMBridgeUtils.erc721Exists(erc721Address: bridgedAssociation!, id: id) : false - if configInfo.updatedFromBridged && bridgedTokenExists { - let bridgedTokenOwner = FlowEVMBridgeUtils.ownerOf(id: id, evmContractAddress: bridgedAssociation!)! - if bridgedTokenOwner.equals(owner) { - FlowEVMBridgeUtils.mustEscrowERC721( - owner: owner, - id: id, - erc721Address: bridgedAssociation!, - protectedTransferCall: protectedTransferCall - ) - } else if bridgedTokenOwner.equals(customERC721) { - // Bridged token owned by custom ERC721 - treat as OpenZeppelin's ERC721Wrapper, escrow & unwrap - FlowEVMBridgeUtils.mustEscrowERC721( - owner: owner, - id: id, - erc721Address: customERC721, - protectedTransferCall: protectedTransferCall - ) - FlowEVMBridgeUtils.mustUnwrapERC721( - id: id, - erc721WrapperAddress: customERC721, - underlyingEVMAddress: bridgedAssociation! - ) - } else { - // Bridged token not wrapped nor owned by caller - could not determine owner - panic("Bridged ERC721 \(bridgedAssociation!.toString()) ID \(id) still exists after \(type.identifier) " - .concat("was updated to associate with ERC721 \(customERC721.toString()), but the bridged token is ") - .concat("neither wrapped nor owned by caller \(owner.toString()). Could not determine owner.")) - } - // Burn the bridged ERC721, taking the bridged representation out of circulation in favor of custom ERC721 - FlowEVMBridgeUtils.mustBurnERC721(erc721Address: bridgedAssociation!, id: id) - } else { - FlowEVMBridgeUtils.mustEscrowERC721( - owner: owner, - id: id, - erc721Address: customERC721, - protectedTransferCall: protectedTransferCall - ) - } - // Cadence-native NFTs must be in escrow, so unlock & return - return <-FlowEVMBridgeNFTEscrow.unlockNFT( - type: type, - id: FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id)! - ) - } - - /// Handler to move registered cross-VM EVM-native NFTs from EVM - /// - access(self) - fun handleEVMNativeCrossVMNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - pre { - id <= UInt256(UInt64.max): - "NFT ID \(id) is greater than the maximum Cadence ID \(UInt64.max) - cannot fulfill this NFT from EVM" - } - var _type = type - let erc721Address = FlowEVMBridgeConfig.getEVMAddressAssociated(with: type)! - - // Burn if NFT is found to be bridge-defined as it's to be replaced by the registered custom cross-VM NFT - if !FlowEVMBridgeUtils.isCadenceNative(type: type) { - // Find and assign the updated custom Cadence NFT Type associated with the EVM-native ERC721 - _type = FlowEVMBridgeConfig.getTypeAssociated(with: erc721Address)! - - // Burn the bridged NFT token if it's locked - if let cadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) { - let bridgedToken <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: cadenceID) as! @{CrossVMNFT.EVMNFT} - emit BridgedNFTBurned( - type: bridgedToken.getType().identifier, - id: bridgedToken.id, - evmID: bridgedToken.evmID, - uuid: bridgedToken.uuid, - erc721Address: erc721Address.toString() - ) - Burner.burn(<-bridgedToken) - } - } - - FlowEVMBridgeUtils.mustEscrowERC721(owner: owner, id: id, erc721Address: erc721Address, protectedTransferCall: protectedTransferCall) - - if FlowEVMBridgeNFTEscrow.isLocked(type: type, id: UInt64(id)) { - // Unlock the NFT from escrow - return <-FlowEVMBridgeNFTEscrow.unlockNFT(type: _type, id: UInt64(id)) - } else { - // Otherwise, fulfill via configured NFTFulfillmentMinter - return <- FlowEVMBridgeCustomAssociations.fulfillNFTFromEVM(forType: _type, id: id) - } - } - - access(self) - fun handleUpdatedBridgedNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - pre { - !FlowEVMBridgeUtils.isCadenceNative(type: type): // expect this type to be bridge-defined - "Expected a bridge-defined NFT but was provided NFT of type \(type.identifier)" - id < UInt256(UInt64.max): - "Requested ID \(id) exceeds UIn64.max - Cross-VM NFT IDs must be within UInt64 range across Cadence & EVM implementations" - } - // Assign the legacy and custom associations - let bridgedAssoc = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type)! - let updatedTypeAssoc = FlowEVMBridgeConfig.getTypeAssociated(with: bridgedAssoc)! - - // Confirm custom association is EVM-native - let configInfo = FlowEVMBridgeCustomAssociations.getCustomConfigInfo(forType: updatedTypeAssoc)! - assert(configInfo.evmPointer.nativeVM == CrossVMMetadataViews.VM.EVM, - message: "Expected native VM for ERC721 \(bridgedAssoc.toString()) associated with NFT type \(type.identifier) to be EVM-native") - - FlowEVMBridgeUtils.mustEscrowERC721(owner: owner, id: id, erc721Address: bridgedAssoc, protectedTransferCall: protectedTransferCall) - - // Check if originally associated bridged token is in escrow, burning if so - if let lockedCadenceID = FlowEVMBridgeNFTEscrow.getLockedCadenceID(type: type, evmID: id) { - let bridgedToken <- FlowEVMBridgeNFTEscrow.unlockNFT(type: type, id: lockedCadenceID) as! @{CrossVMNFT.EVMNFT} - emit BridgedNFTBurned( - type: bridgedToken.getType().identifier, - id: bridgedToken.id, - evmID: bridgedToken.evmID, - uuid: bridgedToken.uuid, - erc721Address: bridgedAssoc.toString() - ) - Burner.burn(<-bridgedToken) - } - // Either unlock if locked or fulfill via configured NFTFulfillmentMinter - if FlowEVMBridgeNFTEscrow.isLocked(type: updatedTypeAssoc, id: UInt64(id)) { - return <- FlowEVMBridgeNFTEscrow.unlockNFT(type: updatedTypeAssoc, id: UInt64(id)) - } else { - return <- FlowEVMBridgeCustomAssociations.fulfillNFTFromEVM(forType: updatedTypeAssoc, id: id) - } - } -} diff --git a/contracts/external/FlowEVMBridgeAccessor.cdc b/contracts/external/FlowEVMBridgeAccessor.cdc deleted file mode 100644 index 7532bd76d..000000000 --- a/contracts/external/FlowEVMBridgeAccessor.cdc +++ /dev/null @@ -1,202 +0,0 @@ -import NonFungibleToken from 0x1d7e57aa55817448 -import FungibleToken from 0xf233dcee88fe0abe -import FlowToken from 0x1654653399040a61 - -import EVM from 0xe467b9dd11fa00df - -import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 -import FlowEVMBridge from 0x1e4aa0b87d10b141 - -/// This contract defines a mechanism for routing bridge requests from the EVM contract to the Flow-EVM bridge contract -/// -access(all) -contract FlowEVMBridgeAccessor { - - access(all) let StoragePath: StoragePath - - /// BridgeAccessor implementation used by the EVM contract to route bridge calls from COA resources - /// - access(all) - resource BridgeAccessor : EVM.BridgeAccessor { - - /// Passes along the bridge request to dedicated bridge contract - /// - /// @param nft: The NFT to be bridged to EVM - /// @param to: The address of the EVM account to receive the bridged NFT - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(EVM.Bridge) - fun depositNFT( - nft: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - FlowEVMBridge.bridgeNFTToEVM(token: <-nft, to: to, feeProvider: feeProvider) - } - - /// Passes along the bridge request to the dedicated bridge contract, returning the bridged NFT - /// - /// @param caller: A reference to the COA which currently owns the NFT in EVM - /// @param type: The Cadence type of the NFT to be bridged from EVM - /// @param id: The ID of the NFT to be bridged from EVM - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - /// @return The bridged NFT - /// - access(EVM.Bridge) - fun withdrawNFT( - caller: auth(EVM.Call) &EVM.CadenceOwnedAccount, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ): @{NonFungibleToken.NFT} { - // Define a callback function, enabling the bridge to act on the ephemeral COA reference in scope - var executed = false - fun callback(target: EVM.EVMAddress): EVM.Result { - pre { - !executed: "Callback can only be executed once" - FlowEVMBridge.getAssociatedEVMAddress(with: type) ?? FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) != nil: - "Could not find EVM association for NFT Type \(type.identifier) - ensure the NFT has been onboarded to the bridge & try again" - } - post { - executed: "Callback must be executed" - } - // Ensure the call is to an EVM contract known to be associated with the NFT Type as registered with - // the VM Bridge - let callAllowed = FlowEVMBridgeAccessor.isValidEVMTarget(forType: type, target: target) - assert(callAllowed, - message: "Target EVM contract \(target.toString()) is not association with NFT Type \(type.identifier) - COA `safeTransferFrom` callback rejected") - - executed = true - return caller.call( - to: target, - data: EVM.encodeABIWithSignature( - "safeTransferFrom(address,address,uint256)", - [caller.address(), FlowEVMBridge.getBridgeCOAEVMAddress(), id] - ), - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: EVM.Balance(attoflow: 0) - ) - } - // Execute the bridge request - return <- FlowEVMBridge.bridgeNFTFromEVM( - owner: caller.address(), - type: type, - id: id, - feeProvider: feeProvider, - protectedTransferCall: callback - ) - } - - /// Passes along the bridge request to dedicated bridge contract - /// - /// @param vault: The fungible token vault to be bridged to EVM - /// @param to: The address of the EVM account to receive the bridged tokens - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(EVM.Bridge) - fun depositTokens( - vault: @{FungibleToken.Vault}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - FlowEVMBridge.bridgeTokensToEVM(vault: <-vault, to: to, feeProvider: feeProvider) - } - - /// Passes along the bridge request to the dedicated bridge contract, returning the bridged FungibleToken - /// - /// @param caller: A reference to the COA which currently owns the tokens in EVM - /// @param type: The Cadence type of the fungible token vault to be bridged from EVM - /// @param amount: The amount of tokens to be bridged - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - /// @return The bridged FungibleToken Vault - /// - access(EVM.Bridge) - fun withdrawTokens( - caller: auth(EVM.Call) &EVM.CadenceOwnedAccount, - type: Type, - amount: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ): @{FungibleToken.Vault} { - // Define a callback function, enabling the bridge to act on the ephemeral COA reference in scope - var executed = false - fun callback(): EVM.Result { - pre { - !executed: "Callback can only be executed once" - } - post { - executed: "Callback must be executed" - } - executed = true - return caller.call( - to: FlowEVMBridge.getAssociatedEVMAddress(with: type) - ?? panic("No EVM address associated with type"), - data: EVM.encodeABIWithSignature( - "transfer(address,uint256)", - [FlowEVMBridge.getBridgeCOAEVMAddress(), amount] - ), - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: EVM.Balance(attoflow: 0) - ) - } - // Execute the bridge request - return <- FlowEVMBridge.bridgeTokensFromEVM( - owner: caller.address(), - type: type, - amount: amount, - feeProvider: feeProvider, - protectedTransferCall: callback - ) - } - - /// Returns a BridgeRouter resource so a Capability on this BridgeAccessor can be stored in the BridgeRouter - /// - access(EVM.Bridge) fun createBridgeRouter(): @BridgeRouter { - return <-create BridgeRouter() - } - } - - /// BridgeRouter implementation used by the EVM contract to capture a BridgeAccessor Capability and route bridge - /// calls from COA resources to the FlowEVMBridge contract - /// - access(all) resource BridgeRouter : EVM.BridgeRouter { - /// Capability to the BridgeAccessor resource, initialized to nil - access(self) var bridgeAccessorCap: Capability? - - init() { - self.bridgeAccessorCap = nil - } - - /// Returns an EVM.Bridge entitled reference to the underlying BridgeAccessor resource - /// - access(EVM.Bridge) view fun borrowBridgeAccessor(): auth(EVM.Bridge) &{EVM.BridgeAccessor} { - let cap = self.bridgeAccessorCap ?? panic("BridgeAccessor Capabaility is not yet set") - return cap.borrow() ?? panic("Problem retrieving BridgeAccessor reference") - } - - /// Sets the BridgeAccessor Capability in the BridgeRouter - access(EVM.Bridge) fun setBridgeAccessor(_ accessorCap: Capability) { - self.bridgeAccessorCap = accessorCap - } - } - - /// Assesses whether the EVM contract address is associated with the provided type based on bridge associations - /// - access(self) - fun isValidEVMTarget(forType: Type, target: EVM.EVMAddress): Bool { - let currentAssociation = FlowEVMBridge.getAssociatedEVMAddress(with: forType) - let bridgedAssociation = FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: forType) - return currentAssociation?.equals(target) ?? false || bridgedAssociation?.equals(target) ?? false - } - - init(publishToEVMAccount: Address) { - self.StoragePath = /storage/flowEVMBridgeAccessor - self.account.storage.save( - <-create BridgeAccessor(), - to: self.StoragePath - ) - let cap = self.account.capabilities.storage.issue(self.StoragePath) - self.account.inbox.publish(cap, name: "FlowEVMBridgeAccessor", recipient: publishToEVMAccount) - } -} diff --git a/contracts/external/FlowEVMBridgeConfig.cdc b/contracts/external/FlowEVMBridgeConfig.cdc deleted file mode 100644 index 01eb72c85..000000000 --- a/contracts/external/FlowEVMBridgeConfig.cdc +++ /dev/null @@ -1,683 +0,0 @@ -import EVM from 0xe467b9dd11fa00df -import NonFungibleToken from 0x1d7e57aa55817448 - -import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141 -import FlowEVMBridgeCustomAssociations from 0x1e4aa0b87d10b141 - -/// This contract is used to store configuration information shared by FlowEVMBridge contracts -/// -access(all) -contract FlowEVMBridgeConfig { - - /****************** - Entitlements - *******************/ - - access(all) entitlement Gas - access(all) entitlement Fee - access(all) entitlement Pause - access(all) entitlement Blocklist - - /************* - Fields - **************/ - - /// Amount of FLOW paid to onboard a Type or EVMAddress to the bridge - access(all) - var onboardFee: UFix64 - /// Flat rate fee for all bridge requests - access(all) - var baseFee: UFix64 - /// Default ERC20.decimals() value - access(all) - let defaultDecimals: UInt8 - /// The gas limit for all EVM calls related to bridge operations - access(all) - var gasLimit: UInt64 - /// Flag enabling pausing of bridge operations - access(self) - var paused: Bool - /// Mapping of Type to its associated EVMAddress. The contained struct values also store the operational status of - /// the association, allowing for pausing of operations by Type - access(self) let registeredTypes: {Type: TypeEVMAssociation} - /// Reverse mapping of registeredTypes. Note the EVMAddress is stored as a hex string since the EVMAddress type - /// as of contract development is not a hashable or equatable type and making it so is not supported by Cadence - access(self) - let evmAddressHexToType: {String: Type} - /// Mapping of Type to its associated EVMAddress as relevant to the bridge - access(self) - let typeToTokenHandlers: @{Type: {FlowEVMBridgeHandlerInterfaces.TokenHandler}} - - /******************** - Path Constants - *********************/ - - /// StoragePath where bridge Cadence Owned Account is stored - access(all) - let coaStoragePath: StoragePath - /// StoragePath where bridge config Admin is stored - access(all) - let adminStoragePath: StoragePath - /// PublicPath where a public Capability on the bridge config Admin is exposed - access(all) - let adminPublicPath: PublicPath - /// StoragePath to store the Provider capability used as a bridge fee Provider - access(all) - let providerCapabilityStoragePath: StoragePath - - /************* - Events - **************/ - - /// Emitted whenever the onboarding fee is updated - /// - access(all) - event BridgeFeeUpdated(old: UFix64, new: UFix64, isOnboarding: Bool) - /// Emitted whenever a TokenHandler is configured - /// - access(all) - event HandlerConfigured(targetType: String, targetEVMAddress: String?, isEnabled: Bool) - /// Emitted whenever the bridge is paused or unpaused globally - true for paused, false for unpaused - /// - access(all) - event BridgePauseStatusUpdated(paused: Bool) - /// Emitted whenever a specific asset is paused or unpaused - true for paused, false for unpaused - /// - access(all) - event AssetPauseStatusUpdated(paused: Bool, type: String, evmAddress: String) - /// Emitted whenever an association is updated - /// - access(all) - event AssociationUpdated(type: String, evmAddress: String) - - /************* - Getters - *************/ - - /// Returns whether all bridge operations are currently paused or active - /// - access(all) - view fun isPaused(): Bool { - return self.paused - } - - /// Returns whether operations for a given Type are paused. A return value of nil indicates the Type is not yet - /// onboarded to the bridge. - /// - access(all) - view fun isTypePaused(_ type: Type): Bool? { - // Paused if the type has a token handler & it's disabled, a custom config has been paused or the bridge config has been paused - return !(self.borrowTokenHandler(type)?.isEnabled() ?? true) - || FlowEVMBridgeCustomAssociations.isCustomConfigPaused(forType: type) ?? false - || self.registeredTypes[type]?.isPaused == true - } - - /// Retrieves the EVMAddress associated with a given Type if it has been onboarded to the bridge - /// - access(all) - view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? { - if self.typeHasTokenHandler(type) { - return self.borrowTokenHandler(type)!.getTargetEVMAddress() - } - let customAssociation = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) - return customAssociation ?? self.registeredTypes[type]?.evmAddress - } - - /// Retrieves the type associated with a given EVMAddress if it has been onboarded to the bridge - /// - access(all) - view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { - let evmAddressHex = evmAddress.toString() - let customAssociation = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmAddress) - return customAssociation ?? self.evmAddressHexToType[evmAddressHex] - } - - /// Returns whether the given EVMAddress is currently blocked from onboarding to the bridge - /// - access(all) - view fun isEVMAddressBlocked(_ evmAddress: EVM.EVMAddress): Bool { - return self.borrowEVMBlocklist().isBlocked(evmAddress) - } - - /// Returns whether the given Cadence Type is currently blocked from onboarding to the bridge - /// - access(all) - view fun isCadenceTypeBlocked(_ type: Type): Bool { - return self.borrowCadenceBlocklist().isBlocked(type) - } - - /// Returns the project-defined Type has been registered as a replacement for the originally bridge-defined asset - /// type. This would arise in the event an EVM-native project onboarded to the bridge via permissionless onboarding - /// & later registered their own Cadence NFT contract as associated with their ERC721 per FLIP-318 mechanisms. - /// If there is not a related custom cross-VM Type registered with the bridge, `nil` is returned. - /// - access(all) - view fun getUpdatedCustomCrossVMTypeForLegacyType(_ type: Type): Type? { - if !type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) || type.address! != self.account.address { - // only bridge-defined NFT Types can have an updated custom cross-VM implementation - return nil - } - if let legacyEVMAssoc = self.getLegacyEVMAddressAssociated(with: type) { - // return the new Type associated with the originally associated EVM contract address - return FlowEVMBridgeCustomAssociations.getTypeAssociated(with: legacyEVMAssoc) - } - return nil - } - - /// Returns the bridge-defined Type that was originally associated with the related EVM contract given some - /// externally defined contract. This would arise in the event an EVM-native project onboarded to the bridge via - /// permissionless onboarding & later registered their own Cadence NFT contract as associated with their ERC721 per - /// FLIP-318 mechanisms. If there is not a related bridge-defined Type registered with the bridge, `nil` is returned. - /// - access(all) - view fun getLegacyTypeForCustomCrossVMType(_ type: Type): Type? { - if !type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) || type.address! == self.account.address { - // only externally-defined NFT Types can have an updated custom cross-VM implementation - return nil - } - if let customEVMAssoc = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type ) { - // return the original bridged NFT Type associated with the custom cross-VM EVM contract address - return self.evmAddressHexToType[customEVMAssoc.toString()] - } - return nil - } - - /// Returns the project-defined EVM contract address has been registered as a replacement for the originally bridge- - /// defined asset EVM contract. This would arise in the event an Cadence-native project onboarded to the bridge via - /// permissionless onboarding & later registered their own EVM contract as associated with their Cadence NFT per - /// FLIP-318 mechanisms. If there is not a related custom cross-VM EVM contract registered with the bridge, `nil` is - /// returned. - /// - access(all) - view fun getUpdatedCustomCrossVMEVMAddressForLegacyEVMAddress(_ evmAddress: EVM.EVMAddress): EVM.EVMAddress? { - if let legacyType = self.getLegacyTypeAssociated(with: evmAddress) { - // return the new EVM address associated with the originally associated Type - return FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: legacyType) - } - return nil - } - - /// Returns the bridge-defined EVM contract address that was originally associated with the related Cadence NFT - /// given some externally defined contract. This would arise in the event a Cadence-native project onboarded to the - /// bridge via permissionless onboarding & later registered their own EVM contract as associated with their - /// Cadence NFT per FLIP-318 mechanisms. If there is not a related bridge-defined EVM contract registered with the - /// bridge, `nil` is returned. - /// - access(all) - view fun getLegacyEVMAddressForCustomCrossVMAddress(_ evmAddress: EVM.EVMAddress): EVM.EVMAddress? { - if let customType = FlowEVMBridgeCustomAssociations.getTypeAssociated(with: evmAddress) { - // return the original bridged NFT Type associated with the custom cross-VM EVM contract address - return self.registeredTypes[customType]?.evmAddress - } - return nil - } - - /**************************** - Bridge Account Methods - ****************************/ - - /// Returns whether the given Type has a TokenHandler configured - /// - access(account) - view fun typeHasTokenHandler(_ type: Type): Bool { - return self.typeToTokenHandlers[type] != nil - } - - /// Returns whether the given EVMAddress has a TokenHandler configured - /// - access(account) - view fun evmAddressHasTokenHandler(_ evmAddress: EVM.EVMAddress): Bool { - let associatedType = self.getTypeAssociated(with: evmAddress) - return associatedType != nil ? self.typeHasTokenHandler(associatedType!) : false - } - - /// Returns the Type associated with the provided EVM contract address if the association was established via - /// the permissionless onboarding path - /// - access(account) - view fun getLegacyTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { - return self.evmAddressHexToType[evmAddress.toString()] ?? nil - } - - /// Returns the EVM contract address associated with the provided Type if the association was established via - /// the permissionless onboarding path - /// - access(account) - view fun getLegacyEVMAddressAssociated(with type: Type): EVM.EVMAddress? { - if self.typeHasTokenHandler(type) { - return self.borrowTokenHandler(type)!.getTargetEVMAddress() - } - return self.registeredTypes[type]?.evmAddress ?? nil - } - - /// Enables bridge contracts to add new associations between types and EVM addresses - /// - access(account) - fun associateType(_ type: Type, with evmAddress: EVM.EVMAddress) { - pre { - self.getEVMAddressAssociated(with: type) == nil: - "Type ".concat(type.identifier).concat(" already associated with an EVMAddress ") - .concat(self.registeredTypes[type]!.evmAddress.toString()) - self.getTypeAssociated(with: evmAddress) == nil: - "EVMAddress ".concat(evmAddress.toString()).concat(" already associated with Type ") - .concat(self.evmAddressHexToType[evmAddress.toString()]!.identifier) - } - self.registeredTypes[type] = TypeEVMAssociation(associated: evmAddress) - let evmAddressHex = evmAddress.toString() - self.evmAddressHexToType[evmAddressHex] = type - - emit AssociationUpdated(type: type.identifier, evmAddress: evmAddressHex) - } - - /// Adds a TokenHandler to the bridge configuration - /// - access(account) - fun addTokenHandler(_ handler: @{FlowEVMBridgeHandlerInterfaces.TokenHandler}) { - pre { - handler.getTargetType() != nil: "Cannot configure Handler without a target Cadence Type set" - self.getEVMAddressAssociated(with: handler.getTargetType()!) == nil: - "Cannot configure Handler for Type that has already been onboarded to the bridge" - self.borrowTokenHandler(handler.getTargetType()!) == nil: - "Cannot configure Handler for Type that already has a Handler configured" - } - let type = handler.getTargetType()! - var targetEVMAddressHex: String? = nil - if let targetEVMAddress = handler.getTargetEVMAddress() { - targetEVMAddressHex = targetEVMAddress.toString() - - let associatedType = self.getTypeAssociated(with: targetEVMAddress) - assert( - associatedType == nil, - message: "Handler target EVMAddress is already associated with a different Type" - ) - self.associateType(type, with: targetEVMAddress) - } - - emit HandlerConfigured( - targetType: type.identifier, - targetEVMAddress: targetEVMAddressHex, - isEnabled: handler.isEnabled() - ) - - self.typeToTokenHandlers[type] <-! handler - } - - /// Returns an unentitled reference to the TokenHandler associated with the given Type - /// - access(account) - view fun borrowTokenHandler( - _ type: Type - ): &{FlowEVMBridgeHandlerInterfaces.TokenHandler}? { - return &self.typeToTokenHandlers[type] - } - - /// Returns an entitled reference to the TokenHandler associated with the given Type - /// - access(self) - view fun borrowTokenHandlerAdmin( - _ type: Type - ): auth(FlowEVMBridgeHandlerInterfaces.Admin) &{FlowEVMBridgeHandlerInterfaces.TokenHandler}? { - return &self.typeToTokenHandlers[type] - } - - /// Returns an entitled reference to the bridge EVMBlocklist - /// - access(self) - view fun borrowEVMBlocklist(): auth(Blocklist) &EVMBlocklist { - return self.account.storage.borrow(from: /storage/evmBlocklist) - ?? panic("Missing or mis-typed EVMBlocklist in storage") - } - - /// Returns an entitled reference to the bridge CadenceBlocklist - /// - access(self) - view fun borrowCadenceBlocklist(): auth(Blocklist) &CadenceBlocklist { - return self.account.storage.borrow(from: /storage/cadenceBlocklist) - ?? panic("Missing or mis-typed CadenceBlocklist in storage") - } - - /// Sets the pause status of a given type, reverting if the type has no associated EVM address as either bridge- - /// defined or registered as a custom cross-VM association - /// - access(self) - fun updatePauseStatus(_ type: Type, pause: Bool) { - var evmAddress = "" - var updated = false - if let customAssoc = FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) { - updated = FlowEVMBridgeCustomAssociations.isCustomConfigPaused(forType: type)! != pause - // Called methods no-op internally, so check for status update is skipped here - pause ? FlowEVMBridgeCustomAssociations.pauseCustomConfig(forType: type) - : FlowEVMBridgeCustomAssociations.unpauseCustomConfig(forType: type) - // Assign the EVM address based on the CustomConfig value - evmAddress = customAssoc.toString() - } - if let bridgedAssoc = &FlowEVMBridgeConfig.registeredTypes[type] as &TypeEVMAssociation? { - if evmAddress.length == 0 { - // Assign as bridge association only if custom association does not exist - evmAddress = bridgedAssoc.evmAddress.toString() - } - // No-op if already meets pause status, otherwise update as specified - if (pause && !bridgedAssoc.isPaused) || (!pause && bridgedAssoc.isPaused) { - updated = true - pause ? bridgedAssoc.pause() : bridgedAssoc.unpause() - } - } - assert(evmAddress.length > 0, - message: "There was no association found for type \(type.identifier). To block the type from onboarding, use the CadenceBlocklist.") - if updated { emit AssetPauseStatusUpdated(paused: pause, type: type.identifier, evmAddress: evmAddress) } - } - - /***************** - Constructs - *****************/ - - /// Entry in the registeredTypes mapping, associating a Type with an EVMAddress and its operational status. Since - /// the registeredTypes mapping is indexed on Type, this struct does not additionally store the Type to reduce - /// redundant storage. - /// - access(all) struct TypeEVMAssociation { - /// The EVMAddress associated with the Type - access(all) let evmAddress: EVM.EVMAddress - /// Flag indicating whether operations for the associated Type are paused - access(all) var isPaused: Bool - - init(associated evmAddress: EVM.EVMAddress) { - self.evmAddress = evmAddress - self.isPaused = false - } - - /// Pauses operations for this association - /// - access(contract) fun pause() { - self.isPaused = true - } - - /// Unpauses operations for this association - /// - access(contract) fun unpause() { - self.isPaused = false - } - } - - /// EVMBlocklist resource stores a mapping of EVM addresses that are blocked from onboarding to the bridge - /// - access(all) resource EVMBlocklist { - /// Mapping of serialized EVM addresses to their blocked status - /// - access(all) let blocklist: {String: Bool} - - init() { - self.blocklist = {} - } - - /// Returns whether the given EVM address is blocked from onboarding to the bridge - /// - access(all) view fun isBlocked(_ evmAddress: EVM.EVMAddress): Bool { - return self.blocklist[evmAddress.toString()] ?? false - } - - /// Blocks the given EVM address from onboarding to the bridge - /// - access(Blocklist) fun block(_ evmAddress: EVM.EVMAddress) { - self.blocklist[evmAddress.toString()] = true - } - - /// Removes the given EVM address from the blocklist - /// - access(Blocklist) fun unblock(_ evmAddress: EVM.EVMAddress) { - self.blocklist.remove(key: evmAddress.toString()) - } - } - - /// CadenceBlocklist resource stores a mapping of Cadence Types that are blocked from onboarding to the bridge - /// - access(all) resource CadenceBlocklist { - /// Mapping of serialized Cadence Type to their blocked status - /// - access(all) let blocklist: {Type: Bool} - - init() { - self.blocklist = {} - } - - /// Returns whether the given Type is blocked from onboarding to the bridge - /// - access(all) view fun isBlocked(_ type: Type): Bool { - return self.blocklist[type] ?? false - } - - /// Blocks the given Type from onboarding to the bridge - /// - access(Blocklist) fun block(_ type: Type) { - self.blocklist[type] = true - } - - /// Removes the given type from the blocklist - /// - access(Blocklist) fun unblock(_ type: Type) { - self.blocklist.remove(key: type) - } - } - - /***************** - Config Admin - *****************/ - - /// Admin resource enables updates to the bridge fees - /// - access(all) - resource Admin { - - /// Sets the TokenMinter for the given Type. If a TokenHandler does not exist for the given Type, the operation - /// reverts. The provided minter must be of the expected type for the TokenHandler and the handler cannot have - /// a minter already set. - /// - /// @param targetType: Cadence type indexing the relevant TokenHandler - /// @param minter: TokenMinter minter to set for the TokenHandler - /// - access(all) - fun setTokenHandlerMinter(targetType: Type, minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { - pre { - FlowEVMBridgeConfig.typeHasTokenHandler(targetType): - "Cannot set minter for Type that does not have a TokenHandler configured" - FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) != nil: - "No handler found for target Type" - FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType)!.getExpectedMinterType() == minter.getType(): - "Invalid minter type" - } - FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType)!.setMinter(<-minter) - } - - /// Sets the gas limit for all EVM calls related to bridge operations - /// - /// @param lim the new gas limit - /// - access(Gas) - fun setGasLimit(_ limit: UInt64) { - FlowEVMBridgeConfig.gasLimit = limit - } - - /// Updates the onboarding fee - /// - /// @param new: UFix64 - new onboarding fee - /// - /// @emits BridgeFeeUpdated with the old and new rates and isOnboarding set to true - /// - access(Fee) - fun updateOnboardingFee(_ new: UFix64) { - emit BridgeFeeUpdated(old: FlowEVMBridgeConfig.onboardFee, new: new, isOnboarding: true) - FlowEVMBridgeConfig.onboardFee = new - } - - /// Updates the base fee - /// - /// @param new: UFix64 - new base fee - /// - /// @emits BridgeFeeUpdated with the old and new rates and isOnboarding set to false - /// - access(Fee) - fun updateBaseFee(_ new: UFix64) { - emit BridgeFeeUpdated(old: FlowEVMBridgeConfig.baseFee, new: new, isOnboarding: false) - FlowEVMBridgeConfig.baseFee = new - } - - /// Pauses the bridge, preventing all bridge operations - /// - /// @emits BridgePauseStatusUpdated with true - /// - access(Pause) - fun pauseBridge() { - if FlowEVMBridgeConfig.isPaused() { - return - } - FlowEVMBridgeConfig.paused = true - emit BridgePauseStatusUpdated(paused: true) - } - - /// Unpauses the bridge, allowing bridge operations to resume - /// - /// @emits BridgePauseStatusUpdated with true - /// - access(Pause) - fun unpauseBridge() { - if !FlowEVMBridgeConfig.isPaused() { - return - } - FlowEVMBridgeConfig.paused = false - emit BridgePauseStatusUpdated(paused: false) - } - - /// Pauses all operations for a given asset type - /// - /// @param type: The Type for which to pause bridge operations - /// - /// @emits AssetPauseStatusUpdated with the pause status and serialized type & associated EVM address - /// - access(Pause) - fun pauseType(_ type: Type) { - pre { - FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) != nil || FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) != nil: - "Could not find a bridged or custom association for type \(type.identifier) - cannot pause a type without an association" - } - FlowEVMBridgeConfig.updatePauseStatus(type, pause: true) - } - - /// Unpauses all operations for a given asset type - /// - /// @param type: The Type for which to unpause bridge operations - /// - /// @emits AssetPauseStatusUpdated with the pause status and serialized type & associated EVM address - /// - access(Pause) - fun unpauseType(_ type: Type) { - pre { - FlowEVMBridgeConfig.getEVMAddressAssociated(with: type) != nil || FlowEVMBridgeCustomAssociations.getEVMAddressAssociated(with: type) != nil: - "Could not find a bridged or custom association for type \(type.identifier) - cannot unpause a type without an association" - } - FlowEVMBridgeConfig.updatePauseStatus(type, pause: false) - } - - /// Sets the target EVM contract address on the handler for a given Type, associating the Cadence type with the - /// provided EVM address. If a TokenHandler does not exist for the given Type, the operation reverts. - /// - /// @param targetType: Cadence type to associate with the target EVM address - /// @param targetEVMAddress: target EVM address to associate with the Cadence type - /// - /// @emits HandlerConfigured with the target Type, target EVM address, and whether the handler is enabled - /// - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setHandlerTargetEVMAddress(targetType: Type, targetEVMAddress: EVM.EVMAddress) { - pre { - FlowEVMBridgeConfig.getEVMAddressAssociated(with: targetType) == nil: - "Type already associated with an EVM Address" - FlowEVMBridgeConfig.getTypeAssociated(with: targetEVMAddress) == nil: - "EVM Address already associated with another Type" - } - post { - FlowEVMBridgeConfig.getEVMAddressAssociated(with: targetType)!.equals(targetEVMAddress): - "Problem associating target Type and target EVM Address" - } - FlowEVMBridgeConfig.associateType(targetType, with: targetEVMAddress) - - let handler = FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) - ?? panic("No handler found for target Type") - handler.setTargetEVMAddress(targetEVMAddress) - - emit HandlerConfigured( - targetType: targetType.identifier, - targetEVMAddress: targetEVMAddress.toString(), - isEnabled: handler.isEnabled() - ) - } - - /// Enables the TokenHandler for the given Type. If a TokenHandler does not exist for the given Type, the - /// operation reverts. - /// - /// @param targetType: Cadence type indexing the relevant TokenHandler - /// - /// @emits HandlerConfigured with the target Type, target EVM address, and whether the handler is enabled - /// - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun enableHandler(targetType: Type) { - let handler = FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) - ?? panic("No handler found for target Type ".concat(targetType.identifier)) - handler.enableBridging() - - let targetEVMAddressHex = handler.getTargetEVMAddress()?.toString() - ?? panic("Handler cannot be enabled without a target EVM Address") - - emit HandlerConfigured( - targetType: handler.getTargetType()!.identifier, - targetEVMAddress: targetEVMAddressHex, - isEnabled: handler.isEnabled() - ) - } - - /// Disables the TokenHandler for the given Type. If a TokenHandler does not exist for the given Type, the - /// operation reverts. - /// - /// @param targetType: Cadence type indexing the relevant TokenHandler - /// - /// @emits HandlerConfigured with the target Type, target EVM address, and whether the handler is enabled - /// - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun disableHandler(targetType: Type) { - let handler = FlowEVMBridgeConfig.borrowTokenHandlerAdmin(targetType) - ?? panic("No handler found for target Type".concat(targetType.identifier)) - handler.disableBridging() - - emit HandlerConfigured( - targetType: handler.getTargetType()!.identifier, - targetEVMAddress: handler.getTargetEVMAddress()?.toString(), - isEnabled: handler.isEnabled() - ) - } - } - - init() { - self.onboardFee = 0.0 - self.baseFee = 0.0 - self.defaultDecimals = 18 - self.gasLimit = 15_000_000 - self.paused = true - - self.registeredTypes = {} - self.evmAddressHexToType = {} - - self.typeToTokenHandlers <- {} - - self.adminStoragePath = /storage/flowEVMBridgeConfigAdmin - self.adminPublicPath = /public/flowEVMBridgeConfigAdmin - self.coaStoragePath = /storage/evm - self.providerCapabilityStoragePath = /storage/bridgeFlowVaultProvider - - // Create & save Admin, issuing a public unentitled Admin Capability - self.account.storage.save(<-create Admin(), to: self.adminStoragePath) - let adminCap = self.account.capabilities.storage.issue<&Admin>(self.adminStoragePath) - self.account.capabilities.publish(adminCap, at: self.adminPublicPath) - - // Initialize the blocklists - self.account.storage.save(<-create EVMBlocklist(), to: /storage/evmBlocklist) - self.account.storage.save(<-create CadenceBlocklist(), to: /storage/cadenceBlocklist) - } -} diff --git a/contracts/external/FlowEVMBridgeCustomAssociations.cdc b/contracts/external/FlowEVMBridgeCustomAssociations.cdc deleted file mode 100644 index 9f735b1b3..000000000 --- a/contracts/external/FlowEVMBridgeCustomAssociations.cdc +++ /dev/null @@ -1,212 +0,0 @@ -import NonFungibleToken from 0x1d7e57aa55817448 -import CrossVMMetadataViews from 0x1d7e57aa55817448 -import EVM from 0xe467b9dd11fa00df - -import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141 - -/// The FlowEVMBridgeCustomAssociations is tasked with preserving custom associations between Cadence assets and their -/// EVM implementations. These associations should be validated before `saveCustomAssociation` is called by -/// leveraging the interfaces outlined in FLIP-318 (https://github.com/onflow/flips/issues/318) to ensure that the -/// declared association is valid and that neither implementation is bridge-defined. -/// -access(all) contract FlowEVMBridgeCustomAssociations { - - /// Stored associations indexed by Cadence Type - access(self) let associationsConfig: @{Type: {FlowEVMBridgeCustomAssociationTypes.CustomConfig}} - /// Reverse lookup indexed on serialized EVM contract address - access(self) let associationsByEVMAddress: {String: Type} - - /// Event emitted whenever a custom association is established - access(all) event CustomAssociationEstablished( - type: String, - evmContractAddress: String, - nativeVMRawValue: UInt8, - updatedFromBridged: Bool, - fulfillmentMinterType: String?, - fulfillmentMinterOrigin: Address?, - fulfillmentMinterCapID: UInt64?, - fulfillmentMinterUUID: UInt64?, - configUUID: UInt64 - ) - - /// Retrieves the EVM address associated with the given Cadence Type if it has been registered as a cross-VM asset - /// - /// @param with: The Cadence Type to query against - /// - /// @return The EVM address configured as associated with the provided Cadence Type - /// - access(all) - view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? { - return self.associationsConfig[type]?.getEVMContractAddress() ?? nil - } - - /// Retrieves the Cadence Type associated with the given EVM address if it has been registered as a cross-VM asset - /// - /// @param with: The EVM contract address to query against - /// - /// @return The Cadence Type configured as associated with the provided EVM address - /// - access(all) - view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? { - return self.associationsByEVMAddress[evmAddress.toString()] - } - - /// Returns an EVMPointer containing the data at the time of registration - /// - /// @param forType: The Cadence Type to query against - /// - /// @return a copy of the EVMPointer view as registered with the bridge - /// - access(all) - fun getEVMPointerAsRegistered(forType: Type): CrossVMMetadataViews.EVMPointer? { - if let config = &self.associationsConfig[forType] as &{FlowEVMBridgeCustomAssociationTypes.CustomConfig}? { - return CrossVMMetadataViews.EVMPointer( - cadenceType: config.getCadenceType(), - cadenceContractAddress: config.getCadenceType().address!, - evmContractAddress: config.getEVMContractAddress(), - nativeVM: config.getNativeVM() - ) - } - return nil - } - - /// Returns whether the related CustomConfig is currently paused or not. `nil` is returned if a CustomConfig is not - /// found for the given Type - /// - /// @param forType: The Cadence Type for which to retrieve a registered CustomConfig - /// - /// @return true if the CustomConfig is paused, false if registered and unpaused, nil if unregistered as a custom - /// association - /// - access(all) - view fun isCustomConfigPaused(forType: Type): Bool? { - return self.borrowNFTCustomConfig(forType: forType)?.isPaused() ?? nil - } - - /// Returns metadata about a registered CustomConfig - /// - /// @param forType: The Cadence Type of the registered cross-VM asset - /// - /// @return The CustomConfigInfo struct if the type is registered, nil otherwise - /// - access(all) - fun getCustomConfigInfo(forType: Type): FlowEVMBridgeCustomAssociationTypes.CustomConfigInfo? { - if let config = self.borrowNFTCustomConfig(forType: forType) { - let fulfillmentMinterType = config.checkFulfillmentMinter() == true ? config.borrowFulfillmentMinter().getType() : nil - return FlowEVMBridgeCustomAssociationTypes.CustomConfigInfo( - updatedFromBridged: config.isUpdatedFromBridged(), - isPaused: config.isPaused(), - fulfillmentMinterType: fulfillmentMinterType, - evmPointer: self.getEVMPointerAsRegistered(forType: forType)! - ) - } - return nil - } - - /// Allows the bridge contracts to preserve a custom association. Will revert if a custom association already exists - /// - /// @param type: The Cadence Type of the associated asset. - /// @param evmContractAddress: The EVM address defining the EVM implementation of the associated asset. - /// @param nativeVM: The VM in which the asset is distributed by the project. The bridge will mint/escrow in the non-native - /// VM environment. - /// @param updatedFromBridged: Whether the asset was originally onboarded to the bridge via permissionless - /// onboarding. In other words, whether there was first a bridge-defined implementation of the underlying asset. - /// @param fulfillmentMinter: An authorized Capability allowing the bridge to fulfill bridge requests moving the - /// underlying asset from EVM. Required if the asset is EVM-native. - /// - access(account) - fun saveCustomAssociation( - type: Type, - evmContractAddress: EVM.EVMAddress, - nativeVM: CrossVMMetadataViews.VM, - updatedFromBridged: Bool, - fulfillmentMinter: Capability? - ) { - pre { - self.associationsConfig[type] == nil: - "Type \(type.identifier) already has a custom association with \(self.borrowNFTCustomConfig(forType: type)!.getEVMContractAddress().toString())" - type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()): - "Only NFT cross-VM associations are currently supported but \(type.identifier) is not an NFT implementation" - self.associationsByEVMAddress[evmContractAddress.toString()] == nil: - "EVM Address \(evmContractAddress.toString()) already has a custom association with \(self.borrowNFTCustomConfig(forType: type)!.getCadenceType().identifier)" - fulfillmentMinter?.check() ?? true: - "The NFTFulfillmentMinter Capability issued from \(fulfillmentMinter!.address.toString()) is invalid. Ensure the Capability is properly issued and active." - } - let config <- FlowEVMBridgeCustomAssociationTypes.createNFTCustomConfig( - type: type, - evmContractAddress: evmContractAddress, - nativeVM: nativeVM, - updatedFromBridged: updatedFromBridged, - fulfillmentMinter: fulfillmentMinter - ) - emit CustomAssociationEstablished( - type: type.identifier, - evmContractAddress: evmContractAddress.toString(), - nativeVMRawValue: nativeVM.rawValue, - updatedFromBridged: updatedFromBridged, - fulfillmentMinterType: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().identifier : nil, - fulfillmentMinterOrigin: fulfillmentMinter?.address ?? nil, - fulfillmentMinterCapID: fulfillmentMinter?.id ?? nil, - fulfillmentMinterUUID: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.uuid : nil, - configUUID: config.uuid - ) - self.associationsByEVMAddress[config.evmContractAddress.toString()] = type - self.associationsConfig[type] <-! config - } - - /// Allows bridge contracts to fulfill NFT bridging requests for EVM-native NFTs, using the provided - /// NFTFulfillmentMinter Capability provided by the project on cross-VM registration to mint a new NFT. - /// **NOTE:** Given the bridge's mint/escrow pattern for the non-native VM, any calls should first check that the - /// requested NFT is not locked in escrow before minting. - /// - /// @param forType: The Cadence Type of the NFT being fulfilled - /// @param id: The ERC721 ID of the requested NFT - /// - /// @param - access(account) - fun fulfillNFTFromEVM(forType: Type, id: UInt256): @{NonFungibleToken.NFT} { - post { - result.getType() == forType: - "Requested \(forType.identifier) but got \(result.getType().identifier) on fulfillment from EVM" - } - let config = self.borrowNFTCustomConfig(forType: forType) - ?? panic("No CustomConfig found for type \(forType.identifier) - cannot fulfill NFT \(id) from EVM") - let minter = config.borrowFulfillmentMinter() - return <- minter.fulfillFromEVM(id: id) - } - - /// Sets the associated CustomConfig as paused, preventing bridging operations on the associated implementations. - /// Expect a no-op in the event the CustomConfig is already paused - /// - access(account) fun pauseCustomConfig(forType: Type) { - let config = self.borrowNFTCustomConfig(forType: forType) - ?? panic("No CustomConfig found for type \(forType.identifier) - cannot pause config that does not exist") - if !config.isPaused() { - config.setPauseStatus(true) - } - } - - /// Sets the associated CustomConfig as unpaused, preventing bridging operations on the associated implementations. - /// Expect a no-op in the event the CustomConfig is already paused - /// - access(account) fun unpauseCustomConfig(forType: Type) { - let config = self.borrowNFTCustomConfig(forType: forType) - ?? panic("No CustomConfig found for type \(forType.identifier) - cannot unpause config that does not exist") - if config.isPaused() { - config.setPauseStatus(false) - } - } - - /// Returns a reference to the NFTCustomConfig if it exists, nil otherwise - /// - access(self) view fun borrowNFTCustomConfig(forType: Type): &FlowEVMBridgeCustomAssociationTypes.NFTCustomConfig? { - let config = &self.associationsConfig[forType] as &{FlowEVMBridgeCustomAssociationTypes.CustomConfig}? - return config as? &FlowEVMBridgeCustomAssociationTypes.NFTCustomConfig - } - - - init() { - self.associationsConfig <- {} - self.associationsByEVMAddress = {} - } -} diff --git a/contracts/external/FlowEVMBridgeHandlerInterfaces.cdc b/contracts/external/FlowEVMBridgeHandlerInterfaces.cdc deleted file mode 100644 index 6c8c9dee0..000000000 --- a/contracts/external/FlowEVMBridgeHandlerInterfaces.cdc +++ /dev/null @@ -1,207 +0,0 @@ -import FungibleToken from 0xf233dcee88fe0abe -import NonFungibleToken from 0x1d7e57aa55817448 - -import EVM from 0xe467b9dd11fa00df - -/// FlowEVMBridgeHandlerInterfaces -/// -/// This contract defines the interfaces for the FlowEVM Bridge Handlers. These Handlers are intended to encapsulate -/// the logic for bridging edge case assets between Cadence and EVM and require configuration by the bridge account to -/// enable. Contracts implementing these resources should be deployed to the bridge account so that privileged methods, -/// particularly those related to fulfilling bridge requests remain in the closed loop of bridge contract logic and -/// defined assets in the custody of the bridge account. -/// -access(all) contract FlowEVMBridgeHandlerInterfaces { - - /****************** - Entitlements - *******************/ - - /// Entitlement related to administrative setters - access(all) entitlement Admin - /// Entitlement related to minting handled assets - access(all) entitlement Mint - - /************* - Events - **************/ - - /// Event emitted when a handler is enabled between a Cadence type and an EVM address - access(all) event HandlerEnabled( - handlerType: String, - handlerUUID: UInt64, - targetType: String, - targetEVMAddress: String - ) - /// Event emitted when a handler is disabled, pausing bridging between VMs - access(all) event HandlerDisabled( - handlerType: String, - handlerUUID: UInt64, - targetType: String?, - targetEVMAddress: String? - ) - /// Emitted when a minter resource is set in a handler - access(all) event MinterSet(handlerType: String, - handlerUUID: UInt64, - targetType: String?, - targetEVMAddress: String?, - minterType: String, - minterUUID: UInt64 - ) - - /**************** - Constructs - *****************/ - - /// Non-privileged interface for querying handler information - /// - access(all) resource interface HandlerInfo { - /// Returns whether the Handler is enabled - access(all) view fun isEnabled(): Bool - /// Returns the Cadence type handled by the Handler, nil if not set - access(all) view fun getTargetType(): Type? - /// Returns the EVM address handled by the Handler, nil if not set - access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? - /// Returns the Type of the expected minter if the handler utilizes one - access(all) view fun getExpectedMinterType(): Type? - } - - /// Administrative interface for Handler configuration - /// - access(all) resource interface HandlerAdmin : HandlerInfo { - /// Sets the target Cadence Type handled by this resource. Once the targe type is set - whether by this method - /// or on initialization - this setter will fail. - access(Admin) fun setTargetType(_ type: Type) { - pre { - self.getTargetType() == nil: "Target Type has already been set" - } - post { - self.getTargetType()! == type: "Problem setting target type" - } - } - /// Sets the target EVM address handled by this resource - access(Admin) fun setTargetEVMAddress(_ address: EVM.EVMAddress) { - pre { - self.getTargetEVMAddress() == nil: "Target EVM address has already been set" - } - post { - self.getTargetEVMAddress()!.equals(address!): "Problem setting target EVM address" - } - } - access(Admin) fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { - pre { - self.getExpectedMinterType() == minter.getType(): "Minter is not of the expected type" - minter.getMintedType() == self.getTargetType(): "Minter does not mint the target type" - emit MinterSet( - handlerType: self.getType().identifier, - handlerUUID: self.uuid, - targetType: self.getTargetType()?.identifier, - targetEVMAddress: self.getTargetEVMAddress()?.toString(), - minterType: minter.getType().identifier, - minterUUID: minter.uuid - ) - } - } - /// Enables the Handler to fulfill bridge requests for the configured targets. If implementers utilize a minter, - /// they should additionally ensure the minter is set before enabling. - access(Admin) fun enableBridging() { - pre { - self.getTargetType() != nil && self.getTargetEVMAddress() != nil: - "Cannot enable before setting bridge target Type and EVM Address" - !self.isEnabled(): "Handler has already been enabled" - } - post { - self.isEnabled(): "Problem enabling Handler" - emit HandlerEnabled( - handlerType: self.getType().identifier, - handlerUUID: self.uuid, - targetType: self.getTargetType()!.identifier, - targetEVMAddress: self.getTargetEVMAddress()!.toString() - ) - } - } - - /// Disables the Handler from fulfilling bridge requests. - access(Admin) fun disableBridging() { - pre { - self.isEnabled(): - "Cannot disable: ".concat(self.getType().identifier).concat(" is already disabled") - } - post { - !self.isEnabled(): - "Problem disabling ".concat(self.getType().identifier) - emit HandlerDisabled( - handlerType: self.getType().identifier, - handlerUUID: self.uuid, - targetType: self.getTargetType()?.identifier, - targetEVMAddress: self.getTargetEVMAddress()?.toString() - ) - } - } - } - - /// Minter interface for configurations requiring the minting of Cadence fungible tokens - /// - access(all) resource interface TokenMinter { - /// Returns the Cadence type minted by this resource - access(all) view fun getMintedType(): Type - /// Mints the specified amount of tokens - access(Mint) fun mint(amount: UFix64): @{FungibleToken.Vault} { - pre { - amount > 0.0: "Attempting to mint 0.0 - Amount minted must be greater than 0" - } - post { - result.getType() == self.getMintedType(): - "TokenMinter ".concat(self.getType().identifier).concat(" with uuid ").concat(self.uuid.toString()) - .concat(" expected to mint ").concat(self.getMintedType().identifier) - .concat(" but returned ").concat(result.getType().identifier) - result.balance == amount: - "Minted amount ".concat(result.balance.toString()) - .concat(" does not match requested amount ").concat(amount.toString()) - } - } - } - - /// Handler interface for bridging FungibleToken assets. Implementations should be stored within the bridge account - /// and called be the bridge contract for bridging operations on the Handler's target Type and EVM contract. - /// - access(all) resource interface TokenHandler : HandlerAdmin { - /// Fulfills a request to bridge tokens from the Cadence side to the EVM side - access(account) fun fulfillTokensToEVM( - tokens: @{FungibleToken.Vault}, - to: EVM.EVMAddress - ) { - pre { - self.isEnabled(): - "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ") - .concat(self.uuid.toString()).concat(" is not yet enabled") - tokens.getType() == self.getTargetType(): - "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ").concat(self.uuid.toString()) - .concat(" expects ").concat(self.getTargetType()?.identifier ?? "nil") - .concat(" but received ").concat(tokens.getType().identifier) - tokens.balance > 0.0: - "Attempting to bridge 0.0 tokens - zero amounts are unsupported" - } - } - /// Fulfills a request to bridge tokens from the EVM side to the Cadence side - access(account) fun fulfillTokensFromEVM( - owner: EVM.EVMAddress, - type: Type, - amount: UInt256, - protectedTransferCall: fun (): EVM.Result - ): @{FungibleToken.Vault} { - pre { - self.isEnabled(): - "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ") - .concat(self.uuid.toString()).concat(" is not yet enabled") - amount > UInt256(0): "Attempting to bridge 0 tokens from EVM - zero amounts are unsupported" - } - post { - result.getType() == self.getTargetType(): - "TokenHandler ".concat(self.getType().identifier).concat(" with uuid ").concat(self.uuid.toString()) - .concat(" expected to return ").concat(self.getTargetType()?.identifier ?? "nil") - .concat(" but returned ").concat(result.getType().identifier) - } - } - } -} diff --git a/contracts/external/FlowEVMBridgeHandlers.cdc b/contracts/external/FlowEVMBridgeHandlers.cdc deleted file mode 100644 index fb9225466..000000000 --- a/contracts/external/FlowEVMBridgeHandlers.cdc +++ /dev/null @@ -1,468 +0,0 @@ -import Burner from 0xf233dcee88fe0abe -import FungibleToken from 0xf233dcee88fe0abe -import NonFungibleToken from 0x1d7e57aa55817448 -import FlowToken from 0x1654653399040a61 - -import EVM from 0xe467b9dd11fa00df - -import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141 -import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 -import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141 - -/// FlowEVMBridgeHandlers -/// -/// This contract is responsible for defining and configuring bridge handlers for special cased assets. -/// -access(all) contract FlowEVMBridgeHandlers { - - /********************** - Contract Fields - ***********************/ - - /// The storage path for the HandlerConfigurator resource - access(all) let ConfiguratorStoragePath: StoragePath - - /**************** - Constructs - *****************/ - - /// Handler for bridging Cadence native fungible tokens to EVM. In the event a Cadence project migrates native - /// support to EVM, this Hander can be configured to facilitate bridging the Cadence tokens to EVM. This Handler - /// then effectively allows the bridge to treat such tokens as bridge-defined on the Cadence side and EVM-native on - /// the EVM side minting/burning in Cadence and escrowing in EVM. - /// In order for this to occur, neither the Cadence token nor the EVM contract can be onboarded to the bridge - in - /// essence, neither side of the asset can be onboarded to the bridge. - /// The Handler must be configured in the bridge via the HandlerConfigurator. Once added, the bridge will filter - /// requests to bridge the token Vault to EVM through this Handler which cannot be enabled until a target EVM - /// address is set. Once the corresponding EVM contract address is known, it can be set and the Handler. It's also - /// suggested that the Handler only be enabled once sufficient liquidity has been arranged in bridge escrow on the - /// EVM side. - /// - access(all) resource CadenceNativeTokenHandler : FlowEVMBridgeHandlerInterfaces.TokenHandler { - /// Flag determining if request handling is enabled - access(self) var enabled: Bool - /// The Cadence Type this handler fulfills requests for - access(self) var targetType: Type - /// The EVM contract address this handler fulfills requests for. This field is optional in the event the EVM - /// contract address is not yet known but the Cadence type must still be filtered via Handler to prevent the - /// type from being onboarded otherwise. - access(self) var targetEVMAddress: EVM.EVMAddress? - /// The expected minter type for minting tokens on fulfillment - access(self) let expectedMinterType: Type - /// The Minter enabling minting of Cadence tokens on fulfillment from EVM - access(self) var minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}? - - init(targetType: Type, targetEVMAddress: EVM.EVMAddress?, expectedMinterType: Type) { - pre { - expectedMinterType.isSubtype(of: Type<@{FlowEVMBridgeHandlerInterfaces.TokenMinter}>()): - "Invalid minter type" - } - self.enabled = false - self.targetType = targetType - self.targetEVMAddress = targetEVMAddress - self.expectedMinterType = expectedMinterType - self.minter <- nil - } - - /* --- HandlerInfo --- */ - - /// Returns the enabled status of the handler - access(all) view fun isEnabled(): Bool { - return self.enabled - } - - /// Returns the type of the asset the handler is configured to handle - access(all) view fun getTargetType(): Type? { - return self.targetType - } - - /// Returns the EVM contract address the handler is configured to handle - access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? { - return self.targetEVMAddress - } - - /// Returns the expected minter type for the handler - access(all) view fun getExpectedMinterType(): Type? { - return self.expectedMinterType - } - - /* --- TokenHandler --- */ - - /// Fulfill a request to bridge tokens from Cadence to EVM, burning the provided Vault and transferring from - /// EVM escrow to the named recipient. Assumes any fees are handled by the caller within the bridge contracts - /// - /// @param tokens: The Vault containing the tokens to bridge - /// @param to: The EVM address to transfer the tokens to - /// - access(account) - fun fulfillTokensToEVM( - tokens: @{FungibleToken.Vault}, - to: EVM.EVMAddress - ) { - let evmAddress = self.getTargetEVMAddress()! - - // Get values from vault and burn - let amount = tokens.balance - let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: evmAddress) - - assert(uintAmount > UInt256(0), message: "Amount to bridge must be greater than 0") - - Burner.burn(<-tokens) - - FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: uintAmount, erc20Address: evmAddress) - } - - /// Fulfill a request to bridge tokens from EVM to Cadence, minting the provided amount of tokens in Cadence - /// and transferring from the named owner to bridge escrow in EVM. - /// - /// @param owner: The EVM address of the owner of the tokens. Should also be the caller executing the protected - /// transfer call. - /// @param type: The type of the asset being bridged - /// @param amount: The amount of tokens to bridge - /// - /// @return The minted Vault containing the the requested amount of Cadence tokens - /// - access(account) - fun fulfillTokensFromEVM( - owner: EVM.EVMAddress, - type: Type, - amount: UInt256, - protectedTransferCall: fun (): EVM.Result - ): @{FungibleToken.Vault} { - let evmAddress = self.getTargetEVMAddress()! - - // Convert the amount to a UFix64 - let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount( - amount, - erc20Address: evmAddress - ) - assert(ufixAmount > 0.0, message: "Amount to bridge must be greater than 0") - - FlowEVMBridgeUtils.mustEscrowERC20( - owner: owner, - amount: amount, - erc20Address: evmAddress, - protectedTransferCall: protectedTransferCall - ) - - // After state confirmation, mint the tokens and return - let minter = self.borrowMinter() - ?? panic("Cannot bridge - Minter not set in ".concat(self.getType().identifier)) - let minted <- minter.mint(amount: ufixAmount) - return <-minted - } - - /* --- Admin --- */ - - /// Sets the target type for the handler - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setTargetType(_ type: Type) { - self.targetType = type - } - - /// Sets the target EVM address for the handler - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setTargetEVMAddress(_ address: EVM.EVMAddress) { - self.targetEVMAddress = address - } - - /// Sets the target type for the handler - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { - pre { - self.minter == nil: "Minter has already been set in ".concat(self.getType().identifier) - } - self.minter <-! minter - } - - /// Enables the handler for request handling. - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun enableBridging() { - pre { - self.minter != nil: "Cannot enable ".concat(self.getType().identifier).concat(" without a minter") - } - self.enabled = true - } - - /// Disables the handler for request handling. - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun disableBridging() { - self.enabled = false - } - - /* --- Internal --- */ - - /// Returns an entitled reference to the encapsulated minter resource - access(self) - view fun borrowMinter(): auth(FlowEVMBridgeHandlerInterfaces.Mint) &{FlowEVMBridgeHandlerInterfaces.TokenMinter}? { - return &self.minter - } - } - - /// Facilitates moving Flow between Cadence and EVM as WFLOW. Since WFLOW is an artifact of the EVM ecosystem, - /// wrapping the native token as an ERC20, it does not have a place in Cadence's fungible token ecosystem. - /// Given the native interface on EVM.CadenceOwnedAccount and EVM.EVMAddress to move FLOW between Cadence and EVM, - /// this handler treats requests to bridge FLOW as WFLOW as a special case. - /// - access(all) resource WFLOWTokenHandler : FlowEVMBridgeHandlerInterfaces.TokenHandler { - /// Flag determining if request handling is enabled - access(self) var enabled: Bool - /// The Cadence Type this handler fulfills requests for - access(self) var targetType: Type - /// The EVM contract address this handler fulfills requests for - access(self) var targetEVMAddress: EVM.EVMAddress - - init(wflowEVMAddress: EVM.EVMAddress) { - self.enabled = false - self.targetType = Type<@FlowToken.Vault>() - self.targetEVMAddress = wflowEVMAddress - } - - /// Returns whether the Handler is enabled - access(all) view fun isEnabled(): Bool { - return self.enabled - } - /// Returns the Cadence type handled by the Handler, nil if not set - access(all) view fun getTargetType(): Type? { - return self.targetType - } - /// Returns the EVM address handled by the Handler, nil if not set - access(all) view fun getTargetEVMAddress(): EVM.EVMAddress? { - return self.targetEVMAddress - } - /// Returns nil as this handler simply unwraps WFLOW to FLOW - access(all) view fun getExpectedMinterType(): Type? { - return nil - } - - /* --- TokenHandler --- */ - - /// Fulfill a request to bridge tokens from Cadence to EVM, burning the provided Vault and transferring from - /// EVM escrow to the named recipient. Assumes any fees are handled by the caller within the bridge contracts - /// - /// @param tokens: The Vault containing the tokens to bridge - /// @param to: The EVM address to transfer the tokens to - /// - access(account) - fun fulfillTokensToEVM( - tokens: @{FungibleToken.Vault}, - to: EVM.EVMAddress - ) { - let flowVault <- tokens as! @FlowToken.Vault - let wflowAddress = self.getTargetEVMAddress()! - - // Get balance from vault - let balance = flowVault.balance - let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(balance, erc20Address: wflowAddress) - - // Deposit to bridge COA - let coa = FlowEVMBridgeUtils.borrowCOA() - coa.deposit(from: <-flowVault) - - let preBalance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: wflowAddress) - - // Wrap the deposited FLOW as WFLOW, giving the bridge COA the necessary WFLOW to transfer - let wrapResult = FlowEVMBridgeUtils.call( - signature: "deposit()", - targetEVMAddress: wflowAddress, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: balance - ) - assert(wrapResult.status == EVM.Status.successful, message: "Failed to wrap FLOW as WFLOW") - - let postBalance = FlowEVMBridgeUtils.balanceOf(owner: coa.address(), evmContractAddress: wflowAddress) - - // Cover underflow - assert( - postBalance > preBalance, - message: "Escrowed WFLOW balance did not increment after wrapping FLOW - pre: " - .concat(preBalance.toString()).concat(" | post: ").concat(postBalance.toString()) - ) - // Confirm bridge COA's WFLOW balance has incremented by the expected amount - assert( - postBalance - preBalance == uintAmount, - message: "Escrowed WFLOW balance after wrapping does not match requested amount - expected: " - .concat((preBalance + uintAmount).toString()) - .concat(" | actual: ") - .concat((postBalance - preBalance).toString()) - ) - - // Transfer WFLOW to recipient - FlowEVMBridgeUtils.mustTransferERC20(to: to, amount: uintAmount, erc20Address: wflowAddress) - } - - /// Fulfill a request to bridge tokens from EVM to Cadence, minting the provided amount of tokens in Cadence - /// and transferring from the named owner to bridge escrow in EVM. - /// - /// @param owner: The EVM address of the owner of the tokens. Should also be the caller executing the protected - /// transfer call. - /// @param type: The type of the asset being bridged - /// @param amount: The amount of tokens to bridge - /// - /// @return The minted Vault containing the the requested amount of Cadence tokens - /// - access(account) - fun fulfillTokensFromEVM( - owner: EVM.EVMAddress, - type: Type, - amount: UInt256, - protectedTransferCall: fun (): EVM.Result - ): @{FungibleToken.Vault} { - let wflowAddress = self.getTargetEVMAddress()! - - // Convert the amount to a UFix64 - let ufixAmount = FlowEVMBridgeUtils.convertERC20AmountToCadenceAmount( - amount, - erc20Address: wflowAddress - ) - assert( - ufixAmount > 0.0, - message: "Requested UInt256 amount ".concat(amount.toString()).concat(" converted to 0.0 ") - .concat(" - try bridging a larger amount to avoid UFix64 precision loss during conversion") - ) - - // Transfers WFLOW to bridge COA as escrow - FlowEVMBridgeUtils.mustEscrowERC20( - owner: owner, - amount: amount, - erc20Address: wflowAddress, - protectedTransferCall: protectedTransferCall - ) - - // Get the bridge COA's FLOW balance before unwrapping WFLOW - let coa = FlowEVMBridgeUtils.borrowCOA() - let preBalance = coa.balance().attoflow - - // Unwrap the transferred WFLOW to FLOW, giving the bridge COA the necessary FLOW to withdraw from EVM - let unwrapResult = FlowEVMBridgeUtils.call( - signature: "withdraw(uint256)", - targetEVMAddress: wflowAddress, - args: [amount], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(unwrapResult.status == EVM.Status.successful, message: "Failed to unwrap WFLOW as FLOW") - - let postBalance = coa.balance().attoflow - - // Cover underflow - assert( - postBalance > preBalance, - message: "Escrowed FLOW Balance did not increment after unwrapping WFLOW - pre: ".concat(preBalance.toString()) - .concat(" | post: ").concat(postBalance.toString()) - ) - // Confirm bridge COA's FLOW balance has incremented by the expected amount - assert( - UInt256(postBalance - preBalance) == amount, - message: "Escrowed WFLOW balance after unwrapping does not match requested amount - expected: " - .concat((UInt256(preBalance) + amount).toString()) - .concat(" | actual: ") - .concat((postBalance - preBalance).toString()) - ) - - // Withdraw escrowed FLOW from bridge COA - let withdrawBalance = EVM.Balance(attoflow: UInt(amount)) - assert( - UInt256(withdrawBalance.attoflow) == amount, - message: "Requested balance failed to convert to attoflow - expected: " - .concat(amount.toString()) - .concat(" | actual: ") - .concat(withdrawBalance.attoflow.toString()) - ) - let flowVault <- coa.withdraw(balance: withdrawBalance) - assert( - flowVault.balance == ufixAmount, - message: "Resulting FLOW Vault balance does not match requested amount - expected: " - .concat(ufixAmount.toString()) - .concat(" | actual: ") - .concat(flowVault.balance.toString()) - ) - return <-flowVault - } - - /* --- HandlerAdmin --- */ - // Conforms to HandlerAdmin for enableBridging, but most of the methods are unnecessary given the strict - // association between FLOW and WFLOW - - /// Sets the target type for the handler - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setTargetType(_ type: Type) { - panic("WFLOWTokenHandler has targetType set to " - .concat(self.targetType.identifier).concat(" at initialization")) - } - - /// Sets the target EVM address for the handler - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setTargetEVMAddress(_ address: EVM.EVMAddress) { - panic("WFLOWTokenHandler has EVMAddress set to " - .concat(self.targetEVMAddress.toString()).concat(" at initialization")) - } - - /// Sets the target type for the handler - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) { - panic("WFLOWTokenHandler does not utilize a minter") - } - - /// Enables the handler for request handling. The - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun enableBridging() { - self.enabled = true - } - - /// Disables the handler for request handling. - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun disableBridging() { - self.enabled = false - } - } - - /// This resource enables the configuration of Handlers. These Handlers are stored in FlowEVMBridgeConfig from which - /// further setting and getting can be executed. - /// - access(all) resource HandlerConfigurator { - /// Creates a new Handler and adds it to the bridge configuration - /// - /// @param handlerType: The type of handler to create as defined in this contract - /// @param targetType: The type of the asset the handler will handle - /// @param targetEVMAddress: The EVM contract address the handler will handle, can be nil if still unknown - /// @param expectedMinterType: The Type of the expected minter to be set for the created TokenHandler - /// - access(FlowEVMBridgeHandlerInterfaces.Admin) - fun createTokenHandler( - handlerType: Type, - targetType: Type, - targetEVMAddress: EVM.EVMAddress?, - expectedMinterType: Type? - ) { - switch handlerType { - case Type<@CadenceNativeTokenHandler>(): - assert( - expectedMinterType != nil, - message: "CadenceNativeTokenHandler requires an expected minter type but received nil" - ) - let handler <-create CadenceNativeTokenHandler( - targetType: targetType, - targetEVMAddress: targetEVMAddress, - expectedMinterType: expectedMinterType! - ) - FlowEVMBridgeConfig.addTokenHandler(<-handler) - case Type<@WFLOWTokenHandler>(): - assert( - targetEVMAddress != nil, - message: "WFLOWTokenHandler requires a target EVM address but received nil" - ) - let handler <-create WFLOWTokenHandler(wflowEVMAddress: targetEVMAddress!) - FlowEVMBridgeConfig.addTokenHandler(<-handler) - default: - panic("Invalid Handler type requested") - } - } - } - - init() { - self.ConfiguratorStoragePath = /storage/BridgeHandlerConfigurator - self.account.storage.save(<-create HandlerConfigurator(), to: self.ConfiguratorStoragePath) - } -} diff --git a/contracts/external/FlowEVMBridgeUtils.cdc b/contracts/external/FlowEVMBridgeUtils.cdc deleted file mode 100644 index 571b860bf..000000000 --- a/contracts/external/FlowEVMBridgeUtils.cdc +++ /dev/null @@ -1,1600 +0,0 @@ -import NonFungibleToken from 0x1d7e57aa55817448 -import FungibleToken from 0xf233dcee88fe0abe -import MetadataViews from 0x1d7e57aa55817448 -import CrossVMMetadataViews from 0x1d7e57aa55817448 -import FungibleTokenMetadataViews from 0xf233dcee88fe0abe -import ViewResolver from 0x1d7e57aa55817448 -import FlowToken from 0x1654653399040a61 -import FlowStorageFees from 0xe467b9dd11fa00df - -import EVM from 0xe467b9dd11fa00df - -import SerializeMetadata from 0x1e4aa0b87d10b141 -import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 -import CrossVMNFT from 0x1e4aa0b87d10b141 -import IBridgePermissions from 0x1e4aa0b87d10b141 - -/// This contract serves as a source of utility methods leveraged by FlowEVMBridge contracts -// -access(all) -contract FlowEVMBridgeUtils { - - /// Address of the bridge factory Solidity contract - access(self) - var bridgeFactoryEVMAddress: EVM.EVMAddress - /// Delimeter used to derive contract names - access(self) - let delimiter: String - /// Mapping containing contract name prefixes - access(self) - let contractNamePrefixes: {Type: {String: String}} - - /**************** - Constructs - *****************/ - - /// Struct used to preserve and pass around multiple values relating to Cadence asset onboarding - /// - access(all) struct CadenceOnboardingValues { - access(all) let contractAddress: Address - access(all) let name: String - access(all) let symbol: String - access(all) let identifier: String - access(all) let contractURI: String - - init( - contractAddress: Address, - name: String, - symbol: String, - identifier: String, - contractURI: String - ) { - self.contractAddress = contractAddress - self.name = name - self.symbol = symbol - self.identifier = identifier - self.contractURI = contractURI - } - } - - /// Struct used to preserve and pass around multiple values preventing the need to make multiple EVM calls - /// during EVM asset onboarding - /// - access(all) struct EVMOnboardingValues { - access(all) let evmContractAddress: EVM.EVMAddress - access(all) let name: String - access(all) let symbol: String - access(all) let decimals: UInt8? - access(all) let contractURI: String? - access(all) let cadenceContractName: String - access(all) let isERC721: Bool - - init( - evmContractAddress: EVM.EVMAddress, - name: String, - symbol: String, - decimals: UInt8?, - contractURI: String?, - cadenceContractName: String, - isERC721: Bool - ) { - self.evmContractAddress = evmContractAddress - self.name = name - self.symbol = symbol - self.decimals = decimals - self.contractURI = contractURI - self.cadenceContractName = cadenceContractName - self.isERC721 = isERC721 - } - } - - /************************** - Public Bridge Utils - **************************/ - - /// Retrieves the bridge factory contract address - /// - /// @returns The EVMAddress of the bridge factory contract in EVM - /// - access(all) - view fun getBridgeFactoryEVMAddress(): EVM.EVMAddress { - return self.bridgeFactoryEVMAddress - } - - /// Calculates the fee bridge fee based on the given storage usage + the current base fee. - /// - /// @param used: The amount of storage used by the asset - /// - /// @return The calculated fee amount - /// - access(all) - view fun calculateBridgeFee(bytes used: UInt64): UFix64 { - let megabytesUsed = FlowStorageFees.convertUInt64StorageBytesToUFix64Megabytes(used) - let storageFee = FlowStorageFees.storageCapacityToFlow(megabytesUsed) - return storageFee + FlowEVMBridgeConfig.baseFee - } - - /// Returns whether the given type is allowed to be bridged as defined by the IBridgePermissions contract interface. - /// If the type's defining contract does not implement IBridgePermissions, the method returns true as the bridge - /// operates permissionlessly by default. Otherwise, the result of {IBridgePermissions}.allowsBridging() is returned - /// - /// @param type: The Type of the asset to check - /// - /// @return true if the type is allowed to be bridged, false otherwise - /// - access(all) - view fun typeAllowsBridging(_ type: Type): Bool { - let contractAddress = self.getContractAddress(fromType: type) - ?? panic("Could not construct contract address from type identifier: ".concat(type.identifier)) - let contractName = self.getContractName(fromType: type) - ?? panic("Could not construct contract name from type identifier: ".concat(type.identifier)) - if let bridgePermissions = getAccount(contractAddress).contracts.borrow<&{IBridgePermissions}>(name: contractName) { - return bridgePermissions.allowsBridging() - } - return true - } - - /// Returns whether the given address has opted out of enabling bridging for its defined assets - /// - /// @param address: The EVM contract address to check - /// - /// @return false if the address has opted out of enabling bridging, true otherwise - /// - access(all) - fun evmAddressAllowsBridging(_ address: EVM.EVMAddress): Bool { - let callResult = self.dryCall( - signature: "allowsBridging()", - targetEVMAddress: address, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - // Contract doesn't support the method - proceed permissionlessly - if callResult.status != EVM.Status.successful { - return true - } - // Contract is IBridgePermissions - return the result - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - return (decodedResult.length == 1 && decodedResult[0] as! Bool) == true ? true : false - } - - /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge contract defines it or not - /// - /// @param type: The Type of the asset to check - /// - /// @return True if the asset is Cadence-native, false if it is EVM-native - /// - access(all) - view fun isCadenceNative(type: Type): Bool { - let definingAddress = self.getContractAddress(fromType: type) - ?? panic("Could not construct address from type identifier: ".concat(type.identifier)) - return definingAddress != self.account.address - } - - /// Identifies if an asset is a type that is defined by a bridge-owned Cadence contract. For NFTs, this would - /// indicate that the NFT is a bridged representation of a corresponding ERC721. For a Vault, this would - /// indicate that the Vault is a bridged representation of a corresponding ERC20. - /// - /// @param type: The Type of the asset to check - /// - /// @return True if the asset is bridge-defined, false if another Cadence contract defines the type. Reverts if the - /// type is a primitive type that is not defined by a Cadence contract. - /// - access(all) - view fun isBridgeDefined(type: Type): Bool { - let definingAddress = self.getContractAddress(fromType: type) - ?? panic("Could not construct address from type identifier: ".concat(type.identifier)) - return definingAddress == self.account.address - } - - /// Identifies if an asset is Cadence- or EVM-native, defined by whether a bridge-owned contract defines it or not. - /// Reverts on EVM call failure. - /// - /// @param type: The Type of the asset to check - /// - /// @return True if the asset is EVM-native, false if it is Cadence-native - /// - access(all) - fun isEVMNative(evmContractAddress: EVM.EVMAddress): Bool { - return self.isEVMContractBridgeOwned(evmContractAddress: evmContractAddress) == false - } - - /// Determines if the given EVM contract address was deployed by the bridge by querying the factory contract - /// Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to check - /// - /// @return True if the contract was deployed by the bridge, false otherwise - /// - access(all) - fun isEVMContractBridgeOwned(evmContractAddress: EVM.EVMAddress): Bool { - // Ask the bridge factory if the given contract address was deployed by the bridge - let callResult = self.dryCall( - signature: "isBridgeDeployed(address)", - targetEVMAddress: self.bridgeFactoryEVMAddress, - args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - - assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) - assert(decodedResult.length == 1, message: "Invalid response length") - - return decodedResult[0] as! Bool - } - - /// Identifies if an asset is ERC721. Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to check - /// - /// @return True if the asset is an ERC721, false otherwise - /// - access(all) - fun isERC721(evmContractAddress: EVM.EVMAddress): Bool { - let callResult = self.dryCall( - signature: "isERC721(address)", - targetEVMAddress: self.bridgeFactoryEVMAddress, - args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - - assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) - assert(decodedResult.length == 1, message: "Invalid response length") - - return decodedResult[0] as! Bool - } - - /// Identifies if an asset is ERC20 as far as is possible without true EVM type introspection. Reverts on EVM call - /// failure. - /// - /// @param evmContractAddress: The EVM contract address to check - /// - /// @return true if the asset is an ERC20, false otherwise - /// - access(all) - fun isERC20(evmContractAddress: EVM.EVMAddress): Bool { - let callResult = self.dryCall( - signature: "isERC20(address)", - targetEVMAddress: self.bridgeFactoryEVMAddress, - args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - - assert(callResult.status == EVM.Status.successful, message: "Call to bridge factory failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) - assert(decodedResult.length == 1, message: "Invalid response length") - - return decodedResult[0] as! Bool - } - - /// Returns whether the contract address is either an ERC721 or ERC20 exclusively. Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to check - /// - /// @return True if the contract is either an ERC721 or ERC20, false otherwise - /// - access(all) - fun isValidEVMAsset(evmContractAddress: EVM.EVMAddress): Bool { - let callResult = self.dryCall( - signature: "isValidAsset(address)", - targetEVMAddress: self.bridgeFactoryEVMAddress, - args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) - assert(decodedResult.length == 1, message: "Invalid response length") - return decodedResult[0] as! Bool - } - - /// Returns whether the given type is either an NFT or FT exclusively - /// - /// @param type: The Type of the asset to check - /// - /// @return True if the type is either an NFT or FT, false otherwise - /// - access(all) - view fun isValidCadenceAsset(type: Type): Bool { - let isCadenceNFT = type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) - let isCadenceFungibleToken = type.isSubtype(of: Type<@{FungibleToken.Vault}>()) - return isCadenceNFT != isCadenceFungibleToken - } - - /// Retrieves the bridge contract's COA EVMAddress - /// - /// @returns The EVMAddress of the bridge contract's COA orchestrating actions in FlowEVM - /// - access(all) - view fun getBridgeCOAEVMAddress(): EVM.EVMAddress { - return self.borrowCOA().address() - } - - /// Retrieves the relevant information for onboarding a Cadence asset to the bridge. This method is used to - /// retrieve the name, symbol, contract address, and contract URI for a given Cadence asset type. These values - /// are used to then deploy a corresponding EVM contract. If EVMBridgedMetadata is supported by the asset's - /// defining contract, the values are retrieved from that view. Otherwise, the values are derived from other - /// common metadata views. - /// - /// @param forAssetType: The Type of the asset to retrieve onboarding values for - /// - /// @return The CadenceOnboardingValues struct containing the asset's name, symbol, identifier, contract address, - /// and contract URI - /// - access(all) - fun getCadenceOnboardingValues(forAssetType: Type): CadenceOnboardingValues { - pre { - self.isValidCadenceAsset(type: forAssetType): "This type is not a supported Flow asset type." - } - // If not an NFT, assumed to be fungible token. - let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) - - // Retrieve the Cadence type's defining contract name, address, & its identifier - var name = self.getContractName(fromType: forAssetType) - ?? panic("Could not contract name from type: ".concat(forAssetType.identifier)) - let identifier = forAssetType.identifier - let cadenceAddress = self.getContractAddress(fromType: forAssetType) - ?? panic("Could not derive contract address for token type: ".concat(identifier)) - // Initialize asset symbol which will be assigned later - // based on presence of asset-defined metadata - var symbol: String? = nil - // Borrow the ViewResolver to attempt to resolve the EVMBridgedMetadata view - let viewResolver = getAccount(cadenceAddress).contracts.borrow<&{ViewResolver}>(name: name)! - var contractURI = "" - - // Try to resolve the EVMBridgedMetadata - let bridgedMetadata = viewResolver.resolveContractView( - resourceType: forAssetType, - viewType: Type() - ) as! MetadataViews.EVMBridgedMetadata? - // Default to project-defined URI if available - if bridgedMetadata != nil { - name = bridgedMetadata!.name - symbol = bridgedMetadata!.symbol - contractURI = bridgedMetadata!.uri.uri() - } else { - if isNFT { - // Otherwise, serialize collection-level NFTCollectionDisplay - if let collectionDisplay = viewResolver.resolveContractView( - resourceType: forAssetType, - viewType: Type() - ) as! MetadataViews.NFTCollectionDisplay? { - name = collectionDisplay.name - let serializedDisplay = SerializeMetadata.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)! - contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}") - } - if symbol == nil { - symbol = SerializeMetadata.deriveSymbol(fromString: name) - } - } else { - let ftDisplay = viewResolver.resolveContractView( - resourceType: forAssetType, - viewType: Type() - ) as! FungibleTokenMetadataViews.FTDisplay? - if ftDisplay != nil { - name = ftDisplay!.name - symbol = ftDisplay!.symbol - } - if contractURI.length == 0 && ftDisplay != nil { - let serializedDisplay = SerializeMetadata.serializeFTDisplay(ftDisplay!) - contractURI = "data:application/json;utf8,{".concat(serializedDisplay).concat("}") - } - } - } - - return CadenceOnboardingValues( - contractAddress: cadenceAddress, - name: name, - symbol: symbol!, - identifier: identifier, - contractURI: contractURI - ) - } - - /// Retrieves identifying information about an EVM contract related to bridge onboarding. - /// - /// @param evmContractAddress: The EVM contract address to retrieve onboarding values for - /// - /// @return The EVMOnboardingValues struct containing the asset's name, symbol, decimals, contractURI, and - /// Cadence contract name as well as whether the asset is an ERC721 - /// - access(all) - fun getEVMOnboardingValues(evmContractAddress: EVM.EVMAddress): EVMOnboardingValues { - // Retrieve the EVM contract's name, symbol, and contractURI - let name: String = self.getName(evmContractAddress: evmContractAddress) - let symbol: String = self.getSymbol(evmContractAddress: evmContractAddress) - let contractURI = self.getContractURI(evmContractAddress: evmContractAddress) - // Default to 18 decimals for ERC20s - var decimals: UInt8 = FlowEVMBridgeConfig.defaultDecimals - - // Derive Cadence contract name - let isERC721: Bool = self.isERC721(evmContractAddress: evmContractAddress) - var cadenceContractName: String = "" - if isERC721 { - // Assert the contract is not mixed asset - let isERC20 = self.isERC20(evmContractAddress: evmContractAddress) - assert(!isERC20, message: "Contract is mixed asset and is not currently supported by the bridge") - // Derive the contract name from the ERC721 contract - cadenceContractName = self.deriveBridgedNFTContractName(from: evmContractAddress) - } else { - // Otherwise, treat as ERC20 - let isERC20 = self.isERC20(evmContractAddress: evmContractAddress) - assert( - isERC20, - message: "Contract ".concat(evmContractAddress.toString()).concat("defines an asset that is not currently supported by the bridge") - ) - cadenceContractName = self.deriveBridgedTokenContractName(from: evmContractAddress) - decimals = self.getTokenDecimals(evmContractAddress: evmContractAddress) - } - - return EVMOnboardingValues( - evmContractAddress: evmContractAddress, - name: name, - symbol: symbol, - decimals: decimals, - contractURI: contractURI, - cadenceContractName: cadenceContractName, - isERC721: isERC721 - ) - } - - /// Retrieves the EVMPointer view from a given type's defining contract if the view is supported. - /// NOTE: This does not guarantee the association is valid, only that the defining Cadence contract declares - /// the association. - /// - /// @param from: The type for which to retrieve the EVMPointer view - /// - /// @return The resolved EVMPointer view for the given type or nil if the view is unsupported - /// - access(all) - fun getEVMPointerView(forType: Type): CrossVMMetadataViews.EVMPointer? { - let contractAddress = forType.address! - let contractName = forType.contractName! - if let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) { - return viewResolver.resolveContractView( - resourceType: forType, - viewType: Type() - ) as? CrossVMMetadataViews.EVMPointer? ?? nil - } - return nil - } - - /************************ - EVM Call Wrappers - ************************/ - - /// Retrieves the NFT/FT name from the given EVM contract address - applies for both ERC20 & ERC721. - /// Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to retrieve the name from - /// - /// @return the name of the asset - /// - access(all) - fun getName(evmContractAddress: EVM.EVMAddress): String { - let callResult = self.dryCall( - signature: "name()", - targetEVMAddress: evmContractAddress, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - - assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset name failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - assert(decodedResult.length == 1, message: "Invalid response length") - - return decodedResult[0] as! String - } - - /// Retrieves the NFT/FT symbol from the given EVM contract address - applies for both ERC20 & ERC721 - /// Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to retrieve the symbol from - /// - /// @return the symbol of the asset - /// - access(all) - fun getSymbol(evmContractAddress: EVM.EVMAddress): String { - let callResult = self.dryCall( - signature: "symbol()", - targetEVMAddress: evmContractAddress, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset symbol failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - assert(decodedResult.length == 1, message: "Invalid response length") - return decodedResult[0] as! String - } - - /// Retrieves the tokenURI for the given NFT ID from the given EVM contract address. Reverts on EVM call failure. - /// Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to retrieve the tokenURI from - /// @param id: The ID of the NFT for which to retrieve the tokenURI value - /// - /// @return the tokenURI of the ERC721 - /// - access(all) - fun getTokenURI(evmContractAddress: EVM.EVMAddress, id: UInt256): String { - let callResult = self.dryCall( - signature: "tokenURI(uint256)", - targetEVMAddress: evmContractAddress, - args: [id], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - - assert(callResult.status == EVM.Status.successful, message: "Call to EVM for tokenURI failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - assert(decodedResult.length == 1, message: "Invalid response length") - - return decodedResult[0] as! String - } - - /// Retrieves the contract URI from the given EVM contract address. Returns nil on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to retrieve the contractURI from - /// - /// @return the contract's contractURI - /// - access(all) - fun getContractURI(evmContractAddress: EVM.EVMAddress): String? { - let callResult = self.dryCall( - signature: "contractURI()", - targetEVMAddress: evmContractAddress, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if callResult.status != EVM.Status.successful { - return nil - } - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - return decodedResult.length == 1 ? decodedResult[0] as! String : nil - } - - /// Retrieves the number of decimals for a given ERC20 contract address. Reverts on EVM call failure. - /// - /// @param evmContractAddress: The ERC20 contract address to retrieve the token decimals from - /// - /// @return the token decimals of the ERC20 - /// - access(all) - fun getTokenDecimals(evmContractAddress: EVM.EVMAddress): UInt8 { - let callResult = self.dryCall( - signature: "decimals()", - targetEVMAddress: evmContractAddress, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - - assert(callResult.status == EVM.Status.successful, message: "Call for EVM asset decimals failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - assert(decodedResult.length == 1, message: "Invalid response length") - - return decodedResult[0] as! UInt8 - } - - /// Determines if the provided owner address is either the owner or approved for the NFT in the ERC721 contract - /// Reverts on EVM call failure. - /// - /// @param ofNFT: The ID of the NFT to query - /// @param owner: The owner address to query - /// @param evmContractAddress: The ERC721 contract address to query - /// - /// @return true if the owner is either the owner or approved for the NFT, false otherwise - /// - access(all) - fun isOwnerOrApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { - return self.isOwner(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress) || - self.isApproved(ofNFT: ofNFT, owner: owner, evmContractAddress: evmContractAddress) - } - - /// Returns whether the given owner is the owner of the given NFT. Reverts on EVM call failure. - /// - /// @param ofNFT: The ID of the NFT to query - /// @param owner: The owner address to query - /// @param evmContractAddress: The ERC721 contract address to query - /// - /// @return true if the owner is in fact the owner of the NFT, false otherwise - /// - access(all) - fun isOwner(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { - return self.ownerOf(id: ofNFT, evmContractAddress: evmContractAddress)?.equals(owner) ?? false - } - - /// Returns the owner of a given ERC721 token - /// - /// @param id: The ID of the NFT to query - /// @param evmContractAddress: The ERC721 contract address to query - /// - /// @return The current owner's EVM address or nil if the `ownerOf` call is unsuccessful - /// - access(all) - fun ownerOf(id: UInt256, evmContractAddress: EVM.EVMAddress): EVM.EVMAddress? { - let callResult = self.dryCall( - signature: "ownerOf(uint256)", - targetEVMAddress: evmContractAddress, - args: [id], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if callResult.status == EVM.Status.failed { - return nil - } - let decodedCallResult = EVM.decodeABI(types: [Type()], data: callResult.data) - return decodedCallResult.length == 1 ? decodedCallResult[0] as! EVM.EVMAddress : nil - } - - /// Returns whether the given owner is approved for the given NFT. Reverts on EVM call failure. - /// - /// @param ofNFT: The ID of the NFT to query - /// @param owner: The owner address to query - /// @param evmContractAddress: The ERC721 contract address to query - /// - /// @return true if the owner is in fact approved for the NFT, false otherwise - /// - access(all) - fun isApproved(ofNFT: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { - let callResult = self.dryCall( - signature: "getApproved(uint256)", - targetEVMAddress: evmContractAddress, - args: [ofNFT], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(callResult.status == EVM.Status.successful, message: "Call to ERC721.getApproved(uint256) failed") - let decodedCallResult = EVM.decodeABI(types: [Type()], data: callResult.data) - if decodedCallResult.length == 1 { - let actualApproved = decodedCallResult[0] as! EVM.EVMAddress - return actualApproved.equals(owner) - } - 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()], - data: self.dryCall( - signature: "exists(uint256)", - targetEVMAddress: erc721Address, - args: [id], - gasLimit: FlowEVMBridgeConfig.gasLimit, - 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 owner: The owner address to query - /// @param evmContractAddress: The ERC20 contract address to query - /// - /// @return The UInt256 balance of the owner at the ERC20 contract address. Callers may wish to convert the return - /// value to a UFix64 via convertERC20AmountToCadenceAmount, though note there may be a loss of precision. - /// - access(all) - fun balanceOf(owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): UInt256 { - let callResult = self.dryCall( - signature: "balanceOf(address)", - targetEVMAddress: evmContractAddress, - args: [owner], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.balanceOf(address) failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - assert(decodedResult.length == 1, message: "Invalid response length") - return decodedResult[0] as! UInt256 - } - - /// Determines if the owner has sufficient funds to bridge the given amount at the ERC20 contract address - /// Reverts on EVM call failure. - /// - /// @param amount: The amount to check if the owner has enough balance to cover - /// @param owner: The owner address to query - /// @param evmContractAddress: The ERC20 contract address to query - /// - /// @return true if the owner's balance >= amount, false otherwise - /// - access(all) - fun hasSufficientBalance(amount: UInt256, owner: EVM.EVMAddress, evmContractAddress: EVM.EVMAddress): Bool { - return self.balanceOf(owner: owner, evmContractAddress: evmContractAddress) >= amount - } - - /// Retrieves the total supply of the ERC20 contract at the given EVM contract address. Reverts on EVM call failure. - /// - /// @param evmContractAddress: The EVM contract address to retrieve the total supply from - /// - /// @return the total supply of the ERC20 - /// - access(all) - fun totalSupply(evmContractAddress: EVM.EVMAddress): UInt256 { - let callResult = self.dryCall( - signature: "totalSupply()", - targetEVMAddress: evmContractAddress, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(callResult.status == EVM.Status.successful, message: "Call to ERC20.totalSupply() failed") - let decodedResult = EVM.decodeABI(types: [Type()], data: callResult.data) as! [AnyStruct] - assert(decodedResult.length == 1, message: "Invalid response length") - return decodedResult[0] as! UInt256 - } - - /// Converts the given amount of ERC20 tokens to the equivalent amount in FLOW tokens based on the ERC20s decimals - /// value. Note that may be some loss of decimal precision as UFix64 supports precision for 8 decimal places. - /// Reverts on EVM call failure. - /// - /// @param amount: The amount of ERC20 tokens to convert - /// @param erc20Address: The EVM contract address of the ERC20 token - /// - /// @return the equivalent amount in FLOW tokens as a UFix64 - /// - access(all) - fun convertERC20AmountToCadenceAmount(_ amount: UInt256, erc20Address: EVM.EVMAddress): UFix64 { - return self.uint256ToUFix64( - value: amount, - decimals: self.getTokenDecimals(evmContractAddress: erc20Address) - ) - } - - /// Converts the given amount of Cadence fungible tokens to the equivalent amount in ERC20 tokens based on the - /// ERC20s decimals. Note that there may be some loss of decimal precision as UFix64 supports precision for 8 - /// decimal places. Reverts on EVM call failure. - /// - /// @param amount: The amount of Cadence fungible tokens to convert - /// @param erc20Address: The EVM contract address of the ERC20 token - /// - /// @return the equivalent amount in ERC20 tokens as a UInt256 - /// - access(all) - fun convertCadenceAmountToERC20Amount(_ amount: UFix64, erc20Address: EVM.EVMAddress): UInt256 { - return self.ufix64ToUInt256(value: amount, decimals: self.getTokenDecimals(evmContractAddress: erc20Address)) - } - - /// Gets the declared Cadence contract address declared by an EVM contract in conformance to the ICrossVM.sol - /// contract interface. Reverts if the EVM call is unsuccessful. - /// NOTE: Just because an EVM contract declares an association does not mean it it is valid! - /// - /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the declared Cadence - /// contract address - /// - /// @return The resulting Cadence Address as declared associated by the provided EVM contract or nil if the call fails - /// - access(all) - fun getDeclaredCadenceAddressFromCrossVM(evmContract: EVM.EVMAddress): Address? { - let cadenceAddrRes = self.dryCall( - signature: "getCadenceAddress()", - targetEVMAddress: evmContract, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if cadenceAddrRes.status != EVM.Status.successful { - return nil - } - let decodedCadenceAddr = EVM.decodeABI(types: [Type()], data: cadenceAddrRes.data) - assert(decodedCadenceAddr.length == 1) - var cadenceAddrStr = decodedCadenceAddr[0] as! String - if cadenceAddrStr[1] != "x" { - cadenceAddrStr = "0x".concat(cadenceAddrStr) - } - return Address.fromString(cadenceAddrStr) ?? nil - } - - /// Gets the declared Cadence Type declared by an EVM contract in conformance to the ICrossVM.sol contract - /// interface. Reverts if the EVM call is unsuccessful. - /// NOTE: Just because an EVM contract declares an association does not mean it it is valid! - /// - /// @param evmContract: The ICrossVM.sol conforming EVM contract from which to retrieve the declared Cadence - /// Type - /// - /// @return The resulting Cadence Type as declared associated by the provided EVM contract or nil if the call fails - /// - /// - access(all) - fun getDeclaredCadenceTypeFromCrossVM(evmContract: EVM.EVMAddress): Type? { - let cadenceIdentifierRes = self.dryCall( - signature: "getCadenceIdentifier()", - targetEVMAddress: evmContract, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if cadenceIdentifierRes.status != EVM.Status.successful { - return nil - } - let decodedCadenceIdentifier = EVM.decodeABI(types: [Type()], data: cadenceIdentifierRes.data) - assert(decodedCadenceIdentifier.length == 1) - let cadenceIdentifier = decodedCadenceIdentifier[0] as! String - return CompositeType(cadenceIdentifier) ?? nil - } - - /// Returns whether the provided EVM contract conforms to ICrossVMBridgeERC721Fulfillment.sol contract interface. - /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully - /// registered - /// - /// @param evmContract: The EVM contract to check for ICrossVMBridgeERC721 conformance - /// - /// @return True if conformance is found, false otherwise - /// - access(all) - fun supportsICrossVMBridgeERC721Fulfillment(evmContract: EVM.EVMAddress): Bool { - let interfaceID = EVM.EVMBytes4(value: "2e608d70".decodeHex().toConstantSized<[UInt8; 4]>()!) - let supportsRes = self.dryCall( - signature: "supportsInterface(bytes4)", - targetEVMAddress: evmContract, - args: [interfaceID], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if supportsRes.status != EVM.Status.successful { - return false - } - let decodedSupports = EVM.decodeABI(types: [Type()], data: supportsRes.data) - if decodedSupports.length != 1 { - return false - } - return decodedSupports[0] as! Bool - } - - /// Returns whether the provided EVM contract conforms to ICrossVMBridgeCallable.sol contract interface. - /// Doing so is one of two interfaces that must be implemented for Cadence-native cross-VM NFTs to be successfully - /// registered - /// - /// @param evmContract: The EVM contract to check for ICrossVMBridgeCallable conformance - /// - /// @return True if conformance is found, false otherwise - /// - access(all) - fun supportsICrossVMBridgeCallable(evmContract: EVM.EVMAddress): Bool { - let interfaceID = EVM.EVMBytes4(value: "b7f9a9ec".decodeHex().toConstantSized<[UInt8; 4]>()!) - let supportsRes = self.dryCall( - signature: "supportsInterface(bytes4)", - targetEVMAddress: evmContract, - args: [interfaceID], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if supportsRes.status != EVM.Status.successful { - return false - } - let decodedSupports = EVM.decodeABI(types: [Type()], data: supportsRes.data) - if decodedSupports.length != 1 { - return false - } - return decodedSupports[0] as! Bool - } - - /// Returns whether the provided EVM contract conforms to both ICrossVMBridgeERC721Fulfillment and - /// ICrossVMBridgeCallable Solidity contract interfaces - /// - /// @param evmContract: The EVM contract to check for conformance - /// - /// @return True if conformance is found, false otherwise - /// - access(all) - fun supportsCadenceNativeNFTEVMInterfaces(evmContract: EVM.EVMAddress): Bool { - return self.supportsICrossVMBridgeCallable(evmContract: evmContract) - && self.supportsICrossVMBridgeERC721Fulfillment(evmContract: evmContract) - } - - /// Returns the VM Bridge address designated by the ICrossVMBridgeCallable conforming EVM contract. Reverts on call - /// failure. - /// - /// @param evmContract: The ICrossVMBridgeCallable EVM contract from which to retrieve the value - /// - /// @return The EVM address designated as the VM bridge address in the provided contract - /// - access(all) - fun getVMBridgeAddressFromICrossVMBridgeCallable(evmContract: EVM.EVMAddress): EVM.EVMAddress? { - let cadenceIdentifierRes = self.dryCall( - signature: "vmBridgeAddress()", - targetEVMAddress: evmContract, - args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - if cadenceIdentifierRes.status != EVM.Status.successful { - return nil - } - let decodedCadenceIdentifier = EVM.decodeABI(types: [Type()], data: cadenceIdentifierRes.data) - return decodedCadenceIdentifier.length == 1 ? decodedCadenceIdentifier[0] as! EVM.EVMAddress : nil - } - - /************************ - Derivation Utils - ************************/ - - /// Derives the StoragePath where the escrow locker is stored for a given Type of asset & returns. The given type - /// must be of an asset supported by the bridge. - /// - /// @param fromType: The type of the asset the escrow locker is being derived for - /// - /// @return The StoragePath associated with the type's escrow Locker, or nil if the type is not supported - /// - access(all) - view fun deriveEscrowStoragePath(fromType: Type): StoragePath? { - if !self.isValidCadenceAsset(type: fromType) { - return nil - } - var prefix = "" - if fromType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) { - prefix = "flowEVMBridgeNFTEscrow" - } else if fromType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { - prefix = "flowEVMBridgeTokenEscrow" - } - assert(prefix.length > 1, message: "Invalid prefix") - if let splitIdentifier = self.splitObjectIdentifier(identifier: fromType.identifier) { - let sourceContractAddress = Address.fromString("0x".concat(splitIdentifier[1]))! - let sourceContractName = splitIdentifier[2] - let resourceName = splitIdentifier[3] - return StoragePath( - identifier: prefix.concat(self.delimiter) - .concat(sourceContractAddress.toString()).concat(self.delimiter) - .concat(sourceContractName).concat(self.delimiter) - .concat(resourceName) - ) ?? nil - } - return nil - } - - /// Derives the Cadence contract name for a given EVM NFT of the form - /// EVMVMBridgedNFT_<0xCONTRACT_ADDRESS> - /// - /// @param from evmContract: The EVM contract address to derive the Cadence NFT contract name for - /// - /// @return The derived Cadence FT contract name - /// - access(all) - view fun deriveBridgedNFTContractName(from evmContract: EVM.EVMAddress): String { - return self.contractNamePrefixes[Type<@{NonFungibleToken.NFT}>()]!["bridged"]! - .concat(self.delimiter) - .concat(evmContract.toString()) - } - - /// Derives the Cadence contract name for a given EVM fungible token of the form - /// EVMVMBridgedToken_<0xCONTRACT_ADDRESS> - /// - /// @param from evmContract: The EVM contract address to derive the Cadence FT contract name for - /// - /// @return The derived Cadence FT contract name - /// - access(all) - view fun deriveBridgedTokenContractName(from evmContract: EVM.EVMAddress): String { - return self.contractNamePrefixes[Type<@{FungibleToken.Vault}>()]!["bridged"]! - .concat(self.delimiter) - .concat(evmContract.toString()) - } - - /**************** - Math Utils - ****************/ - - /// Raises the base to the power of the exponent - /// - access(all) - view fun pow(base: UInt256, exponent: UInt8): UInt256 { - if exponent == 0 { - return 1 - } - - var r = base - var exp: UInt8 = 1 - while exp < exponent { - r = r * base - exp = exp + 1 - } - - return r - } - - /// Raises the fixed point base to the power of the exponent - /// - access(all) - view fun ufixPow(base: UFix64, exponent: UInt8): UFix64 { - if exponent == 0 { - return 1.0 - } - - var r = base - var exp: UInt8 = 1 - while exp < exponent { - r = r * base - exp = exp + 1 - } - - return r - } - - /// Converts a UFix64 to a UInt256 - // - access(all) - view fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 { - // Default to 10e8 scale, catching instances where decimals are less than default and scale appropriately - let ufixScaleExp: UInt8 = decimals < 8 ? decimals : 8 - var ufixScale = self.ufixPow(base: 10.0, exponent: ufixScaleExp) - - // Separate the fractional and integer parts of the UFix64 - let integer = UInt256(value) - var fractional = (value % 1.0) * ufixScale - - // Calculate the multiplier for integer and fractional parts - var integerMultiplier: UInt256 = self.pow(base:10, exponent: decimals) - let fractionalMultiplierExp: UInt8 = decimals < 8 ? 0 : decimals - 8 - var fractionalMultiplier: UInt256 = self.pow(base:10, exponent: fractionalMultiplierExp) - - // Scale and sum the parts - return integer * integerMultiplier + UInt256(fractional) * fractionalMultiplier - } - - /// Converts a UInt256 to a UFix64 - /// - access(all) - view fun uint256ToUFix64(value: UInt256, decimals: UInt8): UFix64 { - // Calculate scale factors for the integer and fractional parts - let absoluteScaleFactor = self.pow(base: 10, exponent: decimals) - - // Separate the integer and fractional parts of the value - let scaledValue = value / absoluteScaleFactor - var fractional = value % absoluteScaleFactor - // Scale the fractional part - let scaledFractional = self.uint256FractionalToScaledUFix64Decimals(value: fractional, decimals: decimals) - - // Ensure the parts do not exceed the max UFix64 value before conversion - assert( - scaledValue <= UInt256(UFix64.max), - message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value") - ) - /// Check for the max value that can be converted to a UFix64 without overflowing - assert( - scaledValue == UInt256(UFix64.max) ? scaledFractional < 0.09551616 : true, - message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value") - ) - - return UFix64(scaledValue) + scaledFractional - } - - /// Converts a UInt256 fractional value with the given decimal places to a scaled UFix64. Note that UFix64 has - /// decimal precision of 8 places so converted values may lose precision and be rounded down. - /// - access(all) - view fun uint256FractionalToScaledUFix64Decimals(value: UInt256, decimals: UInt8): UFix64 { - pre { - self.getNumberOfDigits(value) <= decimals: "Fractional digits exceed the defined decimal places" - } - post { - result < 1.0: "Resulting scaled fractional exceeds 1.0" - } - - var fractional = value - // Truncate fractional to the first 8 decimal places which is the max precision for UFix64 - if decimals >= 8 { - fractional = fractional / self.pow(base: 10, exponent: decimals - 8) - } - // Return early if the truncated fractional part is now 0 - if fractional == 0 { - return 0.0 - } - - // Scale the fractional part - let fractionalMultiplier = self.ufixPow(base: 0.1, exponent: decimals < 8 ? decimals : 8) - return UFix64(fractional) * fractionalMultiplier - } - - /// Returns the value as a UInt64 if it fits, otherwise panics - /// - access(all) - view fun uint256ToUInt64(value: UInt256): UInt64 { - return value <= UInt256(UInt64.max) ? UInt64(value) : panic("Value too large to fit into UInt64") - } - - /// Returns the number of digits in the given UInt256 - /// - access(all) - view fun getNumberOfDigits(_ value: UInt256): UInt8 { - var tmp = value - var digits: UInt8 = 0 - while tmp > 0 { - tmp = tmp / 10 - digits = digits + 1 - } - return digits - } - - /*************************** - Type Identifier Utils - ***************************/ - - /// Returns the contract address from the given Type - /// - /// @param fromType: The Type to extract the contract address from - /// - /// @return The defining contract's Address, or nil if the identifier does not have an associated Address - /// - access(all) - view fun getContractAddress(fromType: Type): Address? { - return fromType.address - } - - /// Returns the defining contract name from the given Type - /// - /// @param fromType: The Type to extract the contract name from - /// - /// @return The defining contract's name, or nil if the identifier does not have an associated contract name - /// - access(all) - view fun getContractName(fromType: Type): String? { - return fromType.contractName - } - - /// Returns the object's name from the given Type's identifier where the identifier is in the format - /// of: A... - /// - /// @param fromType: The Type to extract the object name from - /// - /// @return The object's name, or nil if the identifier does identify an object - /// - access(all) - view fun getObjectName(fromType: Type): String? { - if let identifierSplit = self.splitObjectIdentifier(identifier: fromType.identifier) { - return identifierSplit[3] - } - return nil - } - - /// Splits the given identifier into its constituent parts defined by a delimiter of '".'" - /// - /// @param identifier: The identifier to split - /// - /// @return An array of the identifier's constituent parts, or nil if the identifier does not have 4 parts - /// - access(all) - view fun splitObjectIdentifier(identifier: String): [String]? { - let identifierSplit = identifier.split(separator: ".") - return identifierSplit.length != 4 ? nil : identifierSplit - } - - /// Builds a composite type from the given identifier parts - /// - /// @param address: The defining contract address - /// @param contractName: The defining contract name - /// @param resourceName: The resource name - /// - access(all) - view fun buildCompositeType(address: Address, contractName: String, resourceName: String): Type? { - let addressStr = address.toString() - let subtract0x = addressStr.slice(from: 2, upTo: addressStr.length) - let identifier = "A".concat(".").concat(subtract0x).concat(".").concat(contractName).concat(".").concat(resourceName) - return CompositeType(identifier) - } - - /************************** - FungibleToken Utils - **************************/ - - /// Returns the `createEmptyVault()` function from a Vault Type's defining contract or nil if either the Type is not - access(all) fun getCreateEmptyVaultFunction(forType: Type): (fun (Type): @{FungibleToken.Vault})? { - // We can only reasonably assume that the requested function is accessible from a FungibleToken contract - if !forType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { - return nil - } - // Vault Types should guarantee that the following forced optionals are safe - let contractAddress = self.getContractAddress(fromType: forType)! - let contractName = self.getContractName(fromType: forType)! - let tokenContract: &{FungibleToken} = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>( - name: contractName - )! - return tokenContract.createEmptyVault - } - - /****************************** - Bridge-Access Only Utils - ******************************/ - - /// Deposits fees to the bridge account's FlowToken Vault - helps fund asset storage - /// - access(account) - fun depositFee(_ feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, feeAmount: UFix64) { - let vault = self.account.storage.borrow<&FlowToken.Vault>(from: /storage/flowTokenVault) - ?? panic("Could not borrow FlowToken.Vault reference") - - let feeVault <-feeProvider.withdraw(amount: feeAmount) as! @FlowToken.Vault - assert(feeVault.balance == feeAmount, message: "Fee provider did not return the requested fee") - - vault.deposit(from: <-feeVault) - } - - /// Enables other bridge contracts to orchestrate bridge operations from contract-owned COA - /// - access(account) - view fun borrowCOA(): auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount { - return self.account.storage.borrow( - from: FlowEVMBridgeConfig.coaStoragePath - ) ?? panic("Could not borrow COA reference") - } - - /// Shared helper simplifying calls using the bridge account's COA - /// - access(account) - fun call( - signature: String, - targetEVMAddress: EVM.EVMAddress, - args: [AnyStruct], - gasLimit: UInt64, - value: UFix64 - ): EVM.Result { - let calldata = EVM.encodeABIWithSignature(signature, args) - let valueBalance = EVM.Balance(attoflow: 0) - valueBalance.setFLOW(flow: value) - return self.borrowCOA().call( - to: targetEVMAddress, - data: calldata, - gasLimit: gasLimit, - value: valueBalance - ) - } - - /// Shared helper simplifying dryCalls using the bridge account's COA. Note that `COA.dryCall` does not execute the - /// call within EVM, serving solely as a mechanism for retrieving data from Flow-EVM environment. - /// - access(account) - fun dryCall( - signature: String, - targetEVMAddress: EVM.EVMAddress, - args: [AnyStruct], - gasLimit: UInt64, - value: UFix64 - ): EVM.Result { - let calldata = EVM.encodeABIWithSignature(signature, args) - let valueBalance = EVM.Balance(attoflow: 0) - valueBalance.setFLOW(flow: value) - return self.borrowCOA().dryCall( - to: targetEVMAddress, - data: calldata, - gasLimit: gasLimit, - value: valueBalance - ) - } - - /// 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 ERC721 requesting to be transferred") - assert(!toPreStatus, message: "Recipient already owns ERC721 attempting to be transferred") - - let transferResult: EVM.Result = self.call( - signature: "safeTransferFrom(address,address,uint256)", - targetEVMAddress: erc721Address, - args: [bridgeCOAAddress, to, id], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert( - transferResult.status == EVM.Status.successful, - message: "safeTransferFrom call to ERC721 transferring NFT from escrow 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: "ERC721 is still in escrow after transfer") - assert(toPostStatus, message: "ERC721 was not successfully transferred to recipient from escrow") - } - - /// Executes a safeMint call on the given ERC721 contract address, minting an ERC721 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 = self.call( - signature: "safeMint(address,uint256,string)", - targetEVMAddress: erc721Address, - args: [to, id, uri], - gasLimit: FlowEVMBridgeConfig.gasLimit, - 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 a safeMint call on the given ERC721 contract address, minting an ERC721 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 mustFulfillNFTToEVM(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, maybeBytes: EVM.EVMBytes?) { - let fulfillResult = self.call( - signature: "fulfillToEVM(address,uint256,bytes)", - targetEVMAddress: erc721Address, - args: [to, id, maybeBytes ?? EVM.EVMBytes(value: [])], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert( - fulfillResult.status == EVM.Status.successful, - message: "Fulfill ERC721 \(erc721Address.toString()) with id \(id) to \(to.toString()) failed with error code \(fulfillResult.errorCode): \(fulfillResult.errorMessage)" - ) - - 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 = self.call( - signature: "updateTokenURI(uint256,string)", - targetEVMAddress: erc721Address, - args: [id, uri], - gasLimit: FlowEVMBridgeConfig.gasLimit, - 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 mustEscrowERC721( - owner: EVM.EVMAddress, - id: UInt256, - erc721Address: EVM.EVMAddress, - protectedTransferCall: fun (EVM.EVMAddress): 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 ERC721") - - // Call the protected transfer function which should execute a transfer call from the owner to escrow - let transferResult = protectedTransferCall(erc721Address) - assert(transferResult.status == EVM.Status.successful, message: "Transfer ERC721 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: "ERC721 was not successfully escrowed") - } - - /// Unwraps an ERC721 token, calling `ERC721Wrapper.withdrawTo(address,uint256[])` on the provided wrapper address - /// and ensuring that the underlying ERC721 is owned by the bridge COA before returning. - /// NOTE: This method relies on implementation of OpenZeppelin's `ERC721Wrapper` contract interface, reverting if - /// the unwrap operation is unsuccessful. - /// - access(account) - fun mustUnwrapERC721( - id: UInt256, - erc721WrapperAddress: EVM.EVMAddress, - underlyingEVMAddress: EVM.EVMAddress - ) { - assert( - self.isOwner(ofNFT: id, owner: erc721WrapperAddress, evmContractAddress: underlyingEVMAddress), - message: "Attempting to unwrap \(underlyingEVMAddress.toString()) ID \(id), but token is not wrapped by \(erc721WrapperAddress.toString())" - ) - let bridgeCOA = self.getBridgeCOAEVMAddress() - - let unwrapResult: EVM.Result = self.call( - signature: "withdrawTo(address,uint256[])", - targetEVMAddress: erc721WrapperAddress, - args: [bridgeCOA, [id]], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert( - unwrapResult.status == EVM.Status.successful, - message: "Call to \(erc721WrapperAddress.toString()) ERC721Wrapper.withdrawTo(address,uint256[]) failed" - ) - - assert( - self.isOwner(ofNFT: id, owner: bridgeCOA, evmContractAddress: underlyingEVMAddress), - message: "Unsuccessful escrow of wrapped ERC721 \(erc721WrapperAddress.toString()) wrapping underlying \(underlyingEVMAddress.toString()) ID \(id)" - ) - } - - /// Mints ERC20 tokens to the recipient and confirms that the recipient's balance was updated - /// - access(account) - fun mustMintERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) { - let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) - // Mint tokens to the recipient - let mintResult: EVM.Result = self.call( - signature: "mint(address,uint256)", - targetEVMAddress: erc20Address, - args: [to, amount], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge ERC20 contract failed") - // Ensure bridge to recipient was succcessful - let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) - assert( - toPostBalance == toPreBalance + amount, - message: "Recipient didn't receive minted ERC20 tokens during bridging" - ) - } - - /// Transfers ERC20 tokens to the recipient and confirms that the recipient's balance was incremented and the escrow - /// balance was decremented by the requested amount. - /// - access(account) - fun mustTransferERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) { - let bridgeCOAAddress = self.getBridgeCOAEVMAddress() - - let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) - let escrowPreBalance = self.balanceOf( - owner: bridgeCOAAddress, - evmContractAddress: erc20Address - ) - - // Transfer tokens to the recipient - let transferResult: EVM.Result = self.call( - signature: "transfer(address,uint256)", - targetEVMAddress: erc20Address, - args: [to, amount], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(transferResult.status == EVM.Status.successful, message: "transfer call to ERC20 contract failed") - - // Ensure bridge to recipient was succcessful - let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) - let escrowPostBalance = self.balanceOf( - owner: bridgeCOAAddress, - evmContractAddress: erc20Address - ) - assert( - toPostBalance == toPreBalance + amount, - message: "Recipient's ERC20 balance did not increment by the requested amount after transfer from escrow" - ) - assert( - escrowPostBalance == escrowPreBalance - amount, - message: "Escrow ERC20 balance did not decrement by the requested amount after transfer from escrow" - ) - } - - /// Executes the provided method, assumed to be a protected transfer call, and confirms that the transfer was - /// successful by validating that the named owner's balance was decremented by the requested amount and the bridge - /// escrow balance was incremented by the same amount. - /// - access(account) - fun mustEscrowERC20( - owner: EVM.EVMAddress, - amount: UInt256, - erc20Address: EVM.EVMAddress, - protectedTransferCall: fun (): EVM.Result - ) { - // Ensure the caller is has sufficient balance to bridge the requested amount - let hasSufficientBalance = self.hasSufficientBalance( - amount: amount, - owner: owner, - evmContractAddress: erc20Address - ) - assert(hasSufficientBalance, message: "Caller does not have sufficient balance to bridge requested tokens") - - // Get the owner and escrow balances before transfer - let ownerPreBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) - let bridgePreBalance = self.balanceOf( - owner: self.getBridgeCOAEVMAddress(), - evmContractAddress: erc20Address - ) - - // 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 via callback failed") - - // Get the resulting balances after transfer - let ownerPostBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) - let bridgePostBalance = self.balanceOf( - owner: self.getBridgeCOAEVMAddress(), - evmContractAddress: erc20Address - ) - - // Confirm the transfer of the expected was successful in both sending owner and recipient escrow - assert(ownerPostBalance == ownerPreBalance - amount, message: "Transfer to owner failed") - assert(bridgePostBalance == bridgePreBalance + amount, message: "Transfer to bridge escrow failed") - } - - /// Executes a `burn(uint256)` call targeting the provided ERC721 contract address. Reverts if the call is - /// unsuccessful - /// - access(account) - fun mustBurnERC721(erc721Address: EVM.EVMAddress, id: UInt256) { - let burnResult = FlowEVMBridgeUtils.call( - signature: "burn(uint256)", - targetEVMAddress: erc721Address, - args: [id], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(burnResult.status == EVM.Status.successful, - message: "0x\(erc721Address.toString()).burn(\(id)) failed with error code \(burnResult.errorCode) and message: \(burnResult.errorMessage)") - } - - /// Calls to the bridge factory to deploy an ERC721/ERC20 contract and returns the deployed contract address - /// - access(account) - fun mustDeployEVMContract( - name: String, - symbol: String, - cadenceAddress: Address, - flowIdentifier: String, - contractURI: String, - isERC721: Bool - ): EVM.EVMAddress { - let deployerTag = isERC721 ? "ERC721" : "ERC20" - let deployResult: EVM.Result = self.call( - signature: "deploy(string,string,string,string,string,string)", - targetEVMAddress: self.bridgeFactoryEVMAddress, - args: [deployerTag, name, symbol, cadenceAddress.toString(), flowIdentifier, contractURI], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ) - assert(deployResult.status == EVM.Status.successful, message: "EVM Token contract deployment failed") - let decodedResult: [AnyStruct] = EVM.decodeABI(types: [Type()], data: deployResult.data) - assert(decodedResult.length == 1, message: "Invalid response length") - return decodedResult[0] as! EVM.EVMAddress - } - - /// Calls `setSymbol(string)` on the EVM contract as exposed on FlowEVMBridgedERC721 contracts, enabling Cadence - /// NFTs to update their EVM symbol via EVMBridgedMetadata.symbol. The call's status is returned so conditional - /// execution can be handled on the caller's end. - /// - access(account) - fun tryUpdateSymbol(_ evmContractAddress: EVM.EVMAddress, symbol: String): Bool { - return self.call( - signature: "setSymbol(string)", - targetEVMAddress: evmContractAddress, - args: [symbol], - gasLimit: FlowEVMBridgeConfig.gasLimit, - value: 0.0 - ).status == EVM.Status.successful - } - - init(bridgeFactoryAddressHex: String) { - self.delimiter = "_" - self.contractNamePrefixes = { - Type<@{NonFungibleToken.NFT}>(): { - "bridged": "EVMVMBridgedNFT" - }, - Type<@{FungibleToken.Vault}>(): { - "bridged": "EVMVMBridgedToken" - } - } - self.bridgeFactoryEVMAddress = EVM.addressFromString(bridgeFactoryAddressHex.toLower()) - } -} diff --git a/contracts/external/FungibleToken.cdc b/contracts/external/FungibleToken.cdc deleted file mode 100644 index 9b634118c..000000000 --- a/contracts/external/FungibleToken.cdc +++ /dev/null @@ -1,332 +0,0 @@ -/** - -# The Flow Fungible Token standard - -## `FungibleToken` contract - -If a users wants to deploy a new token contract, their contract -needs to implement the FungibleToken interface and their tokens -need to implement the interfaces defined in this contract. - -/// Contributors (please add to this list if you contribute!): -/// - Joshua Hannan - https://github.com/joshuahannan -/// - Bastian Müller - https://twitter.com/turbolent -/// - Dete Shirley - https://twitter.com/dete73 -/// - Bjarte Karlsen - https://twitter.com/0xBjartek -/// - Austin Kline - https://twitter.com/austin_flowty -/// - Giovanni Sanchez - https://twitter.com/gio_incognito -/// - Deniz Edincik - https://twitter.com/bluesign -/// - Jonny - https://github.com/dryruner -/// -/// Repo reference: https://github.com/onflow/flow-ft - -## `Vault` resource interface - -Each fungible token resource type needs to implement the `Vault` resource interface. - -## `Provider`, `Receiver`, and `Balance` resource interfaces - -These interfaces declare pre-conditions and post-conditions that restrict -the execution of the functions in the Vault. - -It gives users the ability to make custom resources that implement -these interfaces to do various things with the tokens. -For example, a faucet can be implemented by conforming -to the Provider interface. - -*/ - -import ViewResolver from 0x1d7e57aa55817448 -import Burner from 0xf233dcee88fe0abe - -/// FungibleToken -/// -/// Fungible Token implementations should implement the fungible token -/// interface. -access(all) contract interface FungibleToken: ViewResolver { - - // An entitlement for allowing the withdrawal of tokens from a Vault - access(all) entitlement Withdraw - - /// The event that is emitted when tokens are withdrawn - /// from any Vault that implements the `Vault` interface - access(all) event Withdrawn(type: String, - amount: UFix64, - from: Address?, - fromUUID: UInt64, - withdrawnUUID: UInt64, - balanceAfter: UFix64) - - /// The event that is emitted when tokens are deposited to - /// any Vault that implements the `Vault` interface - access(all) event Deposited(type: String, - amount: UFix64, - to: Address?, - toUUID: UInt64, - depositedUUID: UInt64, - balanceAfter: UFix64) - - /// Event that is emitted when the global `Burner.burn()` method - /// is called with a non-zero balance - access(all) event Burned(type: String, amount: UFix64, fromUUID: UInt64) - - /// Balance - /// - /// The interface that provides a standard field - /// for representing balance - /// - access(all) resource interface Balance: Burner.Burnable { - access(all) var balance: UFix64 - - // This default implementation needs to be in a separate interface - // from the one in `Vault` so that the conditions get enforced - // in the correct one - access(contract) fun burnCallback() { - self.balance = 0.0 - } - } - - /// Provider - /// - /// The interface that enforces the requirements for withdrawing - /// tokens from the implementing type. - /// - /// It does not enforce requirements on `balance` here, - /// because it leaves open the possibility of creating custom providers - /// that do not necessarily need their own balance. - /// - access(all) resource interface Provider { - - /// Function to ask a provider if a specific amount of tokens - /// is available to be withdrawn - /// This could be useful to avoid panicing when calling withdraw - /// when the balance is unknown - /// Additionally, if the provider is pulling from multiple vaults - /// it only needs to check some of the vaults until the desired amount - /// is reached, potentially helping with performance. - /// - /// @param amount the amount of tokens requested to potentially withdraw - /// @return Bool Whether or not this amount is available to withdraw - /// - access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool - - /// withdraw subtracts tokens from the implementing resource - /// and returns a Vault with the removed tokens. - /// - /// The function's access level is `access(Withdraw)` - /// So in order to access it, one would either need the object itself - /// or an entitled reference with `Withdraw`. - /// - /// @param amount the amount of tokens to withdraw from the resource - /// @return The Vault with the withdrawn tokens - /// - access(Withdraw) fun withdraw(amount: UFix64): @{Vault} { - post { - // `result` refers to the return value - result.balance == amount: - "FungibleToken.Provider.withdraw: Cannot withdraw tokens!" - .concat("The balance of the withdrawn tokens (").concat(result.balance.toString()) - .concat(") is not equal to the amount requested to be withdrawn (") - .concat(amount.toString()).concat(")") - } - } - } - - /// Receiver - /// - /// The interface that enforces the requirements for depositing - /// tokens into the implementing type. - /// - /// We do not include a condition that checks the balance because - /// we want to give users the ability to make custom receivers that - /// can do custom things with the tokens, like split them up and - /// send them to different places. - /// - access(all) resource interface Receiver { - - /// deposit takes a Vault and deposits it into the implementing resource type - /// - /// @param from the Vault that contains the tokens to deposit - /// - access(all) fun deposit(from: @{Vault}) - - /// getSupportedVaultTypes returns a dictionary of Vault types - /// and whether the type is currently supported by this Receiver - /// - /// @return {Type: Bool} A dictionary that indicates the supported types - /// If a type is not supported, it should be `nil`, not false - /// - access(all) view fun getSupportedVaultTypes(): {Type: Bool} - - /// Returns whether or not the given type is accepted by the Receiver - /// A vault that can accept any type should just return true by default - /// - /// @param type The type to query about - /// @return Bool Whether or not the vault type is supported - /// - access(all) view fun isSupportedVaultType(type: Type): Bool - } - - /// Vault - /// Conforms to all other interfaces so that implementations - /// only have to conform to `Vault` - /// - access(all) resource interface Vault: Receiver, Provider, Balance, ViewResolver.Resolver, Burner.Burnable { - - /// Field that tracks the balance of a vault - access(all) var balance: UFix64 - - /// Called when a fungible token is burned via the `Burner.burn()` method - /// Implementations can do any bookkeeping or emit any events - /// that should be emitted when a vault is destroyed. - /// Many implementations will want to update the token's total supply - /// to reflect that the tokens have been burned and removed from the supply. - /// Implementations also need to set the balance to zero before the end of the function - /// This is to prevent vault owners from spamming fake Burned events. - access(contract) fun burnCallback() { - pre { - emit Burned(type: self.getType().identifier, amount: self.balance, fromUUID: self.uuid) - } - post { - self.balance == 0.0: - "FungibleToken.Vault.burnCallback: Cannot burn this Vault with Burner.burn(). " - .concat("The balance must be set to zero during the burnCallback method so that it cannot be spammed.") - } - } - - /// getSupportedVaultTypes - /// The default implementation is included here because vaults are expected - /// to only accepted their own type, so they have no need to provide an implementation - /// for this function - /// - access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - // Below check is implemented to make sure that run-time type would - // only get returned when the parent resource conforms with `FungibleToken.Vault`. - if self.getType().isSubtype(of: Type<@{FungibleToken.Vault}>()) { - return {self.getType(): true} - } else { - // Return an empty dictionary as the default value for resource who don't - // implement `FungibleToken.Vault`, such as `FungibleTokenSwitchboard`, `TokenForwarder` etc. - return {} - } - } - - /// Checks if the given type is supported by this Vault - access(all) view fun isSupportedVaultType(type: Type): Bool { - return self.getSupportedVaultTypes()[type] ?? false - } - - /// withdraw subtracts `amount` from the Vault's balance - /// and returns a new Vault with the subtracted balance - /// - access(Withdraw) fun withdraw(amount: UFix64): @{Vault} { - pre { - self.balance >= amount: - "FungibleToken.Vault.withdraw: Cannot withdraw tokens! " - .concat("The amount requested to be withdrawn (").concat(amount.toString()) - .concat(") is greater than the balance of the Vault (") - .concat(self.balance.toString()).concat(").") - } - post { - result.getType() == self.getType(): - "FungibleToken.Vault.withdraw: Cannot withdraw tokens! " - .concat("The withdraw method tried to return an incompatible Vault type <") - .concat(result.getType().identifier).concat(">. ") - .concat("It must return a Vault with the same type as self <") - .concat(self.getType().identifier).concat(">.") - - // use the special function `before` to get the value of the `balance` field - // at the beginning of the function execution - // - self.balance == before(self.balance) - amount: - "FungibleToken.Vault.withdraw: Cannot withdraw tokens! " - .concat("The sender's balance after the withdrawal (") - .concat(self.balance.toString()) - .concat(") must be the difference of the previous balance (").concat(before(self.balance.toString())) - .concat(") and the amount withdrawn (").concat(amount.toString()).concat(")") - - emit Withdrawn( - type: result.getType().identifier, - amount: amount, - from: self.owner?.address, - fromUUID: self.uuid, - withdrawnUUID: result.uuid, - balanceAfter: self.balance - ) - } - } - - /// deposit takes a Vault and adds its balance to the balance of this Vault - /// - access(all) fun deposit(from: @{FungibleToken.Vault}) { - // Assert that the concrete type of the deposited vault is the same - // as the vault that is accepting the deposit - pre { - from.isInstance(self.getType()): - "FungibleToken.Vault.deposit: Cannot deposit tokens! " - .concat("The type of the deposited tokens <") - .concat(from.getType().identifier) - .concat("> has to be the same type as the Vault being deposited into <") - .concat(self.getType().identifier) - .concat(">. Check that you are withdrawing and depositing to the correct paths in the sender and receiver accounts ") - .concat("and that those paths hold the same Vault types.") - } - post { - emit Deposited( - type: before(from.getType().identifier), - amount: before(from.balance), - to: self.owner?.address, - toUUID: self.uuid, - depositedUUID: before(from.uuid), - balanceAfter: self.balance - ) - self.balance == before(self.balance) + before(from.balance): - "FungibleToken.Vault.deposit: Cannot deposit tokens! " - .concat("The receiver's balance after the deposit (") - .concat(self.balance.toString()) - .concat(") must be the sum of the previous balance (").concat(before(self.balance.toString())) - .concat(") and the amount deposited (").concat(before(from.balance).toString()).concat(")") - } - } - - /// createEmptyVault allows any user to create a new Vault that has a zero balance - /// - /// @return A Vault of the same type that has a balance of zero - access(all) fun createEmptyVault(): @{Vault} { - post { - result.balance == 0.0: - "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! " - .concat("The newly created Vault must have zero balance but it has a balance of ") - .concat(result.balance.toString()) - - result.getType() == self.getType(): - "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! " - .concat("The type of the new Vault <") - .concat(result.getType().identifier) - .concat("> has to be the same type as the Vault that created it <") - .concat(self.getType().identifier) - .concat(">.") - } - } - } - - /// createEmptyVault allows any user to create a new Vault that has a zero balance - /// - /// @return A Vault of the requested type that has a balance of zero - access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { - post { - result.balance == 0.0: - "FungibleToken.createEmptyVault: Empty Vault creation failed! " - .concat("The newly created Vault must have zero balance but it has a balance of (") - .concat(result.balance.toString()).concat(")") - - result.getType() == vaultType: - "FungibleToken.Vault.createEmptyVault: Empty Vault creation failed! " - .concat("The type of the new Vault <") - .concat(result.getType().identifier) - .concat("> has to be the same as the type that was requested <") - .concat(vaultType.identifier) - .concat(">.") - } - } -} \ No newline at end of file diff --git a/contracts/external/FungibleTokenMetadataViews.cdc b/contracts/external/FungibleTokenMetadataViews.cdc deleted file mode 100644 index eb57f6b75..000000000 --- a/contracts/external/FungibleTokenMetadataViews.cdc +++ /dev/null @@ -1,186 +0,0 @@ -import FungibleToken from 0xf233dcee88fe0abe -import MetadataViews from 0x1d7e57aa55817448 -import ViewResolver from 0x1d7e57aa55817448 - -/// This contract implements the metadata standard proposed -/// in FLIP-1087. -/// -/// Ref: https://github.com/onflow/flips/blob/main/application/20220811-fungible-tokens-metadata.md -/// -/// Structs and resources can implement one or more -/// metadata types, called views. Each view type represents -/// a different kind of metadata. -/// -access(all) contract FungibleTokenMetadataViews { - - /// FTView wraps FTDisplay and FTVaultData, and is used to give a complete - /// picture of a Fungible Token. Most Fungible Token contracts should - /// implement this view. - /// - access(all) struct FTView { - access(all) let ftDisplay: FTDisplay? - access(all) let ftVaultData: FTVaultData? - view init( - ftDisplay: FTDisplay?, - ftVaultData: FTVaultData? - ) { - self.ftDisplay = ftDisplay - self.ftVaultData = ftVaultData - } - } - - /// Helper to get a FT view. - /// - /// @param viewResolver: A reference to the resolver resource - /// @return A FTView struct - /// - access(all) fun getFTView(viewResolver: &{ViewResolver.Resolver}): FTView { - let maybeFTView = viewResolver.resolveView(Type()) - if let ftView = maybeFTView { - return ftView as! FTView - } - return FTView( - ftDisplay: self.getFTDisplay(viewResolver), - ftVaultData: self.getFTVaultData(viewResolver) - ) - } - - /// View to expose the information needed to showcase this FT. - /// This can be used by applications to give an overview and - /// graphics of the FT. - /// - access(all) struct FTDisplay { - /// The display name for this token. - /// - /// Example: "Flow" - /// - access(all) let name: String - - /// The abbreviated symbol for this token. - /// - /// Example: "FLOW" - access(all) let symbol: String - - /// A description the provides an overview of this token. - /// - /// Example: "The FLOW token is the native currency of the Flow network." - access(all) let description: String - - /// External link to a URL to view more information about the fungible token. - access(all) let externalURL: MetadataViews.ExternalURL - - /// One or more versions of the fungible token logo. - access(all) let logos: MetadataViews.Medias - - /// Social links to reach the fungible token's social homepages. - /// Possible keys may be "instagram", "twitter", "discord", etc. - access(all) let socials: {String: MetadataViews.ExternalURL} - - view init( - name: String, - symbol: String, - description: String, - externalURL: MetadataViews.ExternalURL, - logos: MetadataViews.Medias, - socials: {String: MetadataViews.ExternalURL} - ) { - self.name = name - self.symbol = symbol - self.description = description - self.externalURL = externalURL - self.logos = logos - self.socials = socials - } - } - - /// Helper to get FTDisplay in a way that will return a typed optional. - /// - /// @param viewResolver: A reference to the resolver resource - /// @return An optional FTDisplay struct - /// - access(all) fun getFTDisplay(_ viewResolver: &{ViewResolver.Resolver}): FTDisplay? { - if let maybeDisplayView = viewResolver.resolveView(Type()) { - if let displayView = maybeDisplayView as? FTDisplay { - return displayView - } - } - return nil - } - - /// View to expose the information needed store and interact with a FT vault. - /// This can be used by applications to setup a FT vault with proper - /// storage and public capabilities. - /// - access(all) struct FTVaultData { - /// Path in storage where this FT vault is recommended to be stored. - access(all) let storagePath: StoragePath - - /// Public path which must be linked to expose the public receiver capability. - access(all) let receiverPath: PublicPath - - /// Public path which must be linked to expose the balance and resolver public capabilities. - access(all) let metadataPath: PublicPath - - /// Type that should be linked at the `receiverPath`. This is a restricted type requiring - /// the `FungibleToken.Receiver` interface. - access(all) let receiverLinkedType: Type - - /// Type that should be linked at the `receiverPath`. This is a restricted type requiring - /// the `ViewResolver.Resolver` interfaces. - access(all) let metadataLinkedType: Type - - /// Function that allows creation of an empty FT vault that is intended - /// to store the funds. - access(all) let createEmptyVault: fun(): @{FungibleToken.Vault} - - view init( - storagePath: StoragePath, - receiverPath: PublicPath, - metadataPath: PublicPath, - receiverLinkedType: Type, - metadataLinkedType: Type, - createEmptyVaultFunction: fun(): @{FungibleToken.Vault} - ) { - pre { - receiverLinkedType.isSubtype(of: Type<&{FungibleToken.Receiver}>()): - "Receiver public type <".concat(receiverLinkedType.identifier) - .concat("> must be a subtype of <").concat(Type<&{FungibleToken.Receiver}>().identifier) - .concat(">.") - metadataLinkedType.isSubtype(of: Type<&{FungibleToken.Vault}>()): - "Metadata linked type <".concat(metadataLinkedType.identifier) - .concat("> must be a subtype of <").concat(Type<&{FungibleToken.Vault}>().identifier) - .concat(">.") - } - self.storagePath = storagePath - self.receiverPath = receiverPath - self.metadataPath = metadataPath - self.receiverLinkedType = receiverLinkedType - self.metadataLinkedType = metadataLinkedType - self.createEmptyVault = createEmptyVaultFunction - } - } - - /// Helper to get FTVaultData in a way that will return a typed Optional. - /// - /// @param viewResolver: A reference to the resolver resource - /// @return A optional FTVaultData struct - /// - access(all) fun getFTVaultData(_ viewResolver: &{ViewResolver.Resolver}): FTVaultData? { - if let view = viewResolver.resolveView(Type()) { - if let v = view as? FTVaultData { - return v - } - } - return nil - } - - /// View to expose the total supply of the Vault's token - access(all) struct TotalSupply { - access(all) let supply: UFix64 - - view init(totalSupply: UFix64) { - self.supply = totalSupply - } - } -} - \ No newline at end of file diff --git a/contracts/external/FungibleTokenSwitchboard.cdc b/contracts/external/FungibleTokenSwitchboard.cdc deleted file mode 100644 index 10d8ae820..000000000 --- a/contracts/external/FungibleTokenSwitchboard.cdc +++ /dev/null @@ -1,376 +0,0 @@ -import FungibleToken from 0xf233dcee88fe0abe - -/// The contract that allows an account to receive payments in multiple fungible -/// tokens using a single `{FungibleToken.Receiver}` capability. -/// This capability should ideally be stored at the -/// `FungibleTokenSwitchboard.ReceiverPublicPath = /public/GenericFTReceiver` -/// but it can be stored anywhere. -/// -access(all) contract FungibleTokenSwitchboard { - - // Storage and Public Paths - access(all) let StoragePath: StoragePath - access(all) let PublicPath: PublicPath - access(all) let ReceiverPublicPath: PublicPath - - access(all) entitlement Owner - - /// The event that is emitted when a new vault capability is added to a - /// switchboard resource. - /// - access(all) event VaultCapabilityAdded(type: Type, switchboardOwner: Address?, - capabilityOwner: Address?) - - /// The event that is emitted when a vault capability is removed from a - /// switchboard resource. - /// - access(all) event VaultCapabilityRemoved(type: Type, switchboardOwner: Address?, - capabilityOwner: Address?) - - /// The event that is emitted when a deposit can not be completed. - /// - access(all) event NotCompletedDeposit(type: Type, amount: UFix64, - switchboardOwner: Address?) - - /// The interface that enforces the method to allow anyone to check on the - /// available capabilities of a switchboard resource and also exposes the - /// deposit methods to deposit funds on it. - /// - access(all) resource interface SwitchboardPublic { - access(all) view fun getVaultTypesWithAddress(): {Type: Address} - access(all) view fun getSupportedVaultTypes(): {Type: Bool} - access(all) view fun isSupportedVaultType(type: Type): Bool - access(all) fun deposit(from: @{FungibleToken.Vault}) - access(all) fun safeDeposit(from: @{FungibleToken.Vault}): @{FungibleToken.Vault}? - access(all) view fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? - } - - /// The resource that stores the multiple fungible token receiver - /// capabilities, allowing the owner to add and remove them and anyone to - /// deposit any fungible token among the available types. - /// - access(all) resource Switchboard: FungibleToken.Receiver, SwitchboardPublic { - - /// Dictionary holding the fungible token receiver capabilities, - /// indexed by the fungible token vault type. - /// - access(contract) var receiverCapabilities: {Type: Capability<&{FungibleToken.Receiver}>} - - /// Adds a new fungible token receiver capability to the switchboard - /// resource. - /// - /// @param capability: The capability to expose a certain fungible - /// token vault deposit function through `{FungibleToken.Receiver}` that - /// will be added to the switchboard. - /// - access(Owner) fun addNewVault(capability: Capability<&{FungibleToken.Receiver}>) { - // Borrow a reference to the vault pointed to by the capability we - // want to store inside the switchboard - let vaultRef = capability.borrow() - ?? panic("FungibleTokenSwitchboard.Switchboard.addNewVault: Cannot borrow reference to vault from capability! " - .concat("Make sure that the capability path points to a Vault that has been properly initialized. ")) - - // Check if there is a previous capability for this token - if (self.receiverCapabilities[vaultRef.getType()] == nil) { - // use the vault reference type as key for storing the - // capability and then - self.receiverCapabilities[vaultRef.getType()] = capability - // emit the event that indicates that a new capability has been - // added - emit VaultCapabilityAdded(type: vaultRef.getType(), - switchboardOwner: self.owner?.address, - capabilityOwner: capability.address) - } else { - // If there was already a capability for that token, panic - panic("FungibleTokenSwitchboard.Switchboard.addNewVault: Cannot add new Vault capability! " - .concat("There is already a vault in the Switchboard for this type <") - .concat(vaultRef.getType().identifier).concat(">.")) - } - } - - /// Adds a number of new fungible token receiver capabilities by using - /// the paths where they are stored. - /// - /// @param paths: The paths where the public capabilities are stored. - /// @param address: The address of the owner of the capabilities. - /// - access(Owner) fun addNewVaultsByPath(paths: [PublicPath], address: Address) { - // Get the account where the public capabilities are stored - let owner = getAccount(address) - // For each path, get the saved capability and store it - // into the switchboard's receiver capabilities dictionary - for path in paths { - let capability = owner.capabilities.get<&{FungibleToken.Receiver}>(path) - // Borrow a reference to the vault pointed to by the capability - // we want to store inside the switchboard - // If the vault was borrowed successfully... - if let vaultRef = capability.borrow() { - // ...and if there is no previous capability added for that token - if (self.receiverCapabilities[vaultRef!.getType()] == nil) { - // Use the vault reference type as key for storing the - // capability - self.receiverCapabilities[vaultRef!.getType()] = capability - // and emit the event that indicates that a new - // capability has been added - emit VaultCapabilityAdded(type: vaultRef.getType(), - switchboardOwner: self.owner?.address, - capabilityOwner: address, - ) - } - } - } - } - - /// Adds a new fungible token receiver capability to the switchboard - /// resource specifying which `Type` of `@{FungibleToken.Vault}` can be - /// deposited to it. Use it to include in your switchboard "wrapper" - /// receivers such as a `@TokenForwarding.Forwarder`. It can also be - /// used to overwrite the type attached to a certain capability without - /// having to remove that capability first. - /// - /// @param capability: The capability to expose a certain fungible - /// token vault deposit function through `{FungibleToken.Receiver}` that - /// will be added to the switchboard. - /// - /// @param type: The type of fungible token that can be deposited to that - /// capability, rather than the `Type` from the reference borrowed from - /// said capability - /// - access(Owner) fun addNewVaultWrapper(capability: Capability<&{FungibleToken.Receiver}>, - type: Type) { - // Check if the capability is working - assert ( - capability.check(), - message: - "FungibleTokenSwitchboard.Switchboard.addNewVaultWrapper: Cannot borrow reference to a vault from the provided capability! " - .concat("Make sure that the capability path points to a Vault that has been properly initialized.") - ) - // Use the type parameter as key for the capability - self.receiverCapabilities[type] = capability - // emit the event that indicates that a new capability has been - // added - emit VaultCapabilityAdded( - type: type, - switchboardOwner: self.owner?.address, - capabilityOwner: capability.address, - ) - } - - /// Adds zero or more new fungible token receiver capabilities to the - /// switchboard resource specifying which `Type`s of `@{FungibleToken.Vault}`s - /// can be deposited to it. Use it to include in your switchboard "wrapper" - /// receivers such as a `@TokenForwarding.Forwarder`. It can also be - /// used to overwrite the types attached to certain capabilities without - /// having to remove those capabilities first. - /// - /// @param paths: The paths where the public capabilities are stored. - /// @param types: The types of the fungible token to be deposited on each path. - /// @param address: The address of the owner of the capabilities. - /// - access(Owner) fun addNewVaultWrappersByPath(paths: [PublicPath], types: [Type], - address: Address) { - // Get the account where the public capabilities are stored - let owner = getAccount(address) - // For each path, get the saved capability and store it - // into the switchboard's receiver capabilities dictionary - for i, path in paths { - let capability = owner.capabilities.get<&{FungibleToken.Receiver}>(path) - // Borrow a reference to the vault pointed to by the capability - // we want to store inside the switchboard - // If the vault was borrowed successfully... - if let vaultRef = capability.borrow() { - // Use the vault reference type as key for storing the capability - self.receiverCapabilities[types[i]] = capability - // and emit the event that indicates that a new capability has been added - emit VaultCapabilityAdded( - type: types[i], - switchboardOwner: self.owner?.address, - capabilityOwner: address, - ) - } - } - } - - /// Removes a fungible token receiver capability from the switchboard - /// resource. - /// - /// @param capability: The capability to a fungible token vault to be - /// removed from the switchboard. - /// - access(Owner) fun removeVault(capability: Capability<&{FungibleToken.Receiver}>) { - // Borrow a reference to the vault pointed to by the capability we - // want to remove from the switchboard - let vaultRef = capability.borrow() - ?? panic ("FungibleTokenSwitchboard.Switchboard.addNewVaultWrapper: Cannot borrow reference to a vault from the provided capability! " - .concat("Make sure that the capability path points to a Vault that has been properly initialized.")) - - // Use the vault reference to find the capability to remove - self.receiverCapabilities.remove(key: vaultRef.getType()) - // Emit the event that indicates that a new capability has been - // removed - emit VaultCapabilityRemoved( - type: vaultRef.getType(), - switchboardOwner: self.owner?.address, - capabilityOwner: capability.address, - ) - } - - /// Takes a fungible token vault and routes it to the proper fungible - /// token receiver capability for depositing it. - /// - /// @param from: The deposited fungible token vault resource. - /// - access(all) fun deposit(from: @{FungibleToken.Vault}) { - // Get the capability from the ones stored at the switchboard - let depositedVaultCapability = self.receiverCapabilities[from.getType()] - ?? panic ("FungibleTokenSwitchboard.Switchboard.deposit: Cannot deposit Vault! " - .concat("The deposited vault of type <").concat(from.getType().identifier) - .concat("> is not available on this Fungible Token switchboard. ") - .concat("The recipient needs to initialize their account and switchboard to hold and receive the deposited vault type.")) - - // Borrow the reference to the desired vault - let vaultRef = depositedVaultCapability.borrow() - ?? panic ("FungibleTokenSwitchboard.Switchboard.deposit: Cannot borrow reference to a vault " - .concat("from the type of the deposited Vault <").concat(from.getType().identifier) - .concat(">. Make sure that the capability path points to a Vault that has been properly initialized.")) - - vaultRef.deposit(from: <-from) - } - - /// Takes a fungible token vault and tries to route it to the proper - /// fungible token receiver capability for depositing the funds, - /// avoiding panicking if the vault is not available. - /// - /// @param vaultType: The type of the ft vault that wants to be - /// deposited. - /// - /// @return The deposited fungible token vault resource, without the - /// funds if the deposit was successful, or still containing the funds - /// if the reference to the needed vault was not found. - /// - access(all) fun safeDeposit(from: @{FungibleToken.Vault}): @{FungibleToken.Vault}? { - // Try to get the proper vault capability from the switchboard - // If the desired vault is present on the switchboard... - if let depositedVaultCapability = self.receiverCapabilities[from.getType()] { - // We try to borrow a reference to the vault from the capability - // If we can borrow a reference to the vault... - if let vaultRef = depositedVaultCapability.borrow() { - // We deposit the funds on said vault - vaultRef.deposit(from: <-from.withdraw(amount: from.balance)) - } - } - // if deposit failed for some reason - if from.balance > 0.0 { - emit NotCompletedDeposit( - type: from.getType(), - amount: from.balance, - switchboardOwner: self.owner?.address, - ) - return <-from - } - destroy from - return nil - } - - /// Checks that the capability tied to a type is valid - /// - /// @param vaultType: The type of the ft vault whose capability needs to be checked - /// - /// @return a boolean marking the capability for a type as valid or not - access(all) view fun checkReceiverByType(type: Type): Bool { - if self.receiverCapabilities[type] == nil { - return false - } - - return self.receiverCapabilities[type]!.check() - } - - /// Gets the receiver assigned to a provided vault type. - /// This is necessary because without it, it is not possible to look under the hood and see if a capability - /// is of an expected type or not. This helps guard against infinitely chained TokenForwarding or other invalid - /// malicious kinds of updates that could prevent listings from being made that are valid on storefronts. - /// - /// @param vaultType: The type of the ft vault whose capability needs to be checked - /// - /// @return an optional receiver capability for consumers of the switchboard to check/validate on their own - access(all) view fun safeBorrowByType(type: Type): &{FungibleToken.Receiver}? { - if !self.checkReceiverByType(type: type) { - return nil - } - - return self.receiverCapabilities[type]!.borrow() - } - - /// A getter function to know which tokens a certain switchboard - /// resource is prepared to receive along with the address where - /// those tokens will be deposited. - /// - /// @return A dictionary mapping the `{FungibleToken.Receiver}` - /// type to the receiver owner's address - /// - access(all) view fun getVaultTypesWithAddress(): {Type: Address} { - let effectiveTypesWithAddress: {Type: Address} = {} - // Check if each capability is live - for vaultType in self.receiverCapabilities.keys { - if self.receiverCapabilities[vaultType]!.check() { - // and attach it to the owner's address - effectiveTypesWithAddress[vaultType] = self.receiverCapabilities[vaultType]!.address - } - } - return effectiveTypesWithAddress - } - - /// A getter function that returns the token types supported by this resource, - /// which can be deposited using the 'deposit' function. - /// - /// @return Dictionary of FT types that can be deposited. - access(all) view fun getSupportedVaultTypes(): {Type: Bool} { - let supportedVaults: {Type: Bool} = {} - for receiverType in self.receiverCapabilities.keys { - if self.receiverCapabilities[receiverType]!.check() { - if receiverType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { - supportedVaults[receiverType] = true - } - if receiverType.isSubtype(of: Type<@{FungibleToken.Receiver}>()) { - let receiverRef = self.receiverCapabilities[receiverType]!.borrow()! - let subReceiverSupportedTypes = receiverRef.getSupportedVaultTypes() - for subReceiverType in subReceiverSupportedTypes.keys { - if subReceiverType.isSubtype(of: Type<@{FungibleToken.Vault}>()) { - supportedVaults[subReceiverType] = true - } - } - } - } - } - return supportedVaults - } - - /// Returns whether or not the given type is accepted by the Receiver - /// A vault that can accept any type should just return true by default - access(all) view fun isSupportedVaultType(type: Type): Bool { - let supportedVaults = self.getSupportedVaultTypes() - if let supported = supportedVaults[type] { - return supported - } else { return false } - } - - init() { - // Initialize the capabilities dictionary - self.receiverCapabilities = {} - } - - } - - /// Function that allows to create a new blank switchboard. A user must call - /// this function and store the returned resource in their storage. - /// - access(all) fun createSwitchboard(): @Switchboard { - return <-create Switchboard() - } - - init() { - self.StoragePath = /storage/fungibleTokenSwitchboard - self.PublicPath = /public/fungibleTokenSwitchboardPublic - self.ReceiverPublicPath = /public/GenericFTReceiver - } -} diff --git a/contracts/external/IFlowEVMNFTBridge.cdc b/contracts/external/IFlowEVMNFTBridge.cdc deleted file mode 100644 index 0dbcfd04e..000000000 --- a/contracts/external/IFlowEVMNFTBridge.cdc +++ /dev/null @@ -1,119 +0,0 @@ -import FungibleToken from 0xf233dcee88fe0abe -import NonFungibleToken from 0x1d7e57aa55817448 - -import EVM from 0xe467b9dd11fa00df - -import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141 -import CrossVMNFT from 0x1e4aa0b87d10b141 - -access(all) contract interface IFlowEVMNFTBridge { - - /************* - Events - **************/ - - /// Broadcasts an NFT was bridged from Cadence to EVM - access(all) - event BridgedNFTToEVM( - type: String, - id: UInt64, - uuid: UInt64, - evmID: UInt256, - to: String, - evmContractAddress: String, - bridgeAddress: Address - ) - /// Broadcasts an NFT was bridged from EVM to Cadence - access(all) - event BridgedNFTFromEVM( - type: String, - id: UInt64, - uuid: UInt64, - evmID: UInt256, - caller: String, - evmContractAddress: String, - bridgeAddress: Address - ) - - /************** - Getters - ***************/ - - /// Returns the EVM address associated with the provided type - /// - access(all) - view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? - - /// Returns the EVM address of the bridge coordinating COA - /// - access(all) - view fun getBridgeCOAEVMAddress(): EVM.EVMAddress - - /******************************** - Public Bridge Entrypoints - *********************************/ - - /// Public entrypoint to bridge NFTs from Cadence to EVM. - /// - /// @param token: The NFT to be bridged - /// @param to: The NFT recipient in FlowEVM - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun bridgeNFTToEVM( - token: @{NonFungibleToken.NFT}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - emit BridgedNFTToEVM( - type: token.getType().identifier, - id: token.id, - uuid: token.uuid, - evmID: CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) ?? UInt256(token.id), - to: to.toString(), - evmContractAddress: self.getAssociatedEVMAddress(with: token.getType())?.toString() - ?? panic( - "Could not find EVM Contract address associated with provided NFT identifier=" - .concat(token.getType().identifier) - ), - bridgeAddress: self.account.address - ) - } - } - - /// Public entrypoint to bridge NFTs from EVM to Cadence - /// - /// @param owner: The EVM address of the NFT owner. Current ownership and successful transfer (via - /// `protectedTransferCall`) is validated before the bridge request is executed. - /// @param type: The Cadence Type of the NFT to be bridged. If EVM-native, this would be the Cadence Type associated - /// with the EVM contract on the Flow side at onboarding. - /// @param id: The NFT ID to bridged - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the - /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. - /// - /// @returns The bridged NFT - /// - access(account) - fun bridgeNFTFromEVM( - owner: EVM.EVMAddress, - type: Type, - id: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (EVM.EVMAddress): EVM.Result - ): @{NonFungibleToken.NFT} { - post { - emit BridgedNFTFromEVM( - type: result.getType().identifier, - id: result.id, - uuid: result.uuid, - evmID: id, - caller: owner.toString(), - evmContractAddress: self.getAssociatedEVMAddress(with: result.getType())?.toString() - ?? panic("Could not find EVM Contract address associated with provided NFT"), - bridgeAddress: self.account.address - ) - } - } -} \ No newline at end of file diff --git a/contracts/external/IFlowEVMTokenBridge.cdc b/contracts/external/IFlowEVMTokenBridge.cdc deleted file mode 100644 index 942eb8fea..000000000 --- a/contracts/external/IFlowEVMTokenBridge.cdc +++ /dev/null @@ -1,112 +0,0 @@ -import FungibleToken from 0xf233dcee88fe0abe -import NonFungibleToken from 0x1d7e57aa55817448 - -import EVM from 0xe467b9dd11fa00df - -access(all) contract interface IFlowEVMTokenBridge { - - /************* - Events - **************/ - - /// Broadcasts fungible tokens were bridged from Cadence to EVM - access(all) - event BridgedTokensToEVM( - type: String, - amount: UFix64, - bridgedUUID: UInt64, - to: String, - evmContractAddress: String, - bridgeAddress: Address - ) - /// Broadcasts fungible tokens were bridged from EVM to Cadence - access(all) - event BridgedTokensFromEVM( - type: String, - amount: UInt256, - bridgedUUID: UInt64, - caller: String, - evmContractAddress: String, - bridgeAddress: Address - ) - - /************** - Getters - ***************/ - - /// Returns the EVM address associated with the provided type - /// - access(all) - view fun getAssociatedEVMAddress(with type: Type): EVM.EVMAddress? - - /// Returns the EVM address of the bridge coordinating COA - /// - access(all) - view fun getBridgeCOAEVMAddress(): EVM.EVMAddress - - /******************************** - Public Bridge Entrypoints - *********************************/ - - /// Public entrypoint to bridge fungible tokens from Cadence to EVM. - /// - /// @param token: The token Vault to be bridged - /// @param to: The token recipient in EVM - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// - access(all) - fun bridgeTokensToEVM( - vault: @{FungibleToken.Vault}, - to: EVM.EVMAddress, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) { - pre { - emit BridgedTokensToEVM( - type: vault.getType().identifier, - amount: vault.balance, - bridgedUUID: vault.uuid, - to: to.toString(), - evmContractAddress: self.getAssociatedEVMAddress(with: vault.getType())?.toString() - ?? panic( - "Could not find EVM Contract address associated with provided Token identifier=" - .concat(vault.getType().identifier) - ), - bridgeAddress: self.account.address - ) - } - } - - /// Public entrypoint to bridge fungible tokens from EVM to Cadence - /// - /// @param owner: The EVM address of the token owner. Current ownership and successful transfer (via - /// `protectedTransferCall`) is validated before the bridge request is executed. - /// @param type: The Cadence Type of the fungible token to be bridged. If EVM-native, this would be the Cadence - /// Type associated with the EVM contract on the Flow side at onboarding. - /// @param amount: The amount of tokens to bridge from EVM to Cadence - /// @param feeProvider: A reference to a FungibleToken Provider from which the bridging fee is withdrawn in $FLOW - /// @param protectedTransferCall: A function that executes the transfer of the NFT from the named owner to the - /// bridge's COA. This function is expected to return a Result indicating the status of the transfer call. - /// - /// @returns The bridged NFT - /// - access(account) - fun bridgeTokensFromEVM( - owner: EVM.EVMAddress, - type: Type, - amount: UInt256, - feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}, - protectedTransferCall: fun (): EVM.Result - ): @{FungibleToken.Vault} { - post { - emit BridgedTokensFromEVM( - type: result.getType().identifier, - amount: amount, - bridgedUUID: result.uuid, - caller: owner.toString(), - evmContractAddress: self.getAssociatedEVMAddress(with: result.getType())?.toString() - ?? panic("Could not find EVM Contract address associated with provided Vault"), - bridgeAddress: self.account.address - ) - } - } -} \ No newline at end of file