diff --git a/cadence/contracts/connectors/evm/UniswapV3SwapperProvider.cdc b/cadence/contracts/connectors/evm/UniswapV3SwapperProvider.cdc new file mode 100644 index 00000000..6c772bd3 --- /dev/null +++ b/cadence/contracts/connectors/evm/UniswapV3SwapperProvider.cdc @@ -0,0 +1,290 @@ +import "FungibleToken" +import "EVM" +import "FlowEVMBridgeConfig" + +import "DeFiActions" +import "UniswapV3SwapConnectors" + +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// THIS CONTRACT IS IN BETA AND IS NOT FINALIZED - INTERFACES MAY CHANGE AND/OR PENDING CHANGES MAY REQUIRE REDEPLOYMENT +/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +/// +/// UniswapV3SwapperProvider +/// +/// DeFiActions SwapperProvider implementation for Uniswap V3 on Flow EVM. +/// Pre-computes and stores all swappers at initialization for O(1) lookup during liquidations. +/// Supports both direct swaps and multi-hop routes through intermediate tokens. +/// +access(all) contract UniswapV3SwapperProvider { + + /// TokenConfig + /// + /// Configuration for a token supported by the provider. + /// Links a Cadence FungibleToken.Vault type to its corresponding ERC20 address on Flow EVM. + /// + access(all) struct TokenConfig { + access(all) let flowType: Type // Cadence type, e.g., Type<@FlowToken.Vault>() + access(all) let evmAddress: EVM.EVMAddress // Corresponding ERC20 address + + init(flowType: Type, evmAddress: EVM.EVMAddress) { + pre { + flowType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "flowType must be a FungibleToken.Vault" + FlowEVMBridgeConfig.getTypeAssociated(with: evmAddress) == flowType: + "flowType must be associated with evmAddress in FlowEVMBridgeConfig" + } + self.flowType = flowType + self.evmAddress = evmAddress + } + } + + /// RouteConfig + /// + /// Configuration for a trading route between two tokens. + /// Supports multi-hop paths through intermediate tokens. + /// + access(all) struct RouteConfig { + access(all) let inToken: Type + access(all) let outToken: Type + access(all) let tokenPath: [EVM.EVMAddress] // Multi-hop path support + access(all) let feePath: [UInt32] // Fee tiers in basis points (500, 3000, 10000) + + init(inToken: Type, outToken: Type, tokenPath: [EVM.EVMAddress], feePath: [UInt32]) { + pre { + tokenPath.length >= 2: "tokenPath must have at least 2 tokens" + feePath.length == tokenPath.length - 1: "feePath length must be tokenPath.length - 1" + inToken != outToken: "Cannot swap token to itself" + } + self.inToken = inToken + self.outToken = outToken + self.tokenPath = tokenPath + self.feePath = feePath + } + } + + /// UniswapV3SwapperProvider + /// + /// A SwapperProvider that pre-computes all swappers at initialization for predictable performance. + /// When an intermediary token is configured, auto-generates multi-hop routes for any pair + /// that doesn't have an explicit route, routing through the intermediary. + /// Returns nil for unconfigured trading pairs that cannot be auto-generated. + /// + access(all) struct SwapperProvider : DeFiActions.SwapperProvider, DeFiActions.IdentifiableStruct { + // Uniswap V3 contract addresses + access(all) let factoryAddress: EVM.EVMAddress + access(all) let routerAddress: EVM.EVMAddress + access(all) let quoterAddress: EVM.EVMAddress + + // Token configuration + access(all) let tokens: [TokenConfig] + + // Pre-computed swappers + access(self) let swappers: {String: {DeFiActions.Swapper}} + + // Shared COA capability for all swappers + access(self) let coaCapability: Capability + + // IdentifiableStruct conformance + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + init( + factoryAddress: EVM.EVMAddress, + routerAddress: EVM.EVMAddress, + quoterAddress: EVM.EVMAddress, + tokens: [TokenConfig], + routes: [RouteConfig], + coaCapability: Capability, + uniqueID: DeFiActions.UniqueIdentifier?, + intermediaryToken: TokenConfig? + ) { + pre { + tokens.length >= 2: "Must provide at least 2 tokens" + routes.length > 0: "Must provide at least one route" + coaCapability.check(): "Invalid COA capability" + } + + // Initialize fields + self.factoryAddress = factoryAddress + self.routerAddress = routerAddress + self.quoterAddress = quoterAddress + self.tokens = tokens + self.coaCapability = coaCapability + self.uniqueID = uniqueID + self.swappers = {} + + // Validate routes reference configured tokens + let tokenTypes: {Type: Bool} = {} + for token in tokens { + tokenTypes[token.flowType] = true + } + + for route in routes { + assert(tokenTypes.containsKey(route.inToken), + message: "Route inToken not in configured tokens") + assert(tokenTypes.containsKey(route.outToken), + message: "Route outToken not in configured tokens") + } + + // Validate intermediary token is one of the configured tokens + if let intermediary = intermediaryToken { + assert(tokenTypes.containsKey(intermediary.flowType), + message: "Intermediary token must be one of the configured tokens") + } + + // Pre-compute all swappers from explicit routes + for route in routes { + let key = self._makeKey(route.inToken, route.outToken) + + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokenPath: route.tokenPath, + feePath: route.feePath, + inVault: route.inToken, + outVault: route.outToken, + coaCapability: coaCapability, + uniqueID: uniqueID + ) + + self.swappers[key] = swapper + } + + // Auto-generate multi-hop routes through intermediary for unconfigured pairs + if let intermediary = intermediaryToken { + // Build fee lookup from single-hop explicit routes: "addr1_addr2" -> fee + let feeLookup: {String: UInt32} = {} + for route in routes { + if route.tokenPath.length == 2 { + let addr0 = route.tokenPath[0].toString() + let addr1 = route.tokenPath[1].toString() + let fee = route.feePath[0] + // Store both directions (Uniswap pools are bidirectional) + feeLookup[addr0.concat("_").concat(addr1)] = fee + feeLookup[addr1.concat("_").concat(addr0)] = fee + } + } + + let intermediaryHex = intermediary.evmAddress.toString() + + // Validate every non-intermediary token has a fee defined with the intermediary + for token in tokens { + if token.flowType == intermediary.flowType { + continue + } + let tokenHex = token.evmAddress.toString() + let feeKey = tokenHex.concat("_").concat(intermediaryHex) + assert(feeLookup.containsKey(feeKey), + message: "Missing fee tier between token ".concat(tokenHex) + .concat(" and intermediary ").concat(intermediaryHex) + .concat(". An explicit single-hop route must exist for each token paired with the intermediary.")) + } + + // Generate routes for all missing pairs + var i = 0 + while i < tokens.length { + var j = 0 + while j < tokens.length { + if i != j { + let ti = tokens[i] + let tj = tokens[j] + + // Skip if either token IS the intermediary + if ti.flowType != intermediary.flowType && tj.flowType != intermediary.flowType { + let key = self._makeKey(ti.flowType, tj.flowType) + + // Only generate if no explicit route exists + if !self.swappers.containsKey(key) { + let tiHex = ti.evmAddress.toString() + let tjHex = tj.evmAddress.toString() + + let fee1 = feeLookup[tiHex.concat("_").concat(intermediaryHex)]! + let fee2 = feeLookup[intermediaryHex.concat("_").concat(tjHex)]! + + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokenPath: [ti.evmAddress, intermediary.evmAddress, tj.evmAddress], + feePath: [fee1, fee2], + inVault: ti.flowType, + outVault: tj.flowType, + coaCapability: coaCapability, + uniqueID: uniqueID + ) + + self.swappers[key] = swapper + } + } + + // Also generate reverse of explicit routes involving the intermediary + // (e.g., if WFLOW->TokenA exists, ensure TokenA->WFLOW also exists) + let reverseKey = self._makeKey(tj.flowType, ti.flowType) + if self.swappers.containsKey(self._makeKey(ti.flowType, tj.flowType)) + && !self.swappers.containsKey(reverseKey) { + // Look up the route info from explicit routes and create a reversed Swapper + for route in routes { + if route.inToken == ti.flowType && route.outToken == tj.flowType { + let reverseSwapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokenPath: route.tokenPath.reverse(), + feePath: route.feePath.reverse(), + inVault: tj.flowType, + outVault: ti.flowType, + coaCapability: coaCapability, + uniqueID: uniqueID + ) + self.swappers[reverseKey] = reverseSwapper + break + } + } + } + } + j = j + 1 + } + i = i + 1 + } + } + } + + /// SwapperProvider interface implementation + /// + /// Returns a pre-computed swapper for the given trade pair, or nil if not supported. + /// + access(all) fun getSwapper(inType: Type, outType: Type): {DeFiActions.Swapper}? { + return self.swappers[self._makeKey(inType, outType)] + } + + /// IdentifiableStruct conformance + /// + /// Returns information about this provider and all its inner swappers. + /// + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { + let inner: [DeFiActions.ComponentInfo] = [] + for swapper in self.swappers.values { + inner.append(swapper.getComponentInfo()) + } + return DeFiActions.ComponentInfo( + type: self.getType(), + id: self.uniqueID?.id, + innerComponents: inner + ) + } + + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { + return self.uniqueID + } + + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { + self.uniqueID = id + } + + /// Helper method to generate consistent dictionary keys + /// + access(self) view fun _makeKey(_ inType: Type, _ outType: Type): String { + return "\(inType.identifier)_TO_\(outType.identifier)" + } + } +} diff --git a/cadence/tests/UniswapV3SwapperProvider_test.cdc b/cadence/tests/UniswapV3SwapperProvider_test.cdc new file mode 100644 index 00000000..97f5714b --- /dev/null +++ b/cadence/tests/UniswapV3SwapperProvider_test.cdc @@ -0,0 +1,517 @@ +import Test +import BlockchainHelpers +import "test_helpers.cdc" + +import "FlowToken" +import "TokenA" +import "TokenB" +import "DeFiActions" +import "UniswapV3SwapConnectors" +import "UniswapV3SwapperProvider" +import "EVM" + +// Global test accounts +access(all) let serviceAccount = Test.serviceAccount() +access(all) let deployerAccount = Test.createAccount() +access(all) let testTokenAccount = Test.createAccount() + +// EVM addresses (populated in setup) +access(all) var wflowHex = "" +access(all) var tokenAHex = "" +access(all) var tokenBHex = "" +access(all) var deployerCOAHex = "" + +// Mock Uniswap V3 addresses (for testing provider logic) +access(all) var uniV3FactoryHex = "0x1234567890123456789012345678901234567890" +access(all) var uniV3RouterHex = "0x2234567890123456789012345678901234567890" +access(all) var uniV3QuoterHex = "0x3234567890123456789012345678901234567890" + +// Test state +access(all) var snapshot: UInt64 = 0 + +access(all) fun setup() { + log("================== Setting up UniswapV3SwapperProvider test ==================") + + // 1. Initialize bridge templates + tempUpsertBridgeTemplateChunks(serviceAccount) + + // 2. Get WFLOW address (should be auto-bridged) + wflowHex = getEVMAddressAssociated(withType: Type<@FlowToken.Vault>().identifier)! + + // 3. Setup test accounts with funding + transferFlow(signer: serviceAccount, recipient: deployerAccount.address, amount: 50.0) + transferFlow(signer: serviceAccount, recipient: testTokenAccount.address, amount: 50.0) + + // 4. Create COAs + createCOA(deployerAccount, fundingAmount: 10.0) + deployerCOAHex = getCOAAddressHex(atFlowAddress: deployerAccount.address) + + // 5. Set mock addresses for TokenA and TokenB + // Note: These are not actually bridged, so tests that validate FlowEVMBridgeConfig + // association will fail. Tests focus on provider logic that can be validated. + tokenAHex = "0x4444444444444444444444444444444444444444" + tokenBHex = "0x5555555555555555555555555555555555555555" + + // 6. Deploy DeFiActions contracts + var err = Test.deployContract( + name: "DeFiActionsUtils", + path: "../contracts/utils/DeFiActionsUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "DeFiActions", + path: "../contracts/interfaces/DeFiActions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "SwapConnectors", + path: "../contracts/connectors/SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "EVMAbiHelpers", + path: "../contracts/utils/EVMAbiHelpers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "UniswapV3SwapConnectors", + path: "../contracts/connectors/evm/UniswapV3SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + err = Test.deployContract( + name: "UniswapV3SwapperProvider", + path: "../contracts/connectors/evm/UniswapV3SwapperProvider.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + snapshot = getCurrentBlockHeight() + log("Setup completed successfully") +} + +access(all) fun testSetupSucceeds() { + log("UniswapV3SwapperProvider deployment success") +} + +/* ==================== Configuration Validation Tests ==================== */ + +access(all) fun testTokenConfigInitWithValidTypes() { + // Should succeed with proper FungibleToken type and associated EVM address + // FlowToken should be auto-bridged in the test environment + let tokenConfig = UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: EVM.addressFromString(wflowHex) + ) + Test.assertEqual(Type<@FlowToken.Vault>(), tokenConfig.flowType) + Test.assertEqual(wflowHex, tokenConfig.evmAddress.toString()) +} + +access(all) fun testTokenConfigFailsWithUnassociatedAddress() { + // Should panic - WFLOW address not associated with TokenA type + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_invalid_token_config.cdc", + [Type<@TokenA.Vault>().identifier, wflowHex] // Wrong address for TokenA + ) + Test.expect(result, Test.beFailed()) +} + +access(all) fun testRouteConfigInitWithValidPath() { + let routeConfig = UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [EVM.addressFromString(wflowHex), EVM.addressFromString(tokenAHex)], + feePath: [3000] + ) + Test.assertEqual(Type<@FlowToken.Vault>(), routeConfig.inToken) + Test.assertEqual(Type<@TokenA.Vault>(), routeConfig.outToken) + Test.assertEqual(2, routeConfig.tokenPath.length) + Test.assertEqual(1, routeConfig.feePath.length) +} + +access(all) fun testRouteConfigFailsWithSingleToken() { + // Should panic - tokenPath must have at least 2 tokens + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_invalid_route_config.cdc", + ["single_token"] + ) + Test.expect(result, Test.beFailed()) +} + +access(all) fun testRouteConfigFailsWithMismatchedFeePath() { + // Should panic - feePath length must be tokenPath.length - 1 + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_invalid_route_config.cdc", + ["mismatched_fee_path"] + ) + Test.expect(result, Test.beFailed()) +} + +access(all) fun testRouteConfigFailsWithSelfSwap() { + // Should panic - inToken cannot equal outToken + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_invalid_route_config.cdc", + ["self_swap"] + ) + Test.expect(result, Test.beFailed()) +} + +/* ==================== Provider Initialization Tests ==================== */ + +access(all) fun testProviderInitWithValidConfiguration() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex, tokenBHex], + 3 // Number of routes + ] + ) + Test.expect(result, Test.beSucceeded()) +} + +access(all) fun testProviderInitFailsWithSingleToken() { + // Should panic - must provide at least 2 tokens + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex], // Only 1 token + 0 + ] + ) + Test.expect(result, Test.beFailed()) +} + +access(all) fun testProviderInitFailsWithNoRoutes() { + // Should panic - must provide at least one route + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex], + 0 // No routes + ] + ) + Test.expect(result, Test.beFailed()) +} + +access(all) fun testProviderInitFailsWithUnconfiguredRouteToken() { + // Should panic - route references token not in token config + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider_invalid_route.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex], // Only WFLOW and TokenA + Type<@TokenB.Vault>().identifier // Route tries to use TokenB + ] + ) + Test.expect(result, Test.beFailed()) +} + +/* ==================== Swapper Retrieval Tests ==================== */ + +access(all) fun testGetSwapperReturnsConfiguredDirectRoute() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // Create provider with WFLOW -> TokenA route + let createResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex, tokenBHex], + 3 + ] + ) + Test.expect(createResult, Test.beSucceeded()) + + // Get swapper for WFLOW -> TokenA + let getResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper.cdc", + [ + deployerAccount.address, + Type<@FlowToken.Vault>().identifier, + Type<@TokenA.Vault>().identifier + ] + ) + Test.expect(getResult, Test.beSucceeded()) + + // Verify swapper exists and has correct types + let hasSwapper = getResult.returnValue as! Bool + Test.assert(hasSwapper, message: "Swapper should exist for configured route") +} + +access(all) fun testGetSwapperReturnsNilForUnconfiguredPair() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // Create provider with only WFLOW <-> TokenA routes + let createResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider_limited.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex] // Only 2 tokens + ] + ) + Test.expect(createResult, Test.beSucceeded()) + + // Try to get swapper for TokenA -> TokenB (not configured) + let getResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper.cdc", + [ + deployerAccount.address, + Type<@TokenA.Vault>().identifier, + Type<@TokenB.Vault>().identifier + ] + ) + Test.expect(getResult, Test.beSucceeded()) + + let hasSwapper = getResult.returnValue as! Bool + Test.assertEqual(false, hasSwapper) +} + +access(all) fun testKeyGenerationIsConsistent() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // Create provider + let createResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex, tokenBHex], + 3 + ] + ) + Test.expect(createResult, Test.beSucceeded()) + + // Get swapper twice with same types + let result1 = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper.cdc", + [ + deployerAccount.address, + Type<@FlowToken.Vault>().identifier, + Type<@TokenA.Vault>().identifier + ] + ) + + let result2 = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper.cdc", + [ + deployerAccount.address, + Type<@FlowToken.Vault>().identifier, + Type<@TokenA.Vault>().identifier + ] + ) + + Test.expect(result1, Test.beSucceeded()) + Test.expect(result2, Test.beSucceeded()) + + // Both should return the same result + let hasSwapper1 = result1.returnValue as! Bool + let hasSwapper2 = result2.returnValue as! Bool + Test.assertEqual(hasSwapper1, hasSwapper2) +} + +/* ==================== ComponentInfo Tests ==================== */ + +access(all) fun testGetComponentInfoContainsAllInnerSwappers() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // Create provider with 3 routes + let createResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex, tokenBHex], + 3 // 3 routes + ] + ) + Test.expect(createResult, Test.beSucceeded()) + + // Get component info + let infoResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_component_info.cdc", + [deployerAccount.address] + ) + Test.expect(infoResult, Test.beSucceeded()) + + let innerComponentCount = infoResult.returnValue as! Int + Test.assertEqual(3, innerComponentCount) +} + +/* ==================== Intermediary Token Tests ==================== */ + +access(all) fun testProviderWithIntermediaryAutoGeneratesRoutes() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // Create provider with intermediary - should succeed + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider_with_intermediary.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex, tokenBHex] + ] + ) + Test.expect(result, Test.beSucceeded()) +} + +access(all) fun testIntermediaryAutoGeneratesMultiHopSwapper() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // TokenA -> TokenB should be auto-generated via WFLOW intermediary + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc", + [ + deployerAccount.address, + Type<@TokenA.Vault>().identifier, + Type<@TokenB.Vault>().identifier + ] + ) + Test.expect(result, Test.beSucceeded()) + + let hasSwapper = result.returnValue as! Bool + Test.assert(hasSwapper, message: "Auto-generated multi-hop swapper should exist for TokenA -> TokenB") +} + +access(all) fun testIntermediaryAutoGeneratesReverseMultiHopSwapper() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // TokenB -> TokenA should also be auto-generated via WFLOW intermediary + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc", + [ + deployerAccount.address, + Type<@TokenB.Vault>().identifier, + Type<@TokenA.Vault>().identifier + ] + ) + Test.expect(result, Test.beSucceeded()) + + let hasSwapper = result.returnValue as! Bool + Test.assert(hasSwapper, message: "Auto-generated reverse multi-hop swapper should exist for TokenB -> TokenA") +} + +access(all) fun testIntermediaryAutoGeneratesReverseOfExplicitRoutes() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // TokenA -> WFLOW should be auto-generated as reverse of explicit WFLOW -> TokenA + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc", + [ + deployerAccount.address, + Type<@TokenA.Vault>().identifier, + Type<@FlowToken.Vault>().identifier + ] + ) + Test.expect(result, Test.beSucceeded()) + + let hasSwapper = result.returnValue as! Bool + Test.assert(hasSwapper, message: "Reverse of explicit route should be auto-generated (TokenA -> WFLOW)") +} + +access(all) fun testIntermediaryExplicitRoutesStillWork() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // WFLOW -> TokenA (explicit) should still work + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc", + [ + deployerAccount.address, + Type<@FlowToken.Vault>().identifier, + Type<@TokenA.Vault>().identifier + ] + ) + Test.expect(result, Test.beSucceeded()) + + let hasSwapper = result.returnValue as! Bool + Test.assert(hasSwapper, message: "Explicit route should still work (WFLOW -> TokenA)") +} + +access(all) fun testIntermediaryComponentInfoCountsAllSwappers() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + let result = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_component_info_with_intermediary.cdc", + [deployerAccount.address] + ) + Test.expect(result, Test.beSucceeded()) + + let innerComponentCount = result.returnValue as! Int + // With 3 tokens and WFLOW intermediary: + // Explicit: WFLOW->TokenA, WFLOW->TokenB (2) + // Auto multi-hop: TokenA->TokenB, TokenB->TokenA (2) + // Auto reverse: TokenA->WFLOW, TokenB->WFLOW (2) + // Total: 6 + Test.assertEqual(6, innerComponentCount) +} + +access(all) fun testProviderWithNilIntermediarySameAsBefore() { + snapshot < getCurrentBlockHeight() ? Test.reset(to: snapshot) : nil + + // nil intermediary should behave the same as before (no auto-generation) + let createResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/create_provider_limited.cdc", + [ + deployerAccount.address, + uniV3FactoryHex, + uniV3RouterHex, + uniV3QuoterHex, + [wflowHex, tokenAHex] + ] + ) + Test.expect(createResult, Test.beSucceeded()) + + // TokenA -> WFLOW should NOT exist without intermediary + let getResult = _executeScript( + "./scripts/uniswap-v3-swapper-provider/get_swapper.cdc", + [ + deployerAccount.address, + Type<@TokenA.Vault>().identifier, + Type<@FlowToken.Vault>().identifier + ] + ) + Test.expect(getResult, Test.beSucceeded()) + + let hasSwapper = getResult.returnValue as! Bool + Test.assertEqual(false, hasSwapper) +} + +/* ==================== Helper Functions ==================== */ +// Note: Helper functions like executeScript and executeTransaction are available from test_helpers.cdc diff --git a/cadence/tests/fork/UniswapV3SwapperProvider_test.cdc b/cadence/tests/fork/UniswapV3SwapperProvider_test.cdc new file mode 100644 index 00000000..bfedc196 --- /dev/null +++ b/cadence/tests/fork/UniswapV3SwapperProvider_test.cdc @@ -0,0 +1,155 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +import "EVM" +import "FlowToken" + +/// Fork test demonstrating UniswapV3SwapperProvider works against REAL Uniswap V3 on Flow EVM +/// +/// This test: +/// - Deploys LATEST LOCAL contracts to forked mainnet +/// - Tests against ACTUAL Uniswap V3 deployment on Flow EVM +/// - Discovers existing pools by querying the factory +/// - Validates quoting and swapping work for real token pairs +/// +/// Uniswap V3 mainnet addresses on Flow EVM: +/// - Factory: 0xca6d7Bb03334bBf135902e1d919a5feccb461632 +/// - SwapRouter02: 0xeEDC6Ff75e1b10B903D9013c358e446a73d35341 +/// - QuoterV2: 0x370A8DF17742867a44e56223EC20D82092242C85 +/// - WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e +/// + +access(all) let factoryAddr = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" +access(all) let routerAddr = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" +access(all) let quoterAddr = "0x370A8DF17742867a44e56223EC20D82092242C85" +access(all) let wflowAddr = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" + +// Known tokens on Flow EVM (bridged ERC20s) +access(all) let wbtcAddr = "0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579" +access(all) let usdcAddr = "0x2d62C27FC8AB0909bf1A25e22f71Dc477Af493D4" +access(all) let usdtAddr = "0x81B56a36d6b8E5cC53588797cB5E10e00D63bD33" +access(all) let ankrFlowAddr = "0x1b97100eA1D7126C4d60027e231EA4CB25314bdb" +access(all) let wethAddr = "0x4F3e652305cbBEE0D04C63F1d1BEEF6B54a95537" +access(all) let usdcfAddr = "0xF1815bd50B0BD60AA38EE3eBd245862e2A87068B" + +access(all) let signer = Test.getAccount(0xb13b21a06b75536d) + +access(all) fun setup() { + // Deploy the LATEST local contracts to the forked environment + var err = Test.deployContract( + name: "DeFiActionsUtils", + path: "../../contracts/utils/DeFiActionsUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + Test.commitBlock() + + err = Test.deployContract( + name: "DeFiActions", + path: "../../contracts/interfaces/DeFiActions.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + Test.commitBlock() + + err = Test.deployContract( + name: "SwapConnectors", + path: "../../contracts/connectors/SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + Test.commitBlock() + + err = Test.deployContract( + name: "EVMAbiHelpers", + path: "../../contracts/utils/EVMAbiHelpers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + Test.commitBlock() + + err = Test.deployContract( + name: "UniswapV3SwapConnectors", + path: "../../contracts/connectors/evm/UniswapV3SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + Test.commitBlock() + + err = Test.deployContract( + name: "UniswapV3SwapperProvider", + path: "../../contracts/connectors/evm/UniswapV3SwapperProvider.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + Test.commitBlock() +} + +/// Discover which pool exists by querying the factory with various token pairs and fee tiers +/// +access(all) fun testDiscoverAndSwap() { + let zeroAddr = "0000000000000000000000000000000000000000" + + // Candidate pairs to try: WFLOW paired with various tokens and fee tiers + let candidateTokens = [wbtcAddr, usdcAddr, usdtAddr, ankrFlowAddr, wethAddr, usdcfAddr] + let feeTiers: [UInt256] = [500, 3000, 10000] + + var foundToken = "" + var foundFee: UInt32 = 0 + + // Try each combination until we find an existing pool + for token in candidateTokens { + for fee in feeTiers { + let result = Test.executeScript( + Test.readFile("../scripts/uniswap-v3-swapper-provider/fork_find_pool.cdc"), + [signer.address, factoryAddr, wflowAddr, token, fee] + ) + Test.expect(result, Test.beSucceeded()) + + let poolAddr = result.returnValue as! String + if poolAddr != zeroAddr && poolAddr != "CALL_FAILED" && poolAddr != "NO_RESULT" { + log("Found pool: WFLOW / ".concat(token).concat(" fee=").concat(fee.toString()).concat(" at ").concat(poolAddr)) + foundToken = token + foundFee = UInt32(fee) + break + } + } + if foundToken != "" { break } + } + + // If no pool found, log and pass (not an error in our code) + if foundToken == "" { + log("No Uniswap V3 pools found for tested pairs - skipping swap test") + return + } + + // Now test a swap through the discovered pool + let tokenInType = Type<@FlowToken.Vault>() + + // Resolve the Cadence type for the discovered token + // The token type is resolved dynamically from FlowEVMBridgeConfig + let tokenOutResult = Test.executeScript( + Test.readFile("../scripts/uniswap-v3-swapper-provider/fork_resolve_type.cdc"), + [foundToken] + ) + Test.expect(tokenOutResult, Test.beSucceeded()) + + let tokenOutTypeIdentifier = tokenOutResult.returnValue as! String + if tokenOutTypeIdentifier == "" { + log("Could not resolve Cadence type for token ".concat(foundToken).concat(" - skipping swap test")) + return + } + + let tokenOutType = CompositeType(tokenOutTypeIdentifier)! + + // Execute swap + let swapTxn = Test.Transaction( + code: Test.readFile("../../transactions/uniswap-v3-swap-connectors/uniswap_v3_swap.cdc"), + authorizers: [signer.address], + signers: [signer], + arguments: [1.0 as UFix64, factoryAddr, routerAddr, quoterAddr, tokenInType, tokenOutType, foundFee] + ) + let swapResult = Test.executeTransaction(swapTxn) + Test.expect(swapResult, Test.beSucceeded()) +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/create_invalid_route_config.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_invalid_route_config.cdc new file mode 100644 index 00000000..0e28ca60 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_invalid_route_config.cdc @@ -0,0 +1,43 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "UniswapV3SwapperProvider" + +/// Attempts to create invalid RouteConfig instances +/// Supported test cases: +/// - "single_token": tokenPath with only 1 token (should fail) +/// - "mismatched_fee_path": feePath length doesn't match tokenPath.length - 1 +/// - "self_swap": inToken equals outToken +/// +access(all) fun main(testCase: String): Bool { + let dummyAddress1 = EVM.addressFromString("0x1234567890123456789012345678901234567890") + let dummyAddress2 = EVM.addressFromString("0x2234567890123456789012345678901234567890") + + if testCase == "single_token" { + // Should panic: "tokenPath must have at least 2 tokens" + let routeConfig = UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [dummyAddress1], // Only 1 token! + feePath: [] + ) + } else if testCase == "mismatched_fee_path" { + // Should panic: "feePath length must be tokenPath.length - 1" + let routeConfig = UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [dummyAddress1, dummyAddress2], // 2 tokens + feePath: [3000, 500] // 2 fees instead of 1! + ) + } else if testCase == "self_swap" { + // Should panic: "Cannot swap token to itself" + let routeConfig = UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@FlowToken.Vault>(), // Same as inToken! + tokenPath: [dummyAddress1, dummyAddress2], + feePath: [3000] + ) + } + + return true +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/create_invalid_token_config.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_invalid_token_config.cdc new file mode 100644 index 00000000..0f4bfca8 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_invalid_token_config.cdc @@ -0,0 +1,18 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "UniswapV3SwapperProvider" + +/// Attempts to create a TokenConfig with mismatched type and EVM address +/// This should fail because the flowType is not associated with the evmAddress +/// +access(all) fun main(typeIdentifier: String, evmAddressHex: String): Bool { + // Try to create a TokenConfig with TokenA type but WFLOW address + // This should panic with "flowType must be associated with evmAddress" + let tokenConfig = UniswapV3SwapperProvider.TokenConfig( + flowType: CompositeType(typeIdentifier)!, + evmAddress: EVM.addressFromString(evmAddressHex) + ) + + return true +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider.cdc new file mode 100644 index 00000000..fdc202a0 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider.cdc @@ -0,0 +1,103 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Creates a UniswapV3SwapperProvider with specified tokens and routes +/// +/// @param deployerAddress - Address of account with COA capability +/// @param factoryHex - Uniswap V3 factory address +/// @param routerHex - Uniswap V3 router address +/// @param quoterHex - Uniswap V3 quoter address +/// @param tokenHexes - Array of token EVM addresses [wflow, tokenA, tokenB, ...] +/// @param numRoutes - Number of routes to create (3 = WFLOW<->TokenA, WFLOW<->TokenB, TokenA<->TokenB) +/// +access(all) fun main( + deployerAddress: Address, + factoryHex: String, + routerHex: String, + quoterHex: String, + tokenHexes: [String], + numRoutes: Int +): Bool { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + // Build token configs + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [] + + // WFLOW + if tokenHexes.length > 0 { + tokens.append(UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[0]) + )) + } + + // TokenA + if tokenHexes.length > 1 { + tokens.append(UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[1]) + )) + } + + // TokenB + if tokenHexes.length > 2 { + tokens.append(UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenB.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[2]) + )) + } + + // Build route configs based on numRoutes + let routes: [UniswapV3SwapperProvider.RouteConfig] = [] + + if numRoutes >= 1 && tokenHexes.length >= 2 { + // Route 1: WFLOW -> TokenA + routes.append(UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [EVM.addressFromString(tokenHexes[0]), EVM.addressFromString(tokenHexes[1])], + feePath: [3000] + )) + } + + if numRoutes >= 2 && tokenHexes.length >= 3 { + // Route 2: WFLOW -> TokenB + routes.append(UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [EVM.addressFromString(tokenHexes[0]), EVM.addressFromString(tokenHexes[2])], + feePath: [3000] + )) + } + + if numRoutes >= 3 && tokenHexes.length >= 3 { + // Route 3: TokenA -> TokenB + routes.append(UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@TokenA.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [EVM.addressFromString(tokenHexes[1]), EVM.addressFromString(tokenHexes[2])], + feePath: [500] + )) + } + + // Create provider + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: EVM.addressFromString(factoryHex), + routerAddress: EVM.addressFromString(routerHex), + quoterAddress: EVM.addressFromString(quoterHex), + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: nil + ) + + return true +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_invalid_route.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_invalid_route.cdc new file mode 100644 index 00000000..b2832c18 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_invalid_route.cdc @@ -0,0 +1,64 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Creates a UniswapV3SwapperProvider with an invalid route +/// The route references a token (TokenB) that's not in the configured tokens +/// This should fail during provider initialization +/// +access(all) fun main( + deployerAddress: Address, + factoryHex: String, + routerHex: String, + quoterHex: String, + tokenHexes: [String], + unconfiguredTokenType: String +): Bool { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + // Build token configs - only WFLOW and TokenA (NOT TokenB) + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[0]) + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[1]) + ) + ] + + // Try to create a route that uses TokenB (which is not configured) + // This should panic during provider initialization + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), // TokenB is not in tokens config! + tokenPath: [ + EVM.addressFromString(tokenHexes[0]), + EVM.addressFromString("0x9999999999999999999999999999999999999999") + ], + feePath: [3000] + ) + ] + + // This should fail with "Route outToken not in configured tokens" + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: EVM.addressFromString(factoryHex), + routerAddress: EVM.addressFromString(routerHex), + quoterAddress: EVM.addressFromString(quoterHex), + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: nil + ) + + return true +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_limited.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_limited.cdc new file mode 100644 index 00000000..2eae18e9 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_limited.cdc @@ -0,0 +1,57 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Creates a UniswapV3SwapperProvider with only WFLOW <-> TokenA routes +/// Used for testing unconfigured pair scenarios +/// +access(all) fun main( + deployerAddress: Address, + factoryHex: String, + routerHex: String, + quoterHex: String, + tokenHexes: [String] +): Bool { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + // Build token configs - only WFLOW and TokenA + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[0]) + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: EVM.addressFromString(tokenHexes[1]) + ) + ] + + // Build route configs - only WFLOW -> TokenA + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [EVM.addressFromString(tokenHexes[0]), EVM.addressFromString(tokenHexes[1])], + feePath: [3000] + ) + ] + + // Create provider + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: EVM.addressFromString(factoryHex), + routerAddress: EVM.addressFromString(routerHex), + quoterAddress: EVM.addressFromString(quoterHex), + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: nil + ) + + return true +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_with_intermediary.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_with_intermediary.cdc new file mode 100644 index 00000000..a14e510f --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/create_provider_with_intermediary.cdc @@ -0,0 +1,85 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Creates a UniswapV3SwapperProvider with an intermediary token (WFLOW) +/// Only explicit routes are WFLOW<->TokenA and WFLOW<->TokenB. +/// TokenA<->TokenB should be auto-generated via the intermediary. +/// +/// @param deployerAddress - Address of account with COA capability +/// @param factoryHex - Uniswap V3 factory address +/// @param routerHex - Uniswap V3 router address +/// @param quoterHex - Uniswap V3 quoter address +/// @param tokenHexes - Array of token EVM addresses [wflow, tokenA, tokenB] +/// +access(all) fun main( + deployerAddress: Address, + factoryHex: String, + routerHex: String, + quoterHex: String, + tokenHexes: [String] +): Bool { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + let wflowAddr = EVM.addressFromString(tokenHexes[0]) + let tokenAAddr = EVM.addressFromString(tokenHexes[1]) + let tokenBAddr = EVM.addressFromString(tokenHexes[2]) + + // Build token configs + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddr + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: tokenAAddr + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenB.Vault>(), + evmAddress: tokenBAddr + ) + ] + + // Only explicit routes: WFLOW<->TokenA and WFLOW<->TokenB + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [wflowAddr, tokenAAddr], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [wflowAddr, tokenBAddr], + feePath: [500] + ) + ] + + // Intermediary is WFLOW + let intermediary = UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddr + ) + + // Create provider - should auto-generate TokenA<->TokenB routes via WFLOW + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: EVM.addressFromString(factoryHex), + routerAddress: EVM.addressFromString(routerHex), + quoterAddress: EVM.addressFromString(quoterHex), + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: intermediary + ) + + return true +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_find_pool.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_find_pool.cdc new file mode 100644 index 00000000..b7afce0c --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_find_pool.cdc @@ -0,0 +1,44 @@ +import "EVM" + +/// Queries the Uniswap V3 factory to check if a pool exists for the given pair and fee tier. +/// Returns the pool address hex string, or "0x0000000000000000000000000000000000000000" if no pool. +/// +access(all) fun main( + signerAddress: Address, + factoryHex: String, + token0Hex: String, + token1Hex: String, + feeTier: UInt256 +): String { + let account = getAuthAccount(signerAddress) + let coaCap = account.capabilities.storage.issue(/storage/evm) + let coa = coaCap.borrow() ?? panic("No COA") + + let factory = EVM.addressFromString(factoryHex) + let token0 = EVM.addressFromString(token0Hex) + let token1 = EVM.addressFromString(token1Hex) + + let calldata = EVM.encodeABIWithSignature( + "getPool(address,address,uint24)", + [token0, token1, feeTier] + ) + + let res = coa.dryCall( + to: factory, + data: calldata, + gasLimit: 120_000, + value: EVM.Balance(attoflow: 0) + ) + + if res.status != EVM.Status.successful { + return "CALL_FAILED" + } + + let decoded = EVM.decodeABI(types: [Type()], data: res.data) + if decoded.length == 0 { + return "NO_RESULT" + } + + let poolAddr = decoded[0] as! EVM.EVMAddress + return poolAddr.toString() +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_quote_direct.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_quote_direct.cdc new file mode 100644 index 00000000..f49874f2 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_quote_direct.cdc @@ -0,0 +1,63 @@ +import "EVM" +import "FlowEVMBridgeConfig" +import "UniswapV3SwapperProvider" +import "UniswapV3SwapConnectors" +import "DeFiActions" + +/// Creates a SwapperProvider with a direct route and returns the quoteOut amount. +/// Used by fork tests against real Uniswap V3 on Flow EVM. +/// +access(all) fun main( + signerAddress: Address, + factoryHex: String, + routerHex: String, + quoterHex: String, + wflowHex: String, + wbtcHex: String, + tokenInType: Type, + tokenOutType: Type +): UFix64 { + let account = getAuthAccount(signerAddress) + let coaCap = account.capabilities.storage.issue(/storage/evm) + assert(coaCap.check(), message: "Invalid COA capability") + + let wflowAddr = EVM.addressFromString(wflowHex) + let wbtcAddr = EVM.addressFromString(wbtcHex) + + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: tokenInType, + evmAddress: wflowAddr + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: tokenOutType, + evmAddress: wbtcAddr + ) + ] + + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: tokenInType, + outToken: tokenOutType, + tokenPath: [wflowAddr, wbtcAddr], + feePath: [10000] + ) + ] + + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: EVM.addressFromString(factoryHex), + routerAddress: EVM.addressFromString(routerHex), + quoterAddress: EVM.addressFromString(quoterHex), + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: nil + ) + + let swapper = provider.getSwapper(inType: tokenInType, outType: tokenOutType) + ?? panic("No swapper found for pair") + + let quote = swapper.quoteOut(forProvided: 1.0, reverse: false) + return quote.outAmount +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_resolve_type.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_resolve_type.cdc new file mode 100644 index 00000000..992cca8a --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/fork_resolve_type.cdc @@ -0,0 +1,14 @@ +import "EVM" +import "FlowEVMBridgeConfig" + +/// Resolves the Cadence type identifier for a given EVM token address. +/// Returns the type identifier string, or empty string if not found. +/// +access(all) fun main(tokenHex: String): String { + let evmAddr = EVM.addressFromString(tokenHex) + let associatedType = FlowEVMBridgeConfig.getTypeAssociated(with: evmAddr) + if let t = associatedType { + return t.identifier + } + return "" +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/get_component_info.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_component_info.cdc new file mode 100644 index 00000000..fecfe13f --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_component_info.cdc @@ -0,0 +1,78 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Tests getComponentInfo() functionality +/// Returns the number of inner components in the provider +/// +access(all) fun main(deployerAddress: Address): Int { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + // Create dummy addresses for testing + let wflowAddress = EVM.addressFromString("0x1234567890123456789012345678901234567890") + let tokenAAddress = EVM.addressFromString("0x2234567890123456789012345678901234567890") + let tokenBAddress = EVM.addressFromString("0x3234567890123456789012345678901234567890") + let factoryAddress = EVM.addressFromString("0x4234567890123456789012345678901234567890") + let routerAddress = EVM.addressFromString("0x5234567890123456789012345678901234567890") + let quoterAddress = EVM.addressFromString("0x6234567890123456789012345678901234567890") + + // Create a provider with 3 routes + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: tokenAAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenB.Vault>(), + evmAddress: tokenBAddress + ) + ] + + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [wflowAddress, tokenAAddress], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [wflowAddress, tokenBAddress], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@TokenA.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [tokenAAddress, tokenBAddress], + feePath: [500] + ) + ] + + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: nil + ) + + // Get component info + let info = provider.getComponentInfo() + + // Return the number of inner components + return info.innerComponents.length +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/get_component_info_with_intermediary.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_component_info_with_intermediary.cdc new file mode 100644 index 00000000..53cbd5ed --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_component_info_with_intermediary.cdc @@ -0,0 +1,77 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Creates a provider with intermediary and returns the number of inner components. +/// With 3 tokens (WFLOW, TokenA, TokenB) and WFLOW as intermediary: +/// - Explicit: WFLOW->TokenA, WFLOW->TokenB (2) +/// - Auto-generated: TokenA->TokenB, TokenB->TokenA (2 multi-hop) +/// - Auto-generated reverses: TokenA->WFLOW, TokenB->WFLOW (2 reverses of explicit) +/// Total: 6 swappers +/// +access(all) fun main(deployerAddress: Address): Int { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + let wflowAddress = EVM.addressFromString("0x1234567890123456789012345678901234567890") + let tokenAAddress = EVM.addressFromString("0x2234567890123456789012345678901234567890") + let tokenBAddress = EVM.addressFromString("0x3234567890123456789012345678901234567890") + let factoryAddress = EVM.addressFromString("0x4234567890123456789012345678901234567890") + let routerAddress = EVM.addressFromString("0x5234567890123456789012345678901234567890") + let quoterAddress = EVM.addressFromString("0x6234567890123456789012345678901234567890") + + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: tokenAAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenB.Vault>(), + evmAddress: tokenBAddress + ) + ] + + // Only explicit routes through WFLOW + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [wflowAddress, tokenAAddress], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [wflowAddress, tokenBAddress], + feePath: [500] + ) + ] + + let intermediary = UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddress + ) + + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: intermediary + ) + + let info = provider.getComponentInfo() + return info.innerComponents.length +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/get_swapper.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_swapper.cdc new file mode 100644 index 00000000..3867dafc --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_swapper.cdc @@ -0,0 +1,85 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Tests getSwapper() functionality by creating a provider and checking if it returns a swapper +/// Returns true if swapper exists, false if nil +/// +access(all) fun main( + deployerAddress: Address, + inTypeIdentifier: String, + outTypeIdentifier: String +): Bool { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + // Create dummy addresses for testing + let wflowAddress = EVM.addressFromString("0x1234567890123456789012345678901234567890") + let tokenAAddress = EVM.addressFromString("0x2234567890123456789012345678901234567890") + let tokenBAddress = EVM.addressFromString("0x3234567890123456789012345678901234567890") + let factoryAddress = EVM.addressFromString("0x4234567890123456789012345678901234567890") + let routerAddress = EVM.addressFromString("0x5234567890123456789012345678901234567890") + let quoterAddress = EVM.addressFromString("0x6234567890123456789012345678901234567890") + + // Create a simple provider with WFLOW <-> TokenA route + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: tokenAAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenB.Vault>(), + evmAddress: tokenBAddress + ) + ] + + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [wflowAddress, tokenAAddress], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [wflowAddress, tokenBAddress], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@TokenA.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [tokenAAddress, tokenBAddress], + feePath: [500] + ) + ] + + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: nil + ) + + // Convert type identifiers to Type + let inType = CompositeType(inTypeIdentifier)! + let outType = CompositeType(outTypeIdentifier)! + + // Try to get swapper + let swapper = provider.getSwapper(inType: inType, outType: outType) + + return swapper != nil +} diff --git a/cadence/tests/scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc new file mode 100644 index 00000000..0d92bb74 --- /dev/null +++ b/cadence/tests/scripts/uniswap-v3-swapper-provider/get_swapper_with_intermediary.cdc @@ -0,0 +1,85 @@ +import "EVM" +import "FlowToken" +import "TokenA" +import "TokenB" +import "UniswapV3SwapperProvider" +import "DeFiActions" + +/// Creates a provider with intermediary token and tests getSwapper for various pairs. +/// Returns true if the swapper exists for the given pair. +/// +access(all) fun main( + deployerAddress: Address, + inTypeIdentifier: String, + outTypeIdentifier: String +): Bool { + // Get COA capability + let account = getAuthAccount(deployerAddress) + let coaCap = account.capabilities.get(/public/evm) + ?? panic("Missing COA capability") + + // Create dummy addresses for testing + let wflowAddress = EVM.addressFromString("0x1234567890123456789012345678901234567890") + let tokenAAddress = EVM.addressFromString("0x2234567890123456789012345678901234567890") + let tokenBAddress = EVM.addressFromString("0x3234567890123456789012345678901234567890") + let factoryAddress = EVM.addressFromString("0x4234567890123456789012345678901234567890") + let routerAddress = EVM.addressFromString("0x5234567890123456789012345678901234567890") + let quoterAddress = EVM.addressFromString("0x6234567890123456789012345678901234567890") + + let tokens: [UniswapV3SwapperProvider.TokenConfig] = [ + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenA.Vault>(), + evmAddress: tokenAAddress + ), + UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@TokenB.Vault>(), + evmAddress: tokenBAddress + ) + ] + + // Only explicit routes through WFLOW + let routes: [UniswapV3SwapperProvider.RouteConfig] = [ + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenA.Vault>(), + tokenPath: [wflowAddress, tokenAAddress], + feePath: [3000] + ), + UniswapV3SwapperProvider.RouteConfig( + inToken: Type<@FlowToken.Vault>(), + outToken: Type<@TokenB.Vault>(), + tokenPath: [wflowAddress, tokenBAddress], + feePath: [500] + ) + ] + + // WFLOW as intermediary + let intermediary = UniswapV3SwapperProvider.TokenConfig( + flowType: Type<@FlowToken.Vault>(), + evmAddress: wflowAddress + ) + + let provider = UniswapV3SwapperProvider.SwapperProvider( + factoryAddress: factoryAddress, + routerAddress: routerAddress, + quoterAddress: quoterAddress, + tokens: tokens, + routes: routes, + coaCapability: coaCap, + uniqueID: nil, + intermediaryToken: intermediary + ) + + // Convert type identifiers to Type + let inType = CompositeType(inTypeIdentifier)! + let outType = CompositeType(outTypeIdentifier)! + + // Try to get swapper + let swapper = provider.getSwapper(inType: inType, outType: outType) + + return swapper != nil +} diff --git a/cadence/transactions/uniswap-v3-swap-connectors/uniswap_v3_swap.cdc b/cadence/transactions/uniswap-v3-swap-connectors/uniswap_v3_swap.cdc new file mode 100644 index 00000000..a7f59ec7 --- /dev/null +++ b/cadence/transactions/uniswap-v3-swap-connectors/uniswap_v3_swap.cdc @@ -0,0 +1,91 @@ +import "FungibleToken" +import "EVM" +import "FlowEVMBridgeConfig" +import "UniswapV3SwapConnectors" +import "DeFiActions" + +/// Generic Uniswap V3 swap transaction +/// +/// Accepts Cadence token Types and resolves EVM addresses via FlowEVMBridgeConfig. +/// Currently supports FlowToken as input token (hardcoded vault path). +/// Output token is fully generic - any bridged token Type works. +/// +/// @param amount: Amount of input token to swap +/// @param factoryAddress: Uniswap V3 Factory EVM address +/// @param routerAddress: Uniswap V3 SwapRouter02 EVM address +/// @param quoterAddress: Uniswap V3 QuoterV2 EVM address +/// @param tokenInType: Cadence Type of input token (must be bridged) +/// @param tokenOutType: Cadence Type of output token (must be bridged) +/// @param feeTier: Fee tier in basis points (e.g. 500, 3000, 10000) +/// +transaction( + amount: UFix64, + factoryAddress: String, + routerAddress: String, + quoterAddress: String, + tokenInType: Type, + tokenOutType: Type, + feeTier: UInt32 +) { + let swapper: UniswapV3SwapConnectors.Swapper + let tokenInVault: @{FungibleToken.Vault} + let quote: {DeFiActions.Quote} + var tokenOut: UFix64 + + prepare(signer: auth(Storage, IssueStorageCapabilityController, BorrowValue) &Account) { + self.tokenOut = 0.0 + + let coaCap = signer.capabilities.storage.issue(/storage/evm) + assert(coaCap.check(), message: "COA capability is invalid") + + let factory = EVM.addressFromString(factoryAddress) + let router = EVM.addressFromString(routerAddress) + let quoter = EVM.addressFromString(quoterAddress) + + let tokenInEVMAddr = FlowEVMBridgeConfig.getEVMAddressAssociated(with: tokenInType) + ?? panic("Token in type not bridged: ".concat(tokenInType.identifier)) + let tokenOutEVMAddr = FlowEVMBridgeConfig.getEVMAddressAssociated(with: tokenOutType) + ?? panic("Token out type not bridged: ".concat(tokenOutType.identifier)) + + self.swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: factory, + routerAddress: router, + quoterAddress: quoter, + tokenPath: [tokenInEVMAddr, tokenOutEVMAddr], + feePath: [feeTier], + inVault: tokenInType, + outVault: tokenOutType, + coaCapability: coaCap, + uniqueID: nil + ) + + self.quote = self.swapper.quoteOut(forProvided: amount, reverse: false) + + self.tokenInVault <- signer.storage.borrow( + from: /storage/flowTokenVault + )!.withdraw(amount: amount) + } + + pre { + self.tokenInVault.balance == amount: + "Invalid token balance of \(self.tokenInVault.balance) - expected \(amount)" + self.swapper.inType() == self.tokenInVault.getType(): + "Invalid swapper inType of \(self.swapper.inType().identifier) - expected \(self.tokenInVault.getType().identifier)" + self.swapper.outType() == tokenOutType: + "Invalid swapper outType of \(self.swapper.outType().identifier) - expected \(tokenOutType.identifier)" + } + + execute { + let tokenOutVault <- self.swapper.swap(quote: self.quote, inVault: <-self.tokenInVault) + self.tokenOut = tokenOutVault.balance + log("Swapped ".concat(amount.toString()).concat(" → ").concat(tokenOutVault.balance.toString())) + destroy tokenOutVault + } + + post { + self.tokenOut > 0.0: + "Swap output must be greater than 0" + self.quote.outAmount == 0.0 || self.tokenOut >= self.quote.outAmount: + "Swap output (\(self.tokenOut)) must be at least quote amount (\(self.quote.outAmount))" + } +} diff --git a/flow.json b/flow.json index 235cd4bf..80cf0992 100644 --- a/flow.json +++ b/flow.json @@ -47,6 +47,7 @@ "EVMAbiHelpers": { "source": "./cadence/contracts/utils/EVMAbiHelpers.cdc", "aliases": { + "mainnet": "6d888f175c158410", "testing": "0000000000000009" } }, @@ -145,6 +146,14 @@ "UniswapV3SwapConnectors": { "source": "cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", "aliases": { + "mainnet": "6d888f175c158410", + "testing": "0000000000000009" + } + }, + "UniswapV3SwapperProvider": { + "source": "cadence/contracts/connectors/evm/UniswapV3SwapperProvider.cdc", + "aliases": { + "mainnet": "6d888f175c158410", "testing": "0000000000000009" } } @@ -359,7 +368,7 @@ }, "FlowTransactionSchedulerUtils": { "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", - "hash": "b5d6f06dd43e4cee907e08a5bc46df0bb9c2338d806d9d253789aee4c4ac01ad", + "hash": "429ed886472cd65def9e5ab1dd20079b0dcfb23095d18d54077767ac3316a8ce", "aliases": { "mainnet": "e467b9dd11fa00df" } @@ -676,20 +685,20 @@ "resourceID": "projects/flow-foundation-admin/locations/global/keyRings/defi-actions/cryptoKeys/mainnet-defi-actions/cryptoKeyVersions/1" } }, - "mainnet-uniswapv2-swap-connectors": { - "address": "f94f371678513b2b", + "mainnet-uniswapV3-connectors-deployer": { + "address": "a7825d405ac89518", "key": { "type": "google-kms", "hashAlgorithm": "SHA2_256", - "resourceID": "projects/flow-foundation-admin/locations/global/keyRings/defi-actions/cryptoKeys/mainnet-defi-actions/cryptoKeyVersions/1" + "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" } }, - "mainnet-uniswapV3-connectors-deployer": { - "address": "a7825d405ac89518", + "mainnet-uniswapv2-swap-connectors": { + "address": "f94f371678513b2b", "key": { "type": "google-kms", "hashAlgorithm": "SHA2_256", - "resourceID": "projects/dl-flow-devex-production/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" + "resourceID": "projects/flow-foundation-admin/locations/global/keyRings/defi-actions/cryptoKeys/mainnet-defi-actions/cryptoKeyVersions/1" } }, "testnet-band-oracle-connectors": { @@ -786,6 +795,8 @@ "ERC4626PriceOracles", "IncrementFiSwapConnectors", "IncrementFiFlashloanConnectors", + "UniswapV3SwapConnectors", + "UniswapV3SwapperProvider", "SwapPair", "FlowEVMBridgeUtils", "SerializeMetadata", @@ -849,14 +860,14 @@ "mainnet-swap-connectors": [ "SwapConnectors" ], - "mainnet-uniswapv2-swap-connectors": [ - "UniswapV2SwapConnectors" - ], "mainnet-uniswapV3-connectors-deployer": [ "EVMAbiHelpers", - "UniswapV3SwapConnectors" + "UniswapV3SwapConnectors", + "UniswapV3SwapperProvider" + ], + "mainnet-uniswapv2-swap-connectors": [ + "UniswapV2SwapConnectors" ] - }, "testnet": { "testnet-band-oracle-connectors": [ @@ -890,7 +901,8 @@ ], "testnet-uniswapV3-swap-connectors": [ "EVMAbiHelpers", - "UniswapV3SwapConnectors" + "UniswapV3SwapConnectors", + "UniswapV3SwapperProvider" ], "testnet-uniswapv2-swap-connectors": [ "UniswapV2SwapConnectors"