Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 95 additions & 87 deletions cadence/contracts/bridge/FlowEVMBridge.cdc

Large diffs are not rendered by default.

51 changes: 28 additions & 23 deletions cadence/contracts/bridge/FlowEVMBridgeAccessor.cdc
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import NonFungibleToken from 0x1d7e57aa55817448
import FungibleToken from 0xf233dcee88fe0abe
import FlowToken from 0x1654653399040a61
import "NonFungibleToken"
import "FungibleToken"
import "FlowToken"

import EVM from 0xe467b9dd11fa00df
import "EVM"

import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
import FlowEVMBridge from 0x1e4aa0b87d10b141
import "FlowEVMBridgeConfig"
import "FlowEVMBridge"
import "FlowEVMBridgeUtils"

/// This contract defines a mechanism for routing bridge requests from the EVM contract to the Flow-EVM bridge contract
///
Expand Down Expand Up @@ -52,7 +53,7 @@ contract FlowEVMBridgeAccessor {
): @{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 {
fun callback(target: EVM.EVMAddress): EVM.ResultDecoded {
pre {
!executed: "Callback can only be executed once"
FlowEVMBridge.getAssociatedEVMAddress(with: type) ?? FlowEVMBridgeConfig.getLegacyEVMAddressAssociated(with: type) != nil:
Expand All @@ -68,14 +69,13 @@ contract FlowEVMBridgeAccessor {
message: "Target EVM contract \(target.toString()) is not association with NFT Type \(type.identifier) - COA `safeTransferFrom` callback rejected")

executed = true
return caller.call(
return caller.callWithSigAndArgs(
to: target,
data: EVM.encodeABIWithSignature(
"safeTransferFrom(address,address,uint256)",
[caller.address(), FlowEVMBridge.getBridgeCOAEVMAddress(), id]
),
signature: "safeTransferFrom(address,address,uint256)",
args: [caller.address(), FlowEVMBridge.getBridgeCOAEVMAddress(), id],
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: EVM.Balance(attoflow: 0)
value: 0,
resultTypes: nil
)
}
// Execute the bridge request
Expand Down Expand Up @@ -119,32 +119,37 @@ contract FlowEVMBridgeAccessor {
amount: UInt256,
feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
): @{FungibleToken.Vault} {
// Resolve the EVM address associated with the token type
let associatedEVMAddress = FlowEVMBridge.getAssociatedEVMAddress(with: type)
?? panic("No EVM address associated with type")
// Round the requested ERC20 amount down to the maximum precision representable by UFix64 to ensure the
// amount escrowed on the EVM side matches exactly what will be minted or unlocked on the Cadence side,
// preventing sub-UFix64-precision "dust" from being permanently locked in escrow.
let roundedAmount = FlowEVMBridgeUtils.castERC20AmountToCadencePrecision(amount, erc20Address: associatedEVMAddress)
// Define a callback function, enabling the bridge to act on the ephemeral COA reference in scope
var executed = false
fun callback(): EVM.Result {
fun callback(): EVM.ResultDecoded {
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]
),
return caller.callWithSigAndArgs(
to: associatedEVMAddress,
signature: "transfer(address,uint256)",
args: [FlowEVMBridge.getBridgeCOAEVMAddress(), roundedAmount],
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: EVM.Balance(attoflow: 0)
value: 0,
resultTypes: nil
)
}
// Execute the bridge request
return <- FlowEVMBridge.bridgeTokensFromEVM(
owner: caller.address(),
type: type,
amount: amount,
amount: roundedAmount,
feeProvider: feeProvider,
protectedTransferCall: callback
)
Expand Down
18 changes: 12 additions & 6 deletions cadence/contracts/bridge/FlowEVMBridgeConfig.cdc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import EVM from 0xe467b9dd11fa00df
import NonFungibleToken from 0x1d7e57aa55817448
import "EVM"
import "NonFungibleToken"

import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141
import FlowEVMBridgeCustomAssociations from 0x1e4aa0b87d10b141
import "FlowEVMBridgeHandlerInterfaces"
import "FlowEVMBridgeCustomAssociations"

/// This contract is used to store configuration information shared by FlowEVMBridge contracts
///
Expand Down Expand Up @@ -101,8 +101,14 @@ contract FlowEVMBridgeConfig {
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.
/// Returns whether operations for a given Type are paused.
///
/// Note: the return type is `Bool?` for API compatibility, but this function currently always returns a
/// non-nil `Bool`. The three sub-expressions all produce concrete `Bool` values via nil-coalescing or direct
/// comparison. A `nil` return cannot occur under current logic. Call sites should use `== false` (rather than
/// `!= true`) so that a hypothetical future `nil` is treated conservatively as "paused" rather than
/// "not paused". The doc comment claim that `nil` means "not yet onboarded" does not reflect actual behavior
/// and is retained only for historical reference.
///
access(all)
view fun isTypePaused(_ type: Type): Bool? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import FungibleToken from 0xf233dcee88fe0abe
import NonFungibleToken from 0x1d7e57aa55817448
import CrossVMMetadataViews from 0x1d7e57aa55817448
import EVM from 0xe467b9dd11fa00df
import "FungibleToken"
import "NonFungibleToken"
import "CrossVMMetadataViews"
import "EVM"

/// This contract defines types required for custom cross-VM associations as used in FlowEVMBridgeCustomAssociation
/// and in EVM-native cross-VM NFTs.
Expand Down
10 changes: 5 additions & 5 deletions cadence/contracts/bridge/FlowEVMBridgeCustomAssociations.cdc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import NonFungibleToken from 0x1d7e57aa55817448
import CrossVMMetadataViews from 0x1d7e57aa55817448
import EVM from 0xe467b9dd11fa00df
import "NonFungibleToken"
import "CrossVMMetadataViews"
import "EVM"

import FlowEVMBridgeCustomAssociationTypes from 0x1e4aa0b87d10b141
import "FlowEVMBridgeCustomAssociationTypes"

/// 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
Expand Down Expand Up @@ -128,7 +128,7 @@ access(all) contract FlowEVMBridgeCustomAssociations {
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)"
"EVM Address \(evmContractAddress.toString()) already has a custom association with \(self.associationsByEVMAddress[evmContractAddress.toString()]!.identifier)"
fulfillmentMinter?.check() ?? true:
"The NFTFulfillmentMinter Capability issued from \(fulfillmentMinter!.address.toString()) is invalid. Ensure the Capability is properly issued and active."
}
Expand Down
80 changes: 35 additions & 45 deletions cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Burner from 0xf233dcee88fe0abe
import FungibleToken from 0xf233dcee88fe0abe
import NonFungibleToken from 0x1d7e57aa55817448
import FlowToken from 0x1654653399040a61
import "Burner"
import "FungibleToken"
import "NonFungibleToken"
import "FlowToken"

import EVM from 0xe467b9dd11fa00df
import "EVM"

import FlowEVMBridgeHandlerInterfaces from 0x1e4aa0b87d10b141
import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
import "FlowEVMBridgeHandlerInterfaces"
import "FlowEVMBridgeConfig"
import "FlowEVMBridgeUtils"

/// FlowEVMBridgeHandlers
///
Expand Down Expand Up @@ -105,7 +105,7 @@ access(all) contract FlowEVMBridgeHandlers {
let amount = tokens.balance
let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: evmAddress)

assert(uintAmount > UInt256(0), message: "Amount to bridge must be greater than 0")
assert(uintAmount > 0, message: "Amount to bridge must be greater than 0")

Burner.burn(<-tokens)

Expand All @@ -127,7 +127,7 @@ access(all) contract FlowEVMBridgeHandlers {
owner: EVM.EVMAddress,
type: Type,
amount: UInt256,
protectedTransferCall: fun (): EVM.Result
protectedTransferCall: fun (): EVM.ResultDecoded
): @{FungibleToken.Vault} {
let evmAddress = self.getTargetEVMAddress()!

Expand All @@ -147,7 +147,7 @@ access(all) contract FlowEVMBridgeHandlers {

// After state confirmation, mint the tokens and return
let minter = self.borrowMinter()
?? panic("Cannot bridge - Minter not set in ".concat(self.getType().identifier))
?? panic("Cannot bridge - Minter not set in \(self.getType().identifier)")
let minted <- minter.mint(amount: ufixAmount)
return <-minted
}
Expand All @@ -170,7 +170,7 @@ access(all) contract FlowEVMBridgeHandlers {
access(FlowEVMBridgeHandlerInterfaces.Admin)
fun setMinter(_ minter: @{FlowEVMBridgeHandlerInterfaces.TokenMinter}) {
pre {
self.minter == nil: "Minter has already been set in ".concat(self.getType().identifier)
self.minter == nil: "Minter has already been set in \(self.getType().identifier)"
}
self.minter <-! minter
}
Expand All @@ -179,7 +179,7 @@ access(all) contract FlowEVMBridgeHandlers {
access(FlowEVMBridgeHandlerInterfaces.Admin)
fun enableBridging() {
pre {
self.minter != nil: "Cannot enable ".concat(self.getType().identifier).concat(" without a minter")
self.minter != nil: "Cannot enable \(self.getType().identifier) without a minter"
}
self.enabled = true
}
Expand Down Expand Up @@ -262,12 +262,13 @@ access(all) contract FlowEVMBridgeHandlers {
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(
let wrapResult = FlowEVMBridgeUtils.callWithSigAndArgs(
signature: "deposit()",
targetEVMAddress: wflowAddress,
args: [],
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: balance
value: balance,
resultTypes: nil
)
assert(wrapResult.status == EVM.Status.successful, message: "Failed to wrap FLOW as WFLOW")

Expand All @@ -276,16 +277,12 @@ access(all) contract FlowEVMBridgeHandlers {
// Cover underflow
assert(
postBalance > preBalance,
message: "Escrowed WFLOW balance did not increment after wrapping FLOW - pre: "
.concat(preBalance.toString()).concat(" | post: ").concat(postBalance.toString())
message: "Escrowed WFLOW balance did not increment after wrapping FLOW - pre: \(preBalance.toString()) | post: \(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())
message: "Escrowed WFLOW balance after wrapping does not match requested amount - expected: \(preBalance + uintAmount).toString()) | actual: \(postBalance - preBalance).toString())"
)

// Transfer WFLOW to recipient
Expand All @@ -307,7 +304,7 @@ access(all) contract FlowEVMBridgeHandlers {
owner: EVM.EVMAddress,
type: Type,
amount: UInt256,
protectedTransferCall: fun (): EVM.Result
protectedTransferCall: fun (): EVM.ResultDecoded
): @{FungibleToken.Vault} {
let wflowAddress = self.getTargetEVMAddress()!

Expand All @@ -318,8 +315,7 @@ access(all) contract FlowEVMBridgeHandlers {
)
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")
message: "Requested UInt256 amount \(amount.toString()) converted to 0.0 - try bridging a larger amount to avoid UFix64 precision loss during conversion"
)

// Transfers WFLOW to bridge COA as escrow
Expand All @@ -335,12 +331,13 @@ access(all) contract FlowEVMBridgeHandlers {
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(
let unwrapResult = FlowEVMBridgeUtils.callWithSigAndArgs(
signature: "withdraw(uint256)",
targetEVMAddress: wflowAddress,
args: [amount],
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: 0.0
value: 0.0,
resultTypes: nil
)
assert(unwrapResult.status == EVM.Status.successful, message: "Failed to unwrap WFLOW as FLOW")

Expand All @@ -349,34 +346,29 @@ access(all) contract FlowEVMBridgeHandlers {
// Cover underflow
assert(
postBalance > preBalance,
message: "Escrowed FLOW Balance did not increment after unwrapping WFLOW - pre: ".concat(preBalance.toString())
.concat(" | post: ").concat(postBalance.toString())
message: "Escrowed FLOW Balance did not increment after unwrapping WFLOW - pre: \(preBalance.toString()) | post: \(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())
message: "Escrowed WFLOW balance after unwrapping does not match requested amount - expected: \(UInt256(preBalance) + amount).toString()) | actual: \(postBalance - preBalance).toString())"
)

// Withdraw escrowed FLOW from bridge COA
// Withdraw escrowed FLOW from bridge COA.
// EVM.Balance takes a UInt (64-bit on all supported platforms). `UInt(amount)` truncates silently if
// `amount > UInt.max`. The assert immediately below catches any truncation: if truncation occurred,
// `UInt256(withdrawBalance.attoflow) != amount` and the transaction reverts. In practice this cannot
// trigger: total FLOW supply is ~1.25B × 10^18 attoflow ≈ 1.25e27, far below UInt.max (~1.8e19 × 1e9
// = 1.8e28 for 64-bit). No valid WFLOW bridge request can produce an amount large enough to truncate.
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())
message: "Requested balance failed to convert to attoflow - expected: \(amount.toString()) | actual: \(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())
message: "Resulting FLOW Vault balance does not match requested amount - expected: \(ufixAmount.toString()) | actual: \(flowVault.balance.toString())"
)
return <-flowVault
}
Expand All @@ -388,15 +380,13 @@ access(all) contract FlowEVMBridgeHandlers {
/// 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"))
panic("WFLOWTokenHandler has targetType set to \(self.targetType.identifier) 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"))
panic("WFLOWTokenHandler has EVMAddress set to \(self.targetEVMAddress.toString()) at initialization")
}

/// Sets the target type for the handler
Expand Down
18 changes: 9 additions & 9 deletions cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import FungibleToken from 0xf233dcee88fe0abe
import NonFungibleToken from 0x1d7e57aa55817448
import MetadataViews from 0x1d7e57aa55817448
import ViewResolver from 0x1d7e57aa55817448
import FlowToken from 0x1654653399040a61
import "FungibleToken"
import "NonFungibleToken"
import "MetadataViews"
import "ViewResolver"
import "FlowToken"

import EVM from 0xe467b9dd11fa00df
import "EVM"

import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
import CrossVMNFT from 0x1e4aa0b87d10b141
import "FlowEVMBridgeConfig"
import "FlowEVMBridgeUtils"
import "CrossVMNFT"

/// This escrow contract handles the locking of assets that are bridged from Flow to EVM and retrieval of locked
/// assets in escrow when they are bridged back to Flow.
Expand Down
Loading