Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wflow handler #137

Merged
merged 20 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6b0507d
add WFLOWTokenHandler definition
sisyphusSmiling Sep 18, 2024
a8dbbe8
update FlowEVMBridgeConfig init block
sisyphusSmiling Sep 18, 2024
1da5a87
remove checks on FlowToken Vault type from bridge
sisyphusSmiling Sep 18, 2024
a506223
fix FlowEVMBridgeUtils.borrowCOA()
sisyphusSmiling Sep 18, 2024
53ce62f
add initial WFLOW handler tests
sisyphusSmiling Sep 19, 2024
0de2049
Merge branch 'main' into add-wflow-handler
sisyphusSmiling Sep 19, 2024
940d174
remove previewnet from flow.json
sisyphusSmiling Oct 22, 2024
5924724
Merge branch 'main' into add-wflow-handler
sisyphusSmiling Dec 2, 2024
6ddfaaf
update Handler interfaces and WFLOWTokenHandler impl
sisyphusSmiling Dec 3, 2024
8c3241c
fix WFLOWHandler WFLOW withdrawal call
sisyphusSmiling Dec 3, 2024
7fb155c
update error messages
sisyphusSmiling Dec 4, 2024
9708f10
remove redundant test case
sisyphusSmiling Dec 6, 2024
80afa22
update README
sisyphusSmiling Dec 6, 2024
769a6a1
add TokenHandler disabling path & tests
sisyphusSmiling Dec 18, 2024
215b6e1
remove unused test lines
sisyphusSmiling Dec 18, 2024
3a958b9
Merge branch 'main' into add-wflow-handler
sisyphusSmiling Dec 20, 2024
027f676
add ability to remove FlowToken.Vault association to FlowEVMBridgeConfig
sisyphusSmiling Dec 20, 2024
4f9050d
fix Config.getEVMAddressAssociated which failed for handled tokens
sisyphusSmiling Dec 20, 2024
33ba50e
remove unused import
sisyphusSmiling Dec 20, 2024
8545872
fix type:evm association when setting TokenHandler evm address
sisyphusSmiling Dec 21, 2024
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
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,17 @@ side, it must escrow on the other as the native VM contract is owned by an exter

With fungible tokens in particular, there may be some cases where the Cadence contract is not deployed to the bridge
account, but the bridge still follows a mint/burn pattern in Cadence. These cases are handled via
[`TokenHandler`](./cadence/contracts/bridge/interfaces/FlowEVMBridgeHandlerInterfaces.cdc) implementations. Also know
that moving $FLOW to EVM is built into the `EVMAddress` object so any requests bridging $FLOW to EVM will simply
leverage this interface; however, moving $FLOW from EVM to Cadence must be done through the COA resource.
[`TokenHandler`](./cadence/contracts/bridge/interfaces/FlowEVMBridgeHandlerInterfaces.cdc) implementations.

