diff --git a/cmd/util/ledger/migrations/change_contract_code_migration.go b/cmd/util/ledger/migrations/change_contract_code_migration.go index ff4271aa1ff..c327a677e1f 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration.go @@ -223,6 +223,8 @@ func SystemContractChanges(chainID flow.ChainID, options SystemContractsMigratio NewSystemContractChange( systemContracts.EVMContract, evm.ContractCode( + flow.HexToAddress(env.NonFungibleTokenAddress), + flow.HexToAddress(env.FungibleTokenAddress), flow.HexToAddress(env.FlowTokenAddress), ), ), diff --git a/fvm/bootstrap.go b/fvm/bootstrap.go index 85beee49d50..5c7e09d31ed 100644 --- a/fvm/bootstrap.go +++ b/fvm/bootstrap.go @@ -434,7 +434,7 @@ func (b *bootstrapExecutor) Execute() error { b.deployStakingCollection(service, &env) // sets up the EVM environment - b.setupEVM(service, fungibleToken, flowToken) + b.setupEVM(service, nonFungibleToken, fungibleToken, flowToken) err = expectAccounts(systemcontracts.EVMStorageAccountIndex) if err != nil { @@ -895,7 +895,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 @@ -906,7 +906,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 71014e83cac..12ee784d085 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) @@ -10,6 +12,7 @@ contract EVM { access(all) entitlement Call access(all) entitlement Deploy access(all) entitlement Owner + access(all) entitlement Bridge access(all) event CadenceOwnedAccountCreated(addressBytes: [UInt8; 20]) @@ -301,6 +304,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: auth(FungibleToken.Withdraw) &{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(Owner | Bridge) + fun withdrawNFT( + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{NonFungibleToken.NFT} { + return <- EVM.borrowBridgeAccessor().withdrawNFT( + caller: &self as auth(Call) &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: auth(FungibleToken.Withdraw) &{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(Owner | Bridge) + fun withdrawTokens( + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{FungibleToken.Vault} { + return <- EVM.borrowBridgeAccessor().withdrawTokens( + caller: &self as auth(Call) &CadenceOwnedAccount, + type: type, + amount: amount, + feeProvider: feeProvider + ) + } } /// Creates a new cadence owned account @@ -500,4 +556,59 @@ 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(Bridge) + fun depositNFT( + nft: @{NonFungibleToken.NFT}, + to: EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + + /// Endpoint enabling the bridging of an NFT from EVM + access(Bridge) + fun withdrawNFT( + caller: auth(Call) &CadenceOwnedAccount, + type: Type, + id: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ): @{NonFungibleToken.NFT} + + /// Endpoint enabling the bridging of a fungible token vault to EVM + access(Bridge) + fun depositTokens( + vault: @{FungibleToken.Vault}, + to: EVMAddress, + feeProvider: auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + + /// Endpoint enabling the bridging of fungible tokens from EVM + access(Bridge) + fun withdrawTokens( + caller: auth(Call) &CadenceOwnedAccount, + type: Type, + amount: UInt256, + feeProvider: auth(FungibleToken.Withdraw) &{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(Bridge) view fun borrowBridgeAccessor(): auth(Bridge) &{BridgeAccessor} + } + + /// Returns a reference to the BridgeAccessor designated for internal bridge requests + access(self) + view fun borrowBridgeAccessor(): auth(Bridge) &{BridgeAccessor} { + return self.account.storage.borrow(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 532a327956e..8a06af45871 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 { + contractCode = nftImportPattern.ReplaceAllString( + contractCode, + fmt.Sprintf("import NonFungibleToken from %s", nonFungibleTokenAddress.HexWithPrefix()), + ) + contractCode = fungibleTokenImportPattern.ReplaceAllString( + contractCode, + fmt.Sprintf("import FungibleToken from %s", fungibleTokenAddress.HexWithPrefix()), + ) + contractCode = flowTokenImportPattern.ReplaceAllString( contractCode, fmt.Sprintf("import FlowToken from %s", flowTokenAddress.HexWithPrefix()), - )) + ) + return []byte(contractCode) } const ContractName = "EVM" diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 5e3c542288e..2006eeb1d75 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -238,7 +238,7 @@ func deployContracts( }, { name: stdlib.ContractName, - code: stdlib.ContractCode(contractsAddress), + code: stdlib.ContractCode(contractsAddress, contractsAddress, contractsAddress), }, }