Skip to content
Draft
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
128 changes: 60 additions & 68 deletions cadence/contracts/bridge/FlowEVMBridge.cdc

Large diffs are not rendered by default.

39 changes: 17 additions & 22 deletions cadence/contracts/bridge/FlowEVMBridgeAccessor.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import "EVM"

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

executed = true
return caller.callWithSigAndArgs(
return caller.call(
to: target,
signature: "safeTransferFrom(address,address,uint256)",
args: [caller.address(), FlowEVMBridge.getBridgeCOAEVMAddress(), id],
data: EVM.encodeABIWithSignature(
"safeTransferFrom(address,address,uint256)",
[caller.address(), FlowEVMBridge.getBridgeCOAEVMAddress(), id]
),
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: 0,
resultTypes: nil
value: EVM.Balance(attoflow: 0)
)
}
// Execute the bridge request
Expand Down Expand Up @@ -119,37 +119,32 @@ 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.ResultDecoded {
fun callback(): EVM.Result {
pre {
!executed: "Callback can only be executed once"
}
post {
executed: "Callback must be executed"
}
executed = true
return caller.callWithSigAndArgs(
to: associatedEVMAddress,
signature: "transfer(address,uint256)",
args: [FlowEVMBridge.getBridgeCOAEVMAddress(), roundedAmount],
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: 0,
resultTypes: nil
value: EVM.Balance(attoflow: 0)
)
}
// Execute the bridge request
return <- FlowEVMBridge.bridgeTokensFromEVM(
owner: caller.address(),
type: type,
amount: roundedAmount,
amount: amount,
feeProvider: feeProvider,
protectedTransferCall: callback
)
Expand Down
10 changes: 2 additions & 8 deletions cadence/contracts/bridge/FlowEVMBridgeConfig.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,8 @@ contract FlowEVMBridgeConfig {
return self.paused
}

/// 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.
/// 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? {
Expand Down
Original file line number Diff line number Diff line change
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.associationsByEVMAddress[evmContractAddress.toString()]!.identifier)"
"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."
}
Expand Down
64 changes: 37 additions & 27 deletions cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ access(all) contract FlowEVMBridgeHandlers {
let amount = tokens.balance
let uintAmount = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(amount, erc20Address: evmAddress)

assert(uintAmount > 0, message: "Amount to bridge must be greater than 0")
assert(uintAmount > UInt256(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.ResultDecoded
protectedTransferCall: fun (): EVM.Result
): @{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 \(self.getType().identifier)")
?? panic("Cannot bridge - Minter not set in ".concat(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 \(self.getType().identifier)"
self.minter == nil: "Minter has already been set in ".concat(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 \(self.getType().identifier) without a minter"
self.minter != nil: "Cannot enable ".concat(self.getType().identifier).concat(" without a minter")
}
self.enabled = true
}
Expand Down Expand Up @@ -262,13 +262,12 @@ 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.callWithSigAndArgs(
let wrapResult = FlowEVMBridgeUtils.call(
signature: "deposit()",
targetEVMAddress: wflowAddress,
args: [],
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: balance,
resultTypes: nil
value: balance
)
assert(wrapResult.status == EVM.Status.successful, message: "Failed to wrap FLOW as WFLOW")

Expand All @@ -277,12 +276,16 @@ access(all) contract FlowEVMBridgeHandlers {
// Cover underflow
assert(
postBalance > preBalance,
message: "Escrowed WFLOW balance did not increment after wrapping FLOW - pre: \(preBalance.toString()) | post: \(postBalance.toString())"
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: \(preBalance + uintAmount).toString()) | actual: \(postBalance - preBalance).toString())"
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
Expand All @@ -304,7 +307,7 @@ access(all) contract FlowEVMBridgeHandlers {
owner: EVM.EVMAddress,
type: Type,
amount: UInt256,
protectedTransferCall: fun (): EVM.ResultDecoded
protectedTransferCall: fun (): EVM.Result
): @{FungibleToken.Vault} {
let wflowAddress = self.getTargetEVMAddress()!

Expand All @@ -315,7 +318,8 @@ access(all) contract FlowEVMBridgeHandlers {
)
assert(
ufixAmount > 0.0,
message: "Requested UInt256 amount \(amount.toString()) converted to 0.0 - try bridging a larger amount to avoid UFix64 precision loss during conversion"
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
Expand All @@ -331,13 +335,12 @@ 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.callWithSigAndArgs(
let unwrapResult = FlowEVMBridgeUtils.call(
signature: "withdraw(uint256)",
targetEVMAddress: wflowAddress,
args: [amount],
gasLimit: FlowEVMBridgeConfig.gasLimit,
value: 0.0,
resultTypes: nil
value: 0.0
)
assert(unwrapResult.status == EVM.Status.successful, message: "Failed to unwrap WFLOW as FLOW")

Expand All @@ -346,29 +349,34 @@ access(all) contract FlowEVMBridgeHandlers {
// Cover underflow
assert(
postBalance > preBalance,
message: "Escrowed FLOW Balance did not increment after unwrapping WFLOW - pre: \(preBalance.toString()) | post: \(postBalance.toString())"
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: \(UInt256(preBalance) + amount).toString()) | actual: \(postBalance - preBalance).toString())"
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.
// 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.
// 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: \(amount.toString()) | actual: \(withdrawBalance.attoflow.toString())"
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: \(ufixAmount.toString()) | actual: \(flowVault.balance.toString())"
message: "Resulting FLOW Vault balance does not match requested amount - expected: "
.concat(ufixAmount.toString())
.concat(" | actual: ")
.concat(flowVault.balance.toString())
)
return <-flowVault
}
Expand All @@ -380,13 +388,15 @@ access(all) contract FlowEVMBridgeHandlers {
/// Sets the target type for the handler
access(FlowEVMBridgeHandlerInterfaces.Admin)
fun setTargetType(_ type: Type) {
panic("WFLOWTokenHandler has targetType set to \(self.targetType.identifier) at initialization")
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 \(self.targetEVMAddress.toString()) at initialization")
panic("WFLOWTokenHandler has EVMAddress set to "
.concat(self.targetEVMAddress.toString()).concat(" at initialization"))
}

/// Sets the target type for the handler
Expand Down
Loading
Loading