Also know that moving $FLOW to EVM is built into the `EVMAddress` object via `EVMAddress.deposit(from: @FlowToken.Vault)`.
Conversely, moving $FLOW from EVM is facilitated via the `CadenceOwnedAccount.withdraw(balance: Balance): @FlowToken.Vault`
method. Given these existing interfaces, the bridge instead handles $FLOW as corresponding fungible token
implementations - `FungibleToken.Vault` in Cadence & ERC20 in EVM. Therefore, the bridge wraps $FLOW en route to EVM
(depositing WFLOW to the recipient) and unwraps WFLOW when bridging when moving from EVM. In short, the cross-VM
association for $FLOW as far as the bridge is concerned is `@FlowToken.Vault` <> WFLOW
(`0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e` on
[Testnet](https://evm-testnet.flowscan.io/address/0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e) &
[Mainnet](https://evm.flowscan.io/address/0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e)).

Below are transactions relevant to bridging fungible tokens:
- [`bridge_tokens_to_evm.cdc`](./cadence/transactions/bridge/tokens/bridge_tokens_to_evm.cdc)
Expand Down
8 changes: 0 additions & 8 deletions cadence/contracts/bridge/FlowEVMBridge.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
fun onboardByType(_ type: Type, feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider}) {
pre {
!FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
type != Type<@FlowToken.Vault>():
"$FLOW cannot be bridged via the VM bridge - use the CadenceOwnedAccount interface"
self.typeRequiresOnboarding(type) == true: "Onboarding is not needed for this type"
FlowEVMBridgeUtils.typeAllowsBridging(type):
"This type is not supported as defined by the project's development team"
Expand Down Expand Up @@ -353,11 +351,6 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
/* Handle $FLOW requests via EVM interface & return */
//
let vaultType = vault.getType()
if vaultType == Type<@FlowToken.Vault>() {
let flowVault <-vault as! @FlowToken.Vault
to.deposit(from: <-flowVault)
return
}

// Gather the vault balance before acting on the resource
let vaultBalance = vault.balance
Expand Down Expand Up @@ -446,7 +439,6 @@ contract FlowEVMBridge : IFlowEVMNFTBridge, IFlowEVMTokenBridge {
pre {
!FlowEVMBridgeConfig.isPaused(): "Bridge operations are currently paused"
!type.isSubtype(of: Type<@{NonFungibleToken.Collection}>()): "Mixed asset types are not yet supported"
!type.isInstance(Type<@FlowToken.Vault>()): "Must use the CadenceOwnedAccount interface to bridge $FLOW from EVM"
self.typeRequiresOnboarding(type) == false: "FungibleToken must first be onboarded"
FlowEVMBridgeConfig.isTypePaused(type) == false: "Bridging is currently paused for this token"
}
Expand Down
12 changes: 2 additions & 10 deletions cadence/contracts/bridge/FlowEVMBridgeConfig.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -474,16 +474,8 @@ contract FlowEVMBridgeConfig {
self.gasLimit = 15_000_000
self.paused = true

// Although $FLOW does not have ERC20 address, we associate the the Vault with the EVM address from which
// EVM transfers originate
// See FLIP #223 - https://github.com/onflow/flips/pull/225
let flowOriginationAddress = EVM.EVMAddress(
bytes: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
)
let flowVaultType = Type<@FlowToken.Vault>()
let flowOriginationAddressHex = flowOriginationAddress.toString()
self.registeredTypes = { flowVaultType: TypeEVMAssociation(associated: flowOriginationAddress) }
self.evmAddressHexToType = { flowOriginationAddressHex: flowVaultType }
self.registeredTypes = {}
self.evmAddressHexToType = {}

self.typeToTokenHandlers <- {}

Expand Down
233 changes: 230 additions & 3 deletions cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "Burner"
import "FungibleToken"
import "NonFungibleToken"
import "FlowToken"

import "EVM"

Expand Down Expand Up @@ -191,14 +192,229 @@ access(all) contract FlowEVMBridgeHandlers {
}
}

/// 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. This field is optional in the event the Cadence token
/// contract address is not yet known but the EVM Address must still be filtered via Handler to prevent the
/// contract from being onboarded otherwise.
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
}
}

/// 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 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
///
Expand All @@ -207,15 +423,26 @@ access(all) contract FlowEVMBridgeHandlers {
handlerType: Type,
targetType: Type,
targetEVMAddress: EVM.EVMAddress?,
expectedMinterType: Type
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
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")
Expand Down
4 changes: 2 additions & 2 deletions cadence/contracts/bridge/FlowEVMBridgeUtils.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1039,8 +1039,8 @@ contract FlowEVMBridgeUtils {
/// Enables other bridge contracts to orchestrate bridge operations from contract-owned COA
///
access(account)
view fun borrowCOA(): auth(EVM.Call) &EVM.CadenceOwnedAccount {
return self.account.storage.borrow<auth(EVM.Call) &EVM.CadenceOwnedAccount>(
view fun borrowCOA(): auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount {
return self.account.storage.borrow<auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount>(
from: FlowEVMBridgeConfig.coaStoragePath
) ?? panic("Could not borrow COA reference")
}
Expand Down
Loading
Loading