Smart Contract

FlowEVMBridgeCustomAssociations

A.1e4aa0b87d10b141.FlowEVMBridgeCustomAssociations

Valid From

129,477,001

Deployed

1w ago
Feb 16, 2026, 08:14:21 PM UTC

Dependents

0 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import CrossVMMetadataViews from 0x1d7e57aa55817448
3import EVM from 0xe467b9dd11fa00df
4
5import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141
6
7/// The FlowEVMBridgeCustomAssociations is tasked with preserving custom associations between Cadence assets and their
8/// EVM implementations. These associations should be validated before `saveCustomAssociation` is called by
9/// leveraging the interfaces outlined in FLIP-318 (https://github.com/onflow/flips/issues/318) to ensure that the
10/// declared association is valid and that neither implementation is bridge-defined.
11///
12access(all) contract FlowEVMBridgeCustomAssociations {
13
14    /// Stored associations indexed by Cadence Type
15    access(self) let associationsConfig: @{Type: {FlowEVMBridgeCustomAssociationTypes.CustomConfig}}
16    /// Reverse lookup indexed on serialized EVM contract address
17    access(self) let associationsByEVMAddress: {String: Type}
18
19    /// Event emitted whenever a custom association is established
20    access(all) event CustomAssociationEstablished(
21        type: String,
22        evmContractAddress: String,
23        nativeVMRawValue: UInt8,
24        updatedFromBridged: Bool,
25        fulfillmentMinterType: String?,
26        fulfillmentMinterOrigin: Address?,
27        fulfillmentMinterCapID: UInt64?,
28        fulfillmentMinterUUID: UInt64?,
29        configUUID: UInt64
30    )
31
32    /// Retrieves the EVM address associated with the given Cadence Type if it has been registered as a cross-VM asset
33    ///
34    /// @param with: The Cadence Type to query against
35    ///
36    /// @return The EVM address configured as associated with the provided Cadence Type
37    ///
38    access(all)
39    view fun getEVMAddressAssociated(with type: Type): EVM.EVMAddress? {
40        return self.associationsConfig[type]?.getEVMContractAddress() ?? nil
41    }
42
43    /// Retrieves the Cadence Type associated with the given EVM address if it has been registered as a cross-VM asset
44    ///
45    /// @param with: The EVM contract address to query against
46    ///
47    /// @return The Cadence Type configured as associated with the provided EVM address
48    ///
49    access(all)
50    view fun getTypeAssociated(with evmAddress: EVM.EVMAddress): Type? {
51        return self.associationsByEVMAddress[evmAddress.toString()]
52    }
53
54    /// Returns an EVMPointer containing the data at the time of registration
55    ///
56    /// @param forType: The Cadence Type to query against
57    ///
58    /// @return a copy of the EVMPointer view as registered with the bridge
59    ///
60    access(all)
61    fun getEVMPointerAsRegistered(forType: Type): CrossVMMetadataViews.EVMPointer? {
62        if let config = &self.associationsConfig[forType] as &{FlowEVMBridgeCustomAssociationTypes.CustomConfig}? {
63            return CrossVMMetadataViews.EVMPointer(
64                cadenceType: config.getCadenceType(),
65                cadenceContractAddress: config.getCadenceType().address!,
66                evmContractAddress: config.getEVMContractAddress(),
67                nativeVM: config.getNativeVM()
68            )
69        }
70        return nil
71    }
72
73    /// Returns whether the related CustomConfig is currently paused or not. `nil` is returned if a CustomConfig is not
74    /// found for the given Type
75    ///
76    /// @param forType: The Cadence Type for which to retrieve a registered CustomConfig
77    ///
78    /// @return true if the CustomConfig is paused, false if registered and unpaused, nil if unregistered as a custom
79    ///     association
80    ///
81    access(all)
82    view fun isCustomConfigPaused(forType: Type): Bool? {
83        return self.borrowNFTCustomConfig(forType: forType)?.isPaused() ?? nil
84    }
85
86    /// Returns metadata about a registered CustomConfig
87    ///
88    /// @param forType: The Cadence Type of the registered cross-VM asset
89    ///
90    /// @return The CustomConfigInfo struct if the type is registered, nil otherwise
91    ///
92    access(all)
93    fun getCustomConfigInfo(forType: Type): FlowEVMBridgeCustomAssociationTypes.CustomConfigInfo? {
94        if let config = self.borrowNFTCustomConfig(forType: forType) {
95            let fulfillmentMinterType = config.checkFulfillmentMinter() == true ? config.borrowFulfillmentMinter().getType() : nil
96            return FlowEVMBridgeCustomAssociationTypes.CustomConfigInfo(
97                updatedFromBridged: config.isUpdatedFromBridged(),
98                isPaused: config.isPaused(),
99                fulfillmentMinterType: fulfillmentMinterType,
100                evmPointer: self.getEVMPointerAsRegistered(forType: forType)!
101            )
102        }
103        return nil
104    }
105
106    /// Allows the bridge contracts to preserve a custom association. Will revert if a custom association already exists
107    ///
108    /// @param type: The Cadence Type of the associated asset.
109    /// @param evmContractAddress: The EVM address defining the EVM implementation of the associated asset.
110    /// @param nativeVM: The VM in which the asset is distributed by the project. The bridge will mint/escrow in the non-native
111    ///     VM environment.
112    /// @param updatedFromBridged: Whether the asset was originally onboarded to the bridge via permissionless
113    ///     onboarding. In other words, whether there was first a bridge-defined implementation of the underlying asset.
114    /// @param fulfillmentMinter: An authorized Capability allowing the bridge to fulfill bridge requests moving the
115    ///     underlying asset from EVM. Required if the asset is EVM-native.
116    ///
117    access(account)
118    fun saveCustomAssociation(
119        type: Type,
120        evmContractAddress: EVM.EVMAddress,
121        nativeVM: CrossVMMetadataViews.VM,
122        updatedFromBridged: Bool,
123        fulfillmentMinter: Capability<auth(FlowEVMBridgeCustomAssociationTypes.FulfillFromEVM) &{FlowEVMBridgeCustomAssociationTypes.NFTFulfillmentMinter}>?
124    ) {
125        pre {
126            self.associationsConfig[type] == nil:
127            "Type \(type.identifier) already has a custom association with \(self.borrowNFTCustomConfig(forType: type)!.getEVMContractAddress().toString())"
128            type.isSubtype(of: Type<@{NonFungibleToken.NFT}>()):
129            "Only NFT cross-VM associations are currently supported but \(type.identifier) is not an NFT implementation"
130            self.associationsByEVMAddress[evmContractAddress.toString()] == nil:
131            "EVM Address \(evmContractAddress.toString()) already has a custom association with \(self.borrowNFTCustomConfig(forType: type)!.getCadenceType().identifier)"
132            fulfillmentMinter?.check() ?? true:
133            "The NFTFulfillmentMinter Capability issued from \(fulfillmentMinter!.address.toString()) is invalid. Ensure the Capability is properly issued and active."
134        }
135        let config <- FlowEVMBridgeCustomAssociationTypes.createNFTCustomConfig(
136                type: type,
137                evmContractAddress: evmContractAddress,
138                nativeVM: nativeVM,
139                updatedFromBridged: updatedFromBridged,
140                fulfillmentMinter: fulfillmentMinter
141            )
142        emit CustomAssociationEstablished(
143            type: type.identifier,
144            evmContractAddress: evmContractAddress.toString(),
145            nativeVMRawValue: nativeVM.rawValue,
146            updatedFromBridged: updatedFromBridged,
147            fulfillmentMinterType: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.getType().identifier : nil,
148            fulfillmentMinterOrigin: fulfillmentMinter?.address ?? nil,
149            fulfillmentMinterCapID: fulfillmentMinter?.id ?? nil,
150            fulfillmentMinterUUID: fulfillmentMinter != nil ? fulfillmentMinter!.borrow()!.uuid : nil,
151            configUUID: config.uuid
152        )
153        self.associationsByEVMAddress[config.evmContractAddress.toString()] = type
154        self.associationsConfig[type] <-! config
155    }
156
157    /// Allows bridge contracts to fulfill NFT bridging requests for EVM-native NFTs, using the provided
158    /// NFTFulfillmentMinter Capability provided by the project on cross-VM registration to mint a new NFT.
159    /// **NOTE:** Given the bridge's mint/escrow pattern for the non-native VM, any calls should first check that the 
160    /// requested NFT is not locked in escrow before minting.
161    ///
162    /// @param forType: The Cadence Type of the NFT being fulfilled
163    /// @param id: The ERC721 ID of the requested NFT
164    ///
165    /// @param
166    access(account)
167    fun fulfillNFTFromEVM(forType: Type, id: UInt256): @{NonFungibleToken.NFT} {
168        post {
169            result.getType() == forType:
170            "Requested \(forType.identifier) but got \(result.getType().identifier) on fulfillment from EVM"
171        }
172        let config = self.borrowNFTCustomConfig(forType: forType)
173            ?? panic("No CustomConfig found for type \(forType.identifier) - cannot fulfill NFT \(id) from EVM")
174        let minter = config.borrowFulfillmentMinter()
175        return <- minter.fulfillFromEVM(id: id)
176    }
177
178    /// Sets the associated CustomConfig as paused, preventing bridging operations on the associated implementations.
179    /// Expect a no-op in the event the CustomConfig is already paused
180    ///
181    access(account) fun pauseCustomConfig(forType: Type) {
182        let config = self.borrowNFTCustomConfig(forType: forType)
183            ?? panic("No CustomConfig found for type \(forType.identifier) - cannot pause config that does not exist")
184        if !config.isPaused() {
185            config.setPauseStatus(true)
186        }
187    }
188
189    /// Sets the associated CustomConfig as unpaused, preventing bridging operations on the associated implementations.
190    /// Expect a no-op in the event the CustomConfig is already paused
191    ///
192    access(account) fun unpauseCustomConfig(forType: Type) {
193        let config = self.borrowNFTCustomConfig(forType: forType)
194            ?? panic("No CustomConfig found for type \(forType.identifier) - cannot unpause config that does not exist")
195        if config.isPaused() {
196            config.setPauseStatus(false)
197        }
198    }
199
200    /// Returns a reference to the NFTCustomConfig if it exists, nil otherwise
201    ///
202    access(self) view fun borrowNFTCustomConfig(forType: Type): &FlowEVMBridgeCustomAssociationTypes.NFTCustomConfig? {
203        let config = &self.associationsConfig[forType] as &{FlowEVMBridgeCustomAssociationTypes.CustomConfig}?
204        return config as? &FlowEVMBridgeCustomAssociationTypes.NFTCustomConfig
205    }
206
207
208    init() {
209        self.associationsConfig <- {}
210        self.associationsByEVMAddress = {}
211    }
212}
213