diff --git a/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc b/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc index 534e5cef..c1a510d9 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeConfig.cdc @@ -101,6 +101,40 @@ contract FlowEVMBridgeConfig { return self.paused } + /// StoragePath at which an operator-set read gas limit is stored, if one has been set + /// + access(all) + view fun readGasLimitStoragePath(): StoragePath { + return /storage/flowEVMBridgeReadGasLimit + } + + /// Returns the read gas limit applied when no operator override has been set + /// + access(all) + view fun defaultReadGasLimit(): UInt64 { + return 500_000 + } + + /// Returns the gas limit the bridge declares for read-only EVM calls whose cost is bounded by the + /// ERC20/ERC721 standards, such as decimals(), balanceOf(address) and ownerOf(uint256). + /// + /// A limit smaller than `gasLimit` matters because the FVM admits an EVM call only if the remaining Cadence + /// computation budget covers the *declared* gas limit, not the gas the call actually consumes. At the mainnet + /// execution effort weight for EVM gas, a declared limit of 15,000,000 reserves roughly 1,600 of the 9,999 + /// computation units a transaction may spend, so a single cheap read can be refused with most of the budget + /// still unspent. Reads whose cost is not bounded by the standards, notably tokenURI and contractURI, keep + /// using `gasLimit` because they may run arbitrary on-chain metadata generation. + /// + /// This is a function reading from account storage rather than a contract field because Cadence rejects a + /// contract update that adds a field to an already deployed contract, and the bridge is live on Mainnet and + /// Testnet. Storage holds a value only once an operator sets one; otherwise the default applies. + /// + access(all) + view fun readGasLimit(): UInt64 { + return self.account.storage.copy(from: self.readGasLimitStoragePath()) + ?? self.defaultReadGasLimit() + } + /// 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 @@ -503,6 +537,19 @@ contract FlowEVMBridgeConfig { FlowEVMBridgeConfig.gasLimit = limit } + /// Sets the gas limit for read-only EVM calls whose cost is bounded by the ERC20/ERC721 standards. + /// The value is kept in account storage rather than a contract field so that this can ship as a + /// contract update; see FlowEVMBridgeConfig.readGasLimit(). + /// + /// @param limit the new read gas limit + /// + access(Gas) + fun setReadGasLimit(_ limit: UInt64) { + let path = FlowEVMBridgeConfig.readGasLimitStoragePath() + let _ = FlowEVMBridgeConfig.account.storage.load(from: path) + FlowEVMBridgeConfig.account.storage.save(limit, to: path) + } + /// Updates the onboarding fee /// /// @param new: UFix64 - new onboarding fee diff --git a/cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc b/cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc index f29f587d..b8751471 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeHandlers.cdc @@ -282,7 +282,7 @@ access(all) contract FlowEVMBridgeHandlers { // 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: \((preBalance + uintAmount).toString()) | actual: \((postBalance - preBalance).toString())" ) // Transfer WFLOW to recipient @@ -315,7 +315,7 @@ 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 \(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 @@ -351,7 +351,7 @@ access(all) contract FlowEVMBridgeHandlers { // 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: \((UInt256(preBalance) + amount).toString()) | actual: \((postBalance - preBalance).toString())" ) // Withdraw escrowed FLOW from bridge COA. diff --git a/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc b/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc index b5cf981a..660727e2 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc @@ -193,7 +193,7 @@ contract FlowEVMBridgeUtils { signature: "allowsBridging()", targetEVMAddress: address, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -260,7 +260,7 @@ contract FlowEVMBridgeUtils { signature: "isBridgeDeployed(address)", targetEVMAddress: self.bridgeFactoryEVMAddress, args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -283,7 +283,7 @@ contract FlowEVMBridgeUtils { signature: "isERC721(address)", targetEVMAddress: self.bridgeFactoryEVMAddress, args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -307,7 +307,7 @@ contract FlowEVMBridgeUtils { signature: "isERC20(address)", targetEVMAddress: self.bridgeFactoryEVMAddress, args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -330,7 +330,7 @@ contract FlowEVMBridgeUtils { signature: "isValidAsset(address)", targetEVMAddress: self.bridgeFactoryEVMAddress, args: [evmContractAddress], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -475,7 +475,7 @@ contract FlowEVMBridgeUtils { let isERC20 = self.isERC20(evmContractAddress: evmContractAddress) assert( isERC20, - message: "Contract \(evmContractAddress.toString())defines an asset that is not currently supported by the bridge" + message: "Contract \(evmContractAddress.toString()) defines an asset that is not currently supported by the bridge" ) cadenceContractName = self.deriveBridgedTokenContractName(from: evmContractAddress) decimals = self.getTokenDecimals(evmContractAddress: evmContractAddress) @@ -530,7 +530,7 @@ contract FlowEVMBridgeUtils { signature: "name()", targetEVMAddress: evmContractAddress, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -554,7 +554,7 @@ contract FlowEVMBridgeUtils { signature: "symbol()", targetEVMAddress: evmContractAddress, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -577,6 +577,8 @@ contract FlowEVMBridgeUtils { signature: "tokenURI(uint256)", targetEVMAddress: evmContractAddress, args: [id], + // Uses the full gasLimit rather than readGasLimit: tokenURI may generate metadata on-chain, + // so its cost is not bounded by the ERC721 standard. gasLimit: FlowEVMBridgeConfig.gasLimit, value: 0.0, resultTypes: [Type()] @@ -600,6 +602,8 @@ contract FlowEVMBridgeUtils { signature: "contractURI()", targetEVMAddress: evmContractAddress, args: [], + // Uses the full gasLimit rather than readGasLimit: contractURI may generate metadata on-chain, + // so its cost is not bounded by the ERC721 standard. gasLimit: FlowEVMBridgeConfig.gasLimit, value: 0.0, resultTypes: [Type()] @@ -622,7 +626,7 @@ contract FlowEVMBridgeUtils { signature: "decimals()", targetEVMAddress: evmContractAddress, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -674,7 +678,7 @@ contract FlowEVMBridgeUtils { signature: "ownerOf(uint256)", targetEVMAddress: evmContractAddress, args: [id], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -698,7 +702,7 @@ contract FlowEVMBridgeUtils { signature: "getApproved(uint256)", targetEVMAddress: evmContractAddress, args: [ofNFT], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -725,7 +729,7 @@ contract FlowEVMBridgeUtils { signature: "exists(uint256)", targetEVMAddress: erc721Address, args: [id], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -747,7 +751,7 @@ contract FlowEVMBridgeUtils { signature: "balanceOf(address)", targetEVMAddress: evmContractAddress, args: [owner], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -782,7 +786,7 @@ contract FlowEVMBridgeUtils { signature: "totalSupply()", targetEVMAddress: evmContractAddress, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -854,7 +858,7 @@ contract FlowEVMBridgeUtils { signature: "getCadenceAddress()", targetEVMAddress: evmContract, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -885,7 +889,7 @@ contract FlowEVMBridgeUtils { signature: "getCadenceIdentifier()", targetEVMAddress: evmContract, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -912,7 +916,7 @@ contract FlowEVMBridgeUtils { signature: "supportsInterface(bytes4)", targetEVMAddress: evmContract, args: [interfaceID], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -940,7 +944,7 @@ contract FlowEVMBridgeUtils { signature: "supportsInterface(bytes4)", targetEVMAddress: evmContract, args: [interfaceID], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -979,7 +983,7 @@ contract FlowEVMBridgeUtils { signature: "vmBridgeAddress()", targetEVMAddress: evmContract, args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0.0, resultTypes: [Type()] ) @@ -1585,18 +1589,18 @@ contract FlowEVMBridgeUtils { erc20Address: EVM.EVMAddress, protectedTransferCall: fun (): EVM.ResultDecoded ) { - // Ensure the caller is has sufficient balance to bridge the requested amount - let hasSufficientBalance = self.hasSufficientBalance( - amount: amount, - owner: owner, - evmContractAddress: erc20Address - ) - assert(hasSufficientBalance, message: "Caller does not have sufficient balance to bridge requested tokens") + let bridgeCOAAddress = self.getBridgeCOAEVMAddress() // Get the owner and escrow balances before transfer let ownerPreBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) + + // Ensure the caller has sufficient balance to bridge the requested amount. The owner's pre-transfer + // balance read above answers this, so calling hasSufficientBalance would repeat an identical + // balanceOf(address) call for a value already in scope. + assert(ownerPreBalance >= amount, message: "Caller does not have sufficient balance to bridge requested tokens") + let bridgePreBalance = self.balanceOf( - owner: self.getBridgeCOAEVMAddress(), + owner: bridgeCOAAddress, evmContractAddress: erc20Address ) @@ -1607,7 +1611,7 @@ contract FlowEVMBridgeUtils { // Get the resulting balances after transfer let ownerPostBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) let bridgePostBalance = self.balanceOf( - owner: self.getBridgeCOAEVMAddress(), + owner: bridgeCOAAddress, evmContractAddress: erc20Address ) diff --git a/cadence/scripts/bridge/get_read_gas_limit.cdc b/cadence/scripts/bridge/get_read_gas_limit.cdc new file mode 100644 index 00000000..65ae75bf --- /dev/null +++ b/cadence/scripts/bridge/get_read_gas_limit.cdc @@ -0,0 +1,11 @@ +import "FlowEVMBridgeConfig" + +/// Returns the gas limit the bridge declares for read-only EVM calls whose cost is bounded by the +/// ERC20/ERC721 standards, such as decimals(), balanceOf(address) and ownerOf(uint256). +/// +/// @returns The current read gas limit. +/// +access(all) +fun main(): UInt64 { + return FlowEVMBridgeConfig.readGasLimit() +} diff --git a/cadence/scripts/utils/get_deployer_address.cdc b/cadence/scripts/utils/get_deployer_address.cdc index 9b7e4789..8553b351 100644 --- a/cadence/scripts/utils/get_deployer_address.cdc +++ b/cadence/scripts/utils/get_deployer_address.cdc @@ -13,7 +13,7 @@ fun main(coaHost: Address, deployerTag: String): String { to: FlowEVMBridgeUtils.getBridgeFactoryEVMAddress(), signature: "getDeployer(string)", args: [deployerTag], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0, resultTypes: [Type()] ) diff --git a/cadence/scripts/utils/get_registry_address.cdc b/cadence/scripts/utils/get_registry_address.cdc index 92ee2e1d..3782771a 100644 --- a/cadence/scripts/utils/get_registry_address.cdc +++ b/cadence/scripts/utils/get_registry_address.cdc @@ -13,7 +13,7 @@ fun main(coaHost: Address): String { to: FlowEVMBridgeUtils.getBridgeFactoryEVMAddress(), signature: "getRegistry()", args: [], - gasLimit: FlowEVMBridgeConfig.gasLimit, + gasLimit: FlowEVMBridgeConfig.readGasLimit(), value: 0, resultTypes: [Type()] ) diff --git a/cadence/tests/flow_evm_bridge_tests.cdc b/cadence/tests/flow_evm_bridge_tests.cdc index 1ed24a78..1ad63b94 100644 --- a/cadence/tests/flow_evm_bridge_tests.cdc +++ b/cadence/tests/flow_evm_bridge_tests.cdc @@ -115,6 +115,36 @@ fun testSetGasLimitSucceeds() { Test.reset(to: snapshot) } +access(all) +fun testSetReadGasLimitSucceeds() { + + fun getReadGasLimit(): UInt64 { + let readGasLimitResult = executeScript( + "../scripts/bridge/get_read_gas_limit.cdc", + [] + ) + Test.expect(readGasLimitResult, Test.beSucceeded()) + return readGasLimitResult.returnValue as! UInt64? ?? panic("Problem getting read gas limit") + } + + snapshot = getCurrentBlockHeight() + + let preReadGasLimit = getReadGasLimit() + let readGasLimit = preReadGasLimit + 1_000 + + let setReadGasLimitResult = executeTransaction( + "../transactions/bridge/admin/gas/set_read_gas_limit.cdc", + [readGasLimit], + bridgeAccount + ) + Test.expect(setReadGasLimitResult, Test.beSucceeded()) + + let postReadGasLimit = getReadGasLimit() + Test.assertEqual(readGasLimit, postReadGasLimit) + + Test.reset(to: snapshot) +} + /* --- ASSET & ACCOUNT SETUP - Configure test accounts with assets to bridge --- */ access(all) diff --git a/cadence/transactions/bridge/admin/gas/set_read_gas_limit.cdc b/cadence/transactions/bridge/admin/gas/set_read_gas_limit.cdc new file mode 100644 index 00000000..e342bbc8 --- /dev/null +++ b/cadence/transactions/bridge/admin/gas/set_read_gas_limit.cdc @@ -0,0 +1,24 @@ +import "FlowEVMBridgeConfig" + +/// Sets the gas limit the bridge declares for read-only EVM calls whose cost is bounded by the +/// ERC20/ERC721 standards. +/// +/// @param readGasLimit: The new read gas limit. +/// +transaction(readGasLimit: UInt64) { + + let admin: auth(FlowEVMBridgeConfig.Gas) &FlowEVMBridgeConfig.Admin + + prepare(signer: auth(BorrowValue) &Account) { + self.admin = signer.storage.borrow(from: FlowEVMBridgeConfig.adminStoragePath) + ?? panic("Could not borrow FlowEVMBridgeConfig Admin reference") + } + + execute { + self.admin.setReadGasLimit(readGasLimit) + } + + post { + FlowEVMBridgeConfig.readGasLimit() == readGasLimit: "Problem setting readGasLimit to: ".concat(readGasLimit.toString()) + } +} diff --git a/templates.go b/templates.go index 9953726a..c4d658d4 100644 --- a/templates.go +++ b/templates.go @@ -49,6 +49,7 @@ import ( //go:embed cadence/scripts/bridge/get_associated_type.cdc //go:embed cadence/scripts/bridge/get_bridge_coa_address.cdc //go:embed cadence/scripts/bridge/get_gas_limit.cdc +//go:embed cadence/scripts/bridge/get_read_gas_limit.cdc //go:embed cadence/scripts/bridge/is_cadence_type_blocked.cdc //go:embed cadence/scripts/bridge/is_evm_address_blocked.cdc //go:embed cadence/scripts/bridge/is_paused.cdc @@ -111,6 +112,7 @@ import ( //go:embed cadence/transactions/bridge/admin/fee/update_base_fee.cdc //go:embed cadence/transactions/bridge/admin/fee/update_onboard_fee.cdc //go:embed cadence/transactions/bridge/admin/gas/set_gas_limit.cdc +//go:embed cadence/transactions/bridge/admin/gas/set_read_gas_limit.cdc //go:embed cadence/transactions/bridge/admin/metadata/set_bridged_ft_display_view.cdc //go:embed cadence/transactions/bridge/admin/metadata/set_bridged_nft_collection_display_view.cdc //go:embed cadence/transactions/bridge/admin/metadata/set_bridged_nft_display_view.cdc