Updated on 2026-08-14
This commit is contained in:
parent
c3740a70c0
commit
312f241f7c
4 changed files with 177 additions and 2 deletions
|
|
@ -476,6 +476,16 @@ internal class DefaultWalletManagersFacade @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun isSwapSpenderAllowed(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
spenderAddress: String,
|
||||
): Boolean = withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
|
||||
?: return@withContext false
|
||||
walletManager.isSwapSpenderAllowed(spenderAddress)
|
||||
}
|
||||
|
||||
override suspend fun getDynamicAddressesReceiveAddress(userWalletId: UserWalletId, network: Network): String? {
|
||||
val dynamicAddressesManager = getEnabledDynamicAddressesManagerOrNull(userWalletId, network) ?: return null
|
||||
return dynamicAddressesManager.findFirstUnusedReceiveAddress()?.address
|
||||
|
|
|
|||
|
|
@ -246,6 +246,8 @@ interface WalletManagersFacade {
|
|||
*/
|
||||
suspend fun getPsbtFee(userWalletId: UserWalletId, network: Network, psbtBase64: String): BigDecimal?
|
||||
|
||||
suspend fun isSwapSpenderAllowed(userWalletId: UserWalletId, network: Network, spenderAddress: String): Boolean
|
||||
|
||||
/**
|
||||
* Get requirements for asset(currency)
|
||||
* @return null if there's no requirement, otherwise [AssetRequirementsCondition].
|
||||
|
|
|
|||
|
|
@ -131,11 +131,29 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
ConcurrentHashMap<IntegratedApprovalFallbackKey, Boolean>(),
|
||||
)
|
||||
|
||||
private val yieldSwapAllowedRouters = newSetFromMap(ConcurrentHashMap<String, Boolean>())
|
||||
|
||||
private fun hasIntegratedApprovalFallenBack(fromSwapCurrencyStatus: SwapCurrencyStatus, spenderAddress: String?) =
|
||||
integratedApprovalFallbackContexts.contains(
|
||||
IntegratedApprovalFallbackKey.of(fromSwapCurrencyStatus, spenderAddress),
|
||||
)
|
||||
|
||||
private suspend fun isYieldSwapRouterAllowed(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
routerAddress: String,
|
||||
): Boolean {
|
||||
val network = fromSwapCurrencyStatus.currency.network
|
||||
val key = "${network.rawId}:${routerAddress.lowercase()}"
|
||||
if (yieldSwapAllowedRouters.contains(key)) return true
|
||||
val isAllowed = walletManagersFacade.isSwapSpenderAllowed(
|
||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||
network = network,
|
||||
spenderAddress = routerAddress,
|
||||
)
|
||||
if (isAllowed) yieldSwapAllowedRouters.add(key)
|
||||
return isAllowed
|
||||
}
|
||||
|
||||
override suspend fun getPair(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
|
|
@ -304,7 +322,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll().toMap()
|
||||
}.awaitAll().filterNotNull().toMap()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +334,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount: SwapAmount,
|
||||
reduceBalanceBy: BigDecimal,
|
||||
expressOperationType: ExpressOperationType,
|
||||
): Pair<SwapProvider, SwapState> {
|
||||
): Pair<SwapProvider, SwapState>? {
|
||||
if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true &&
|
||||
!swapFeatureToggles.isYieldSwapEnabled
|
||||
) {
|
||||
|
|
@ -366,6 +384,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
|
||||
val dexRouterSpenderAddress = maybeQuote.getOrNull()?.allowanceContract
|
||||
|
||||
if (isYieldSwap && dexRouterSpenderAddress != null &&
|
||||
!isYieldSwapRouterAllowed(fromSwapCurrencyStatus, dexRouterSpenderAddress)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
val allowanceInfo = spenderAddress?.let { allowanceContract ->
|
||||
getAllowanceInfoUseCase(
|
||||
userWalletId = fromSwapCurrencyStatus.userWalletId,
|
||||
|
|
|
|||
|
|
@ -888,6 +888,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
|||
coEvery {
|
||||
yieldModuleAddressProvider.getOrFetch(any(), any())
|
||||
} returns yieldProxyAddress
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns true
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -1131,6 +1132,144 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase(
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class YieldSwapRouterAllowlist {
|
||||
|
||||
private val yieldProxyAddress = "0xYieldModuleProxy"
|
||||
private val yieldTokenContract = "0xTokenContract"
|
||||
private val notAllowedRouter = "0xMoonPayRouter"
|
||||
private val allowedRouter = "0xOneInchRouter"
|
||||
|
||||
@BeforeEach
|
||||
fun enableYieldSwap() {
|
||||
every { swapFeatureToggles.isYieldSwapEnabled } returns true
|
||||
coEvery { yieldModuleAddressProvider.getOrFetch(any(), any()) } returns yieldProxyAddress
|
||||
}
|
||||
|
||||
private fun yieldTokenStatus(yieldActive: Boolean = true) = buildSwapCurrencyStatus(
|
||||
networkRawId = ethNetwork,
|
||||
contractAddress = yieldTokenContract,
|
||||
isCoin = false,
|
||||
amount = BigDecimal("10"),
|
||||
yieldSupplyActive = yieldActive,
|
||||
yieldSupplyAllowedToSpend = true,
|
||||
)
|
||||
|
||||
private fun stubDexQuote(providerId: String, router: String) {
|
||||
coEvery {
|
||||
repository.findBestQuote(
|
||||
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||
toContractAddress = any(), toNetwork = any(), fromAmount = any(),
|
||||
fromDecimals = any(), toDecimals = any(),
|
||||
providerId = providerId, rateType = any(),
|
||||
)
|
||||
} returns buildQuoteModel(allowanceContract = router).right()
|
||||
}
|
||||
|
||||
private fun stubExchangeData(providerId: String) {
|
||||
coEvery {
|
||||
repository.getExchangeData(
|
||||
userWallet = any(), fromContractAddress = any(), fromNetwork = any(),
|
||||
toContractAddress = any(), fromAddress = any(), toNetwork = any(),
|
||||
fromAmount = any(), fromDecimals = any(), toDecimals = any(),
|
||||
providerId = providerId, rateType = any(), toAddress = any(),
|
||||
expressOperationType = any(), refundAddress = any(),
|
||||
)
|
||||
} returns buildSwapDataModelDex().right()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should hide yield-swap DEX provider whose router is not allowed by the registry`() = runTest {
|
||||
// Given — yield active, router NOT in the SwapExecutionRegistry (MoonPay/swaps.xyz)
|
||||
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||
stubDexQuote(dexProvider.providerId, notAllowedRouter)
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), notAllowedRouter) } returns false
|
||||
|
||||
// When
|
||||
val result = sut.findBestQuote(
|
||||
fromSwapCurrencyStatus = yieldTokenStatus(),
|
||||
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||
providers = listOf(dexProvider),
|
||||
amountToSwap = "1.0",
|
||||
reduceBalanceBy = BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
// Then — provider is absent from the list
|
||||
assertThat(result.containsKey(dexProvider)).isFalse()
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should keep yield-swap DEX provider whose router is allowed by the registry`() = runTest {
|
||||
// Given — yield active, router whitelisted (1inch)
|
||||
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||
stubDexQuote(dexProvider.providerId, allowedRouter)
|
||||
stubExchangeData(dexProvider.providerId)
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), allowedRouter) } returns true
|
||||
|
||||
// When
|
||||
val result = sut.findBestQuote(
|
||||
fromSwapCurrencyStatus = yieldTokenStatus(),
|
||||
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||
providers = listOf(dexProvider),
|
||||
amountToSwap = "1.0",
|
||||
reduceBalanceBy = BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
// Then
|
||||
assertThat(result.containsKey(dexProvider)).isTrue()
|
||||
assertThat(result[dexProvider]).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should hide only the not-allowed router and keep the allowed one for yield swaps`() = runTest {
|
||||
// Given — two DEX providers, only one router whitelisted
|
||||
val allowedProvider = buildSwapProvider(ExchangeProviderType.DEX, "dex-allowed")
|
||||
val blockedProvider = buildSwapProvider(ExchangeProviderType.DEX, "dex-blocked")
|
||||
stubDexQuote(allowedProvider.providerId, allowedRouter)
|
||||
stubDexQuote(blockedProvider.providerId, notAllowedRouter)
|
||||
stubExchangeData(allowedProvider.providerId)
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), allowedRouter) } returns true
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), notAllowedRouter) } returns false
|
||||
|
||||
// When
|
||||
val result = sut.findBestQuote(
|
||||
fromSwapCurrencyStatus = yieldTokenStatus(),
|
||||
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||
providers = listOf(allowedProvider, blockedProvider),
|
||||
amountToSwap = "1.0",
|
||||
reduceBalanceBy = BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
// Then
|
||||
assertThat(result.containsKey(allowedProvider)).isTrue()
|
||||
assertThat(result.containsKey(blockedProvider)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not apply the registry filter to regular non-yield swaps`() = runTest {
|
||||
// Given — yield NOT active; the registry verdict must be irrelevant for plain DEX swaps
|
||||
val dexProvider = buildSwapProvider(ExchangeProviderType.DEX)
|
||||
stubDexQuote(dexProvider.providerId, notAllowedRouter)
|
||||
stubExchangeData(dexProvider.providerId)
|
||||
coEvery { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) } returns false
|
||||
|
||||
// When
|
||||
val result = sut.findBestQuote(
|
||||
fromSwapCurrencyStatus = yieldTokenStatus(yieldActive = false),
|
||||
toSwapCurrencyStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork),
|
||||
providers = listOf(dexProvider),
|
||||
amountToSwap = "1.0",
|
||||
reduceBalanceBy = BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
// Then — provider stays; the on-chain allowlist does not gate non-yield swaps
|
||||
assertThat(result.containsKey(dexProvider)).isTrue()
|
||||
coVerify(exactly = 0) { walletManagersFacade.isSwapSpenderAllowed(any(), any(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular (non-yield) DEX swap with the integrated-approve toggle ON: the
|
||||
* `isAllowanceSatisfied` matrix in `manageDex`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue