diff --git a/cmd/util/ledger/migrations/change_contract_code_migration.go b/cmd/util/ledger/migrations/change_contract_code_migration.go index c2715bdc8d0..f21f3b43c4f 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration.go @@ -326,7 +326,11 @@ func SystemContractChanges(chainID flow.ChainID) []SystemContractChange { // EVM related contracts NewSystemContractChange( systemContracts.EVMContract, - evm.ContractCode(systemContracts.FlowToken.Address), + evm.ContractCode( + systemContracts.NonFungibleToken.Address, + systemContracts.FungibleToken.Address, + systemContracts.FlowToken.Address, + ), ), } } diff --git a/fvm/bootstrap.go b/fvm/bootstrap.go index 7ce37e0828b..858fea9d9b6 100644 --- a/fvm/bootstrap.go +++ b/fvm/bootstrap.go @@ -392,7 +392,7 @@ func (b *bootstrapExecutor) Execute() error { b.setStakingAllowlist(service, b.identities.NodeIDs()) // sets up the EVM environment - b.setupEVM(service, fungibleToken, flowToken) + b.setupEVM(service, nonFungibleToken, fungibleToken, flowToken) return nil } @@ -806,7 +806,7 @@ func (b *bootstrapExecutor) setStakingAllowlist( panicOnMetaInvokeErrf("failed to set staking allow-list: %s", txError, err) } -func (b *bootstrapExecutor) setupEVM(serviceAddress, fungibleTokenAddress, flowTokenAddress flow.Address) { +func (b *bootstrapExecutor) setupEVM(serviceAddress, nonFungibleTokenAddress, fungibleTokenAddress, flowTokenAddress flow.Address) { if b.setupEVMEnabled { // account for storage // we dont need to deploy anything to this account, but it needs to exist @@ -817,7 +817,7 @@ func (b *bootstrapExecutor) setupEVM(serviceAddress, fungibleTokenAddress, flowT // deploy the EVM contract to the service account tx := blueprints.DeployContractTransaction( serviceAddress, - stdlib.ContractCode(flowTokenAddress), + stdlib.ContractCode(nonFungibleTokenAddress, fungibleTokenAddress, flowTokenAddress), stdlib.ContractName, ) // WithEVMEnabled should only be used after we create an account for storage diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index ff87ab299a3..048c4f7ee7c 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -1,4 +1,6 @@ import Crypto +import "NonFungibleToken" +import "FungibleToken" import "FlowToken" access(all) @@ -19,6 +21,19 @@ contract EVM { access(all) event FLOWTokensWithdrawn(addressBytes: [UInt8; 20], amount: UFix64) + /// BridgeAccessorUpdated is emitted when the BridgeAccessor Capability + /// is updated in the stored BridgeRouter along with identifying + /// information about both. + access(all) + event BridgeAccessorUpdated( + routerType: Type, + routerUUID: UInt64, + routerAddress: Address, + accessorType: Type, + accessorUUID: UInt64, + accessorAddress: Address + ) + /// EVMAddress is an EVM-compatible address access(all) struct EVMAddress { @@ -288,6 +303,59 @@ contract EVM { value: value.attoflow ) as! Result } + + /// Bridges the given NFT to the EVM environment, requiring a Provider from which to withdraw a fee to fulfill + /// the bridge request + access(all) + fun depositNFT( + nft: @NonFungibleToken.NFT, + feeProvider: &{FungibleToken.Provider} + ) { + EVM.borrowBridgeAccessor().depositNFT(nft: <-nft, to: self.address(), feeProvider: feeProvider) + } + + /// Bridges the given NFT from the EVM environment, requiring a Provider from which to withdraw a fee to fulfill + /// the bridge request. Note: the caller should own the requested NFT in EVM + access(all) + fun withdrawNFT( + type: Type, + id: UInt256, + feeProvider: &{FungibleToken.Provider} + ): @NonFungibleToken.NFT { + return <- EVM.borrowBridgeAccessor().withdrawNFT( + caller: &self as &CadenceOwnedAccount, + type: type, + id: id, + feeProvider: feeProvider + ) + } + + /// Bridges the given Vault to the EVM environment, requiring a Provider from which to withdraw a fee to fulfill + /// the bridge request + access(all) + fun depositTokens( + vault: @FungibleToken.Vault, + feeProvider: &{FungibleToken.Provider} + ) { + EVM.borrowBridgeAccessor().depositTokens(vault: <-vault, to: self.address(), feeProvider: feeProvider) + } + + /// Bridges the given fungible tokens from the EVM environment, requiring a Provider from which to withdraw a + /// fee to fulfill the bridge request. Note: the caller should own the requested tokens & sufficient balance of + /// requested tokens in EVM + access(all) + fun withdrawTokens( + type: Type, + amount: UInt256, + feeProvider: &{FungibleToken.Provider} + ): @FungibleToken.Vault { + return <- EVM.borrowBridgeAccessor().withdrawTokens( + caller: &self as &CadenceOwnedAccount, + type: type, + amount: amount, + feeProvider: feeProvider + ) + } } /// Creates a new cadence owned account @@ -492,4 +560,66 @@ contract EVM { fun getLatestBlock(): EVMBlock { return InternalEVM.getLatestBlock() as! EVMBlock } + + /// Interface for a resource which acts as an entrypoint to the VM bridge + access(all) + resource interface BridgeAccessor { + + /// Endpoint enabling the bridging of an NFT to EVM + access(all) + fun depositNFT( + nft: @NonFungibleToken.NFT, + to: EVMAddress, + feeProvider: &{FungibleToken.Provider} + ) + + /// Endpoint enabling the bridging of an NFT from EVM + access(all) + fun withdrawNFT( + caller: &CadenceOwnedAccount, + type: Type, + id: UInt256, + feeProvider: &{FungibleToken.Provider} + ): @NonFungibleToken.NFT + + /// Endpoint enabling the bridging of a fungible token vault to EVM + access(all) + fun depositTokens( + vault: @FungibleToken.Vault, + to: EVMAddress, + feeProvider: &{FungibleToken.Provider} + ) + + /// Endpoint enabling the bridging of fungible tokens from EVM + access(all) + fun withdrawTokens( + caller: &CadenceOwnedAccount, + type: Type, + amount: UInt256, + feeProvider: &{FungibleToken.Provider} + ): @FungibleToken.Vault + } + + /// Interface which captures a Capability to the bridge Accessor, saving it within the BridgeRouter resource + access(all) + resource interface BridgeRouter { + + /// Returns a reference to the BridgeAccessor designated for internal bridge requests + access(all) view fun borrowBridgeAccessor(): &{BridgeAccessor} + + /// Sets the BridgeAccessor Capability in the BridgeRouter + access(all) fun setBridgeAccessor(_ accessor: Capability<&{BridgeAccessor}>) { + pre { + accessor.check(): "Invalid BridgeAccessor Capability provided" + } + } + } + + /// Returns a reference to the BridgeAccessor designated for internal bridge requests + access(self) + view fun borrowBridgeAccessor(): &{BridgeAccessor} { + return self.account.borrow<&{BridgeRouter}>(from: /storage/evmBridgeRouter) + ?.borrowBridgeAccessor() + ?? panic("Could not borrow reference to the EVM bridge") + } } diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 8f680c322d4..75736781890 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -27,13 +27,24 @@ import ( //go:embed contract.cdc var contractCode string -var flowTokenImportPattern = regexp.MustCompile(`(?m)^import "FlowToken"\n`) +var nftImportPattern = regexp.MustCompile(`(?m)^import "NonFungibleToken"`) +var fungibleTokenImportPattern = regexp.MustCompile(`(?m)^import "FungibleToken"`) +var flowTokenImportPattern = regexp.MustCompile(`(?m)^import "FlowToken"`) -func ContractCode(flowTokenAddress flow.Address) []byte { - return []byte(flowTokenImportPattern.ReplaceAllString( +func ContractCode(nonFungibleTokenAddress, fungibleTokenAddress, flowTokenAddress flow.Address) []byte { + evmContract := nftImportPattern.ReplaceAllString( contractCode, + fmt.Sprintf("import NonFungibleToken from %s", nonFungibleTokenAddress.HexWithPrefix()), + ) + evmContract = fungibleTokenImportPattern.ReplaceAllString( + evmContract, + fmt.Sprintf("import FungibleToken from %s", fungibleTokenAddress.HexWithPrefix()), + ) + evmContract = flowTokenImportPattern.ReplaceAllString( + evmContract, fmt.Sprintf("import FlowToken from %s", flowTokenAddress.HexWithPrefix()), - )) + ) + return []byte(evmContract) } const ContractName = "EVM" diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 32650c40506..4bb9e4103bf 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -223,7 +223,7 @@ func deployContracts( }, { name: stdlib.ContractName, - code: stdlib.ContractCode(contractsAddress), + code: stdlib.ContractCode(contractsAddress, contractsAddress, contractsAddress), }, } diff --git a/fvm/fvm_test.go b/fvm/fvm_test.go index 7fe79a3d033..4cc9059cbdf 100644 --- a/fvm/fvm_test.go +++ b/fvm/fvm_test.go @@ -3071,6 +3071,9 @@ func TestEVM(t *testing.T) { ).Return(block1.Header, nil) ctxOpts := []fvm.Option{ + // default is testnet, but testnet has a special EVM storage contract location + // so we have to use emulator here so that the EVM storage contract is deployed + // to the 5th address fvm.WithChain(flow.Emulator.Chain()), fvm.WithEVMEnabled(true), fvm.WithBlocks(blocks), @@ -3226,17 +3229,8 @@ func TestEVM(t *testing.T) { ) t.Run("deploy contract code", newVMTest(). - withBootstrapProcedureOptions( - fvm.WithSetupEVMEnabled(true), - ). - withContextOptions( - // default is testnet, but testnet has a special EVM storage contract location - // so we have to use emulator here so that the EVM storage contract is deployed - // to the 5th address - fvm.WithChain(flow.Emulator.Chain()), - fvm.WithBlocks(blocks), - fvm.WithBlockHeader(block1.Header), - ). + withBootstrapProcedureOptions(fvm.WithSetupEVMEnabled(true)). + withContextOptions(ctxOpts...). run(func( t *testing.T, vm fvm.VM,