From db2df231b7d4d2984d5d7bae42cdef657b92f6ac Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:44:01 -0500 Subject: [PATCH] fix ufix64<->uint256 conversion and add initial test coverage --- .../contracts/bridge/FlowEVMBridgeUtils.cdc | 123 ++++++++++----- cadence/contracts/standards/EVM.cdc | 6 +- cadence/tests/flow_evm_bridge_utils_tests.cdc | 148 ++++++++++++++++++ flow.json | 2 +- 4 files changed, 239 insertions(+), 40 deletions(-) create mode 100644 cadence/tests/flow_evm_bridge_utils_tests.cdc diff --git a/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc b/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc index 77c5ad2f..bf8850cd 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeUtils.cdc @@ -256,8 +256,8 @@ contract FlowEVMBridgeUtils { /// access(all) fun isValidEVMAsset(evmContractAddress: EVM.EVMAddress): Bool { - let isERC721 = FlowEVMBridgeUtils.isERC721(evmContractAddress: evmContractAddress) - let isERC20 = FlowEVMBridgeUtils.isERC20(evmContractAddress: evmContractAddress) + let isERC721 = self.isERC721(evmContractAddress: evmContractAddress) + let isERC20 = self.isERC20(evmContractAddress: evmContractAddress) return (isERC721 && !isERC20) || (!isERC721 && isERC20) } @@ -280,7 +280,7 @@ contract FlowEVMBridgeUtils { /// access(all) view fun getBridgeCOAEVMAddress(): EVM.EVMAddress { - return FlowEVMBridgeUtils.borrowCOA().address() + return self.borrowCOA().address() } /// Retrieves the relevant information for onboarding a Cadence asset to the bridge. This method is used to @@ -303,10 +303,10 @@ contract FlowEVMBridgeUtils { let isNFT = forAssetType.isSubtype(of: Type<@{NonFungibleToken.NFT}>()) // Retrieve the Cadence type's defining contract name, address, & its identifier - var name = FlowEVMBridgeUtils.getContractName(fromType: forAssetType) + var name = self.getContractName(fromType: forAssetType) ?? panic("Could not contract name from type: ".concat(forAssetType.identifier)) let identifier = forAssetType.identifier - let cadenceAddress = FlowEVMBridgeUtils.getContractAddress(fromType: forAssetType) + let cadenceAddress = self.getContractAddress(fromType: forAssetType) ?? panic("Could not derive contract address for token type: ".concat(identifier)) // Initialize asset symbol which will be assigned later // based on presence of asset-defined metadata @@ -607,7 +607,7 @@ contract FlowEVMBridgeUtils { fun erc721Exists(erc721Address: EVM.EVMAddress, id: UInt256): Bool { let existsResponse = EVM.decodeABI( types: [Type()], - data: FlowEVMBridgeUtils.call( + data: self.call( signature: "exists(uint256)", targetEVMAddress: erc721Address, args: [id], @@ -793,32 +793,83 @@ contract FlowEVMBridgeUtils { return r } + /// Raises the fixed point base to the power of the exponent + /// + access(all) + view fun ufixPow(base: UFix64, exponent: UInt8): UFix64 { + if exponent == 0 { + return 1.0 + } + + var r = base + var exp: UInt8 = 1 + while exp < exponent { + r = r * base + exp = exp + 1 + } + + return r + } + /// Converts a UInt256 to a UFix64 /// access(all) view fun uint256ToUFix64(value: UInt256, decimals: UInt8): UFix64 { - let scaleFactor: UInt256 = self.pow(base: 10, exponent: decimals) - let scaledValue: UInt256 = value / scaleFactor + // Calculate scale factors for the integer and fractional parts + let absoluteScaleFactor = self.pow(base: 10, exponent: decimals) + + // Separate the integer and fractional parts of the value + let scaledValue = value / absoluteScaleFactor + var fractional = value % absoluteScaleFactor + + var e: UInt8 = 0 + while true { + if fractional % 10 == 0 { + fractional = fractional / 10 + e = e + 1 + } else { + break + } + } assert( - scaledValue < UInt256(UInt64.max), - message: "Value ".concat(value.toString()).concat(" exceeds max UFix64 value") + scaledValue < UInt256(UFix64.max), + message: "Scaled integer value ".concat(value.toString()).concat(" exceeds max UFix64 value") + ) + assert( + fractional < UInt256(UFix64.max), + message: "Fractional ".concat(value.toString()).concat(" exceeds max UFix64 value") ) - return UFix64(scaledValue) + // Scale and add fractional part + let fractionalMultiplier = self.ufixPow(base: 0.1, exponent: decimals - e) + let scaledFractional: UFix64 = UFix64(fractional) * fractionalMultiplier + assert(scaledFractional < 1.0, message: "Scaled fractional exceeds 1.0") + + return UFix64(scaledValue) + scaledFractional } /// Converts a UFix64 to a UInt256 // access(all) - view fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 { - let integerPart: UInt64 = UInt64(value) - var r = UInt256(integerPart) + fun ufix64ToUInt256(value: UFix64, decimals: UInt8): UInt256 { + // Default to 10e8 scale, catching instances where decimals are less than default and scale appropriately + let ufixScaleExp: UInt8 = decimals < 8 ? decimals : 8 + var ufixScale = self.ufixPow(base: 10.0, exponent: ufixScaleExp) + + // Separate the fractional and integer parts of the UFix64 + let integer = UInt256(value) + var fractional = (value % 1.0) * ufixScale - var multiplier: UInt256 = self.pow(base:10, exponent: decimals) - return r * multiplier + // Calculate the scale for integer and fractional parts + var integerMultiplier: UInt256 = self.pow(base:10, exponent: decimals) + let fractionalMultiplierExp: UInt8 = decimals < 8 ? decimals : decimals - 8 + var fractionalMultiplier: UInt256 = self.pow(base:10, exponent: fractionalMultiplierExp) + + return integer > 0 ? integer * integerMultiplier + UInt256(fractional) : fractionalMultiplier * UInt256(fractional) } + /// Returns the value as a UInt64 if it fits, otherwise panics /// access(all) @@ -979,7 +1030,7 @@ contract FlowEVMBridgeUtils { assert(bridgePreStatus, message: "Bridge COA does not own ERC721 requesting to be transferred") assert(!toPreStatus, message: "Recipient already owns ERC721 attempting to be transferred") - let transferResult: EVM.Result = FlowEVMBridgeUtils.call( + let transferResult: EVM.Result = self.call( signature: "safeTransferFrom(address,address,uint256)", targetEVMAddress: erc721Address, args: [bridgeCOAAddress, to, id], @@ -1004,7 +1055,7 @@ contract FlowEVMBridgeUtils { fun mustSafeMintERC721(erc721Address: EVM.EVMAddress, to: EVM.EVMAddress, id: UInt256, uri: String) { let bridgeCOAAddress = self.getBridgeCOAEVMAddress() - let mintResult: EVM.Result = FlowEVMBridgeUtils.call( + let mintResult: EVM.Result = self.call( signature: "safeMint(address,uint256,string)", targetEVMAddress: erc721Address, args: [to, id, uri], @@ -1025,7 +1076,7 @@ contract FlowEVMBridgeUtils { fun mustUpdateTokenURI(erc721Address: EVM.EVMAddress, id: UInt256, uri: String) { let bridgeCOAAddress = self.getBridgeCOAEVMAddress() - let updateResult: EVM.Result = FlowEVMBridgeUtils.call( + let updateResult: EVM.Result = self.call( signature: "updateTokenURI(uint256,string)", targetEVMAddress: erc721Address, args: [id, uri], @@ -1063,9 +1114,9 @@ contract FlowEVMBridgeUtils { /// access(account) fun mustMintERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) { - let toPreBalance = FlowEVMBridgeUtils.balanceOf(owner: to, evmContractAddress: erc20Address) + let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) // Mint tokens to the recipient - let mintResult: EVM.Result = FlowEVMBridgeUtils.call( + let mintResult: EVM.Result = self.call( signature: "mint(address,uint256)", targetEVMAddress: erc20Address, args: [to, amount], @@ -1074,7 +1125,7 @@ contract FlowEVMBridgeUtils { ) assert(mintResult.status == EVM.Status.successful, message: "Mint to bridge ERC20 contract failed") // Ensure bridge to recipient was succcessful - let toPostBalance = FlowEVMBridgeUtils.balanceOf(owner: to, evmContractAddress: erc20Address) + let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) assert( toPostBalance == toPreBalance + amount, message: "Recipient didn't receive minted ERC20 tokens during bridging" @@ -1088,14 +1139,14 @@ contract FlowEVMBridgeUtils { fun mustTransferERC20(to: EVM.EVMAddress, amount: UInt256, erc20Address: EVM.EVMAddress) { let bridgeCOAAddress = self.getBridgeCOAEVMAddress() - let toPreBalance = FlowEVMBridgeUtils.balanceOf(owner: to, evmContractAddress: erc20Address) - let escrowPreBalance = FlowEVMBridgeUtils.balanceOf( + let toPreBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) + let escrowPreBalance = self.balanceOf( owner: bridgeCOAAddress, evmContractAddress: erc20Address ) // Transfer tokens to the recipient - let transferResult: EVM.Result = FlowEVMBridgeUtils.call( + let transferResult: EVM.Result = self.call( signature: "transfer(address,uint256)", targetEVMAddress: erc20Address, args: [to, amount], @@ -1105,8 +1156,8 @@ contract FlowEVMBridgeUtils { assert(transferResult.status == EVM.Status.successful, message: "transfer call to ERC20 contract failed") // Ensure bridge to recipient was succcessful - let toPostBalance = FlowEVMBridgeUtils.balanceOf(owner: to, evmContractAddress: erc20Address) - let escrowPostBalance = FlowEVMBridgeUtils.balanceOf( + let toPostBalance = self.balanceOf(owner: to, evmContractAddress: erc20Address) + let escrowPostBalance = self.balanceOf( owner: bridgeCOAAddress, evmContractAddress: erc20Address ) @@ -1132,7 +1183,7 @@ contract FlowEVMBridgeUtils { protectedTransferCall: fun (): EVM.Result ) { // Ensure the caller is has sufficient balance to bridge the requested amount - let hasSufficientBalance = FlowEVMBridgeUtils.hasSufficientBalance( + let hasSufficientBalance = self.hasSufficientBalance( amount: amount, owner: owner, evmContractAddress: erc20Address @@ -1140,9 +1191,9 @@ contract FlowEVMBridgeUtils { assert(hasSufficientBalance, message: "Caller does not have sufficient balance to bridge requested tokens") // Get the owner and escrow balances before transfer - let ownerPreBalance = FlowEVMBridgeUtils.balanceOf(owner: owner, evmContractAddress: erc20Address) - let bridgePreBalance = FlowEVMBridgeUtils.balanceOf( - owner: FlowEVMBridgeUtils.getBridgeCOAEVMAddress(), + let ownerPreBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) + let bridgePreBalance = self.balanceOf( + owner: self.getBridgeCOAEVMAddress(), evmContractAddress: erc20Address ) @@ -1151,9 +1202,9 @@ contract FlowEVMBridgeUtils { assert(transferResult.status == EVM.Status.successful, message: "Transfer via callback failed") // Get the resulting balances after transfer - let ownerPostBalance = FlowEVMBridgeUtils.balanceOf(owner: owner, evmContractAddress: erc20Address) - let bridgePostBalance = FlowEVMBridgeUtils.balanceOf( - owner: FlowEVMBridgeUtils.getBridgeCOAEVMAddress(), + let ownerPostBalance = self.balanceOf(owner: owner, evmContractAddress: erc20Address) + let bridgePostBalance = self.balanceOf( + owner: self.getBridgeCOAEVMAddress(), evmContractAddress: erc20Address ) @@ -1174,9 +1225,9 @@ contract FlowEVMBridgeUtils { isERC721: Bool ): EVM.EVMAddress { let signature = isERC721 ? "deployERC721(string,string,string,string,string)" : "deployERC20(string,string,string,string,string)" - let deployResult: EVM.Result = FlowEVMBridgeUtils.call( + let deployResult: EVM.Result = self.call( signature: signature, - targetEVMAddress: FlowEVMBridgeUtils.bridgeFactoryEVMAddress, + targetEVMAddress: self.bridgeFactoryEVMAddress, args: [name, symbol, cadenceAddress.toString(), flowIdentifier, contractURI], gasLimit: 15000000, value: 0.0 diff --git a/cadence/contracts/standards/EVM.cdc b/cadence/contracts/standards/EVM.cdc index 1130a4c5..aa220145 100644 --- a/cadence/contracts/standards/EVM.cdc +++ b/cadence/contracts/standards/EVM.cdc @@ -1,7 +1,7 @@ import Crypto -import "NonFungibleToken" -import "FungibleToken" -import "FlowToken" +import NonFungibleToken from 0xf8d6e0586b0a20c7 +import FungibleToken from 0xee82856bf20e2aa6 +import FlowToken from 0x0ae53cb6e3f42a79 access(all) contract EVM { diff --git a/cadence/tests/flow_evm_bridge_utils_tests.cdc b/cadence/tests/flow_evm_bridge_utils_tests.cdc new file mode 100644 index 00000000..7e61a200 --- /dev/null +++ b/cadence/tests/flow_evm_bridge_utils_tests.cdc @@ -0,0 +1,148 @@ +import Test +import BlockchainHelpers + +import "test_helpers.cdc" + +access(all) let serviceAccount = Test.serviceAccount() +access(all) let bridgeAccount = Test.getAccount(0x0000000000000007) + +access(all) +fun setup() { + // Deploy supporting util contracts + var err = Test.deployContract( + name: "ArrayUtils", + path: "../contracts/utils/ArrayUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "StringUtils", + path: "../contracts/utils/StringUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "ScopedFTProviders", + path: "../contracts/utils/ScopedFTProviders.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "Serialize", + path: "../contracts/utils/Serialize.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "SerializeMetadata", + path: "../contracts/utils/SerializeMetadata.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "EVMUtils", + path: "../contracts/utils/EVMUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Update MetadataViews contract with proposed URI & EVMBridgedMetadata view COA integration + // TODO: Remove once MetadataViews contract is updated in CLI's core contracts + var updateResult = executeTransaction( + "./transactions/update_contract.cdc", + ["MetadataViews", getMetadataViewsUpdateCode()], + serviceAccount + ) + // Update EVM contract with proposed bridge-supporting COA integration + // TODO: Remove once EVM contract is updated in CLI's core contracts + updateResult = executeTransaction( + "./transactions/update_contract.cdc", + ["EVM", getEVMUpdateCode()], + serviceAccount + ) + Test.expect(updateResult, Test.beSucceeded()) + // Transfer bridge account some $FLOW + transferFlow(signer: serviceAccount, recipient: bridgeAccount.address, amount: 10_000.0) + // Configure bridge account with a COA + createCOA(signer: bridgeAccount, fundingAmount: 1_000.0) + + err = Test.deployContract( + name: "IBridgePermissions", + path: "../contracts/bridge/interfaces/IBridgePermissions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "ICrossVM", + path: "../contracts/bridge/interfaces/ICrossVM.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "CrossVMNFT", + path: "../contracts/bridge/interfaces/CrossVMNFT.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "CrossVMToken", + path: "../contracts/bridge/interfaces/CrossVMToken.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeHandlerInterfaces", + path: "../contracts/bridge/interfaces/FlowEVMBridgeHandlerInterfaces.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeConfig", + path: "../contracts/bridge/FlowEVMBridgeConfig.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "FlowEVMBridgeUtils", + path: "../contracts/bridge/FlowEVMBridgeUtils.cdc", + arguments: [getCompiledFactoryBytecode()] + ) +} + +access(all) +fun testReducedPrecisionUInt256ToUFix64Succeeds() { + let uintAmount: UInt256 = 24_244_814_054_591 + let ufixAmount: UFix64 = 24_244_814.05459100 + + let actualUFixAmount = uint256ToUFix64(uintAmount, decimals: 6) + Test.assert(actualUFixAmount == ufixAmount) +} + +// Converting from UFix64 to UInt256 with reduced point precision (6 vs. 8) should round down +access(all) +fun testReducedPrecisionUFix64ToUInt256Succeeds() { + let uintAmount: UInt256 = 24_244_814_054_591 + let ufixAmount: UFix64 = 24_244_814.05459154 + + let actualUIntAmount = ufix64ToUInt256(ufixAmount, decimals: 6) + Test.assert(actualUIntAmount == uintAmount) +} + +access(all) +fun testDustUInt256ToUFix64Succeeds() { + let dustUFixAmount: UFix64 = 0.00002547 + let dustUIntAmount: UInt256 = 25_470_000_000_000 + + let actualUFixAmount = uint256ToUFix64(dustUIntAmount, decimals: 18) + assert(actualUFixAmount <= dustUFixAmount, message: "Actual UFix amount greater: ".concat(actualUFixAmount.toString())) + assert(actualUFixAmount > 0.0, message: "Actual UFix zero: ".concat(actualUFixAmount.toString())) +} + +access(all) +fun testDustUFix64ToUInt256Succeeds() { + let dustUFixAmount: UFix64 = 0.00002547 + let dustUIntAmount: UInt256 = 25_470_000_000_000 + + let actualUIntAmount = ufix64ToUInt256(dustUFixAmount, decimals: 18) + Test.assert(actualUIntAmount == dustUIntAmount && actualUIntAmount > 0) +} diff --git a/flow.json b/flow.json index 165a4824..8389396c 100644 --- a/flow.json +++ b/flow.json @@ -19,7 +19,7 @@ "Burner": { "source": "./cadence/contracts/standards/Burner.cdc", "aliases": { - "emulator": "ee82856bf20e2aa6", + "emulator": "f8d6e0586b0a20c7", "previewnet": "b6763b4399a888c8", "testing": "0000000000000001" }