-
Notifications
You must be signed in to change notification settings - Fork 2
slippage protection using price feed #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
3ee57ad to
d3a714b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements slippage protection for merchant payments in the DSPay protocol. It adds configurable slippage tolerance that validates the exchange rate between source and target assets using Chainlink price feeds during swaps.
- Extends MerchantConfig with slippage protection settings (slippage percentage and price feed address)
- Adds slippage validation logic in SwapLogic using price feeds to calculate USD values
- Updates all test files to include the new slippageProtection field in merchant configurations
Reviewed Changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/MerchantLogic.sol | Added SlippageProtection struct and SlippageToleranceExceeded error |
| src/libraries/SwapLogic.sol | Added slippage validation using price feeds and integrated with swap operations |
| src/module/PaymentLogic.sol | Updated payment processing to pass merchant config slippage protection to swap logic |
| test/PaymentFunctions.t.sol | Added comprehensive tests for slippage protection functionality |
| test/module/PaymentLogic.t.sol | Updated test wrappers to include slippage protection in merchant configs |
| test/libraries/MerchantLogic.t.sol | Updated all test cases to include slippage protection fields |
| test/libraries/SwapLogic.t.sol | Added asset mapping and updated swap logic test wrapper |
| test/DSPayMerchantConfig.t.sol | Updated merchant config tests with slippage protection |
| foundry.toml | Enabled via_ir optimization flag |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| receivedTargetAssetAmount = _swapExactAmountIn( | ||
| _swapLogicStorage.swapLogicConfig.router, swapPath, sourceAssetAmountIn, targetAssetRecipient | ||
| ); | ||
|
|
||
| if (slippageProtection.targetAssetPriceFeed != ZERO_ADDRESS) { | ||
| _validateSlippage( | ||
| sourceAssetAmountIn, | ||
| sourceAssetPriceFeed, | ||
| sourceAssetDecimals, | ||
| receivedTargetAssetAmount, | ||
| slippageProtection.targetAssetPriceFeed, | ||
| targetAssetDecimals, | ||
| slippageProtection.slippage | ||
| ); | ||
| } |
Copilot
AI
Sep 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The slippage validation occurs after the swap has already been executed. If slippage exceeds the tolerance, the transaction will revert but the swap will have already consumed gas and potentially moved market prices. Consider validating expected slippage before executing the swap using price estimates.
| receivedTargetAssetAmount = _swapExactAmountIn( | |
| _swapLogicStorage.swapLogicConfig.router, swapPath, sourceAssetAmountIn, targetAssetRecipient | |
| ); | |
| if (slippageProtection.targetAssetPriceFeed != ZERO_ADDRESS) { | |
| _validateSlippage( | |
| sourceAssetAmountIn, | |
| sourceAssetPriceFeed, | |
| sourceAssetDecimals, | |
| receivedTargetAssetAmount, | |
| slippageProtection.targetAssetPriceFeed, | |
| targetAssetDecimals, | |
| slippageProtection.slippage | |
| ); | |
| } | |
| // Pre-swap slippage validation using price feeds | |
| if (slippageProtection.targetAssetPriceFeed != ZERO_ADDRESS) { | |
| // Estimate target asset amount using price feeds | |
| ( | |
| , | |
| int256 sourceAssetPrice | |
| ) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | |
| ( | |
| , | |
| int256 targetAssetPrice | |
| ) = AggregatorV3Interface(slippageProtection.targetAssetPriceFeed).latestRoundData(); | |
| require(sourceAssetPrice > 0 && targetAssetPrice > 0, "Invalid price feed data"); | |
| // Normalize prices to 18 decimals for calculation | |
| uint256 sourceAssetPriceNorm = uint256(sourceAssetPrice) * (10 ** (18 - sourceAssetDecimals)); | |
| uint256 targetAssetPriceNorm = uint256(targetAssetPrice) * (10 ** (18 - targetAssetDecimals)); | |
| // Estimate target asset amount | |
| uint256 estimatedTargetAssetAmount = (sourceAssetAmountIn * sourceAssetPriceNorm) / targetAssetPriceNorm; | |
| _validateSlippage( | |
| sourceAssetAmountIn, | |
| sourceAssetPriceFeed, | |
| sourceAssetDecimals, | |
| estimatedTargetAssetAmount, | |
| slippageProtection.targetAssetPriceFeed, | |
| targetAssetDecimals, | |
| slippageProtection.slippage | |
| ); | |
| } | |
| receivedTargetAssetAmount = _swapExactAmountIn( | |
| _swapLogicStorage.swapLogicConfig.router, swapPath, sourceAssetAmountIn, targetAssetRecipient | |
| ); |
| if (sourceValueInUSD > 0) { | ||
| uint256 slippageBasisPoints = ((sourceValueInUSD - targetValueInUSD) * 10000) / sourceValueInUSD; | ||
| if (slippageBasisPoints > maxSlippage) { | ||
| revert MerchantLogic.SlippageToleranceExceeded(); | ||
| } | ||
| } |
Copilot
AI
Sep 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The slippage calculation will underflow if targetValueInUSD > sourceValueInUSD, causing the transaction to revert unexpectedly. This could happen in favorable swap scenarios. Add a check to handle cases where the received value exceeds the input value.
| // slither-disable-next-line unused-return | ||
| (, int256 sourcePrice,,,) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | ||
| // slither-disable-next-line unused-return | ||
| (, int256 targetPrice,,,) = AggregatorV3Interface(targetAssetPriceFeed).latestRoundData(); | ||
|
|
Copilot
AI
Sep 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The price feed calls lack validation for stale or invalid data. Chainlink aggregators can return negative prices, zero prices, or stale data. Add validation to check that prices are positive, recent (using updatedAt timestamp), and within reasonable bounds.
| // slither-disable-next-line unused-return | |
| (, int256 sourcePrice,,,) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | |
| // slither-disable-next-line unused-return | |
| (, int256 targetPrice,,,) = AggregatorV3Interface(targetAssetPriceFeed).latestRoundData(); | |
| // Chainlink price data staleness threshold (e.g., 1 hour) | |
| uint256 constant STALENESS_THRESHOLD = 3600; | |
| // Fetch source price and updatedAt | |
| (, int256 sourcePrice,, uint256 sourceUpdatedAt,) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | |
| // Fetch target price and updatedAt | |
| (, int256 targetPrice,, uint256 targetUpdatedAt,) = AggregatorV3Interface(targetAssetPriceFeed).latestRoundData(); | |
| // Validate source price | |
| if (sourcePrice <= 0) { | |
| revert("Invalid source price from price feed"); | |
| } | |
| if (block.timestamp - sourceUpdatedAt > STALENESS_THRESHOLD) { | |
| revert("Stale source price feed data"); | |
| } | |
| // Validate target price | |
| if (targetPrice <= 0) { | |
| revert("Invalid target price from price feed"); | |
| } | |
| if (block.timestamp - targetUpdatedAt > STALENESS_THRESHOLD) { | |
| revert("Stale target price feed data"); | |
| } |
| // slither-disable-next-line unused-return | ||
| (, int256 sourcePrice,,,) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | ||
| // slither-disable-next-line unused-return | ||
| (, int256 targetPrice,,,) = AggregatorV3Interface(targetAssetPriceFeed).latestRoundData(); | ||
|
|
Copilot
AI
Sep 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The price feed calls lack validation for stale or invalid data. Chainlink aggregators can return negative prices, zero prices, or stale data. Add validation to check that prices are positive, recent (using updatedAt timestamp), and within reasonable bounds.
| // slither-disable-next-line unused-return | |
| (, int256 sourcePrice,,,) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | |
| // slither-disable-next-line unused-return | |
| (, int256 targetPrice,,,) = AggregatorV3Interface(targetAssetPriceFeed).latestRoundData(); | |
| // Chainlink price feed staleness threshold (e.g., 1 hour) | |
| uint256 STALENESS_THRESHOLD = 3600; | |
| // Validate source price feed | |
| ( | |
| uint80 sourceRoundId, | |
| int256 sourcePrice, | |
| , | |
| uint256 sourceUpdatedAt, | |
| uint80 sourceAnsweredInRound | |
| ) = AggregatorV3Interface(sourceAssetPriceFeed).latestRoundData(); | |
| if (sourcePrice <= 0) revert MerchantLogic.InvalidPriceFeed(); | |
| if (sourceUpdatedAt == 0 || block.timestamp - sourceUpdatedAt > STALENESS_THRESHOLD) revert MerchantLogic.StalePriceFeed(); | |
| if (sourceAnsweredInRound < sourceRoundId) revert MerchantLogic.IncompletePriceFeedRound(); | |
| // Validate target price feed | |
| ( | |
| uint80 targetRoundId, | |
| int256 targetPrice, | |
| , | |
| uint256 targetUpdatedAt, | |
| uint80 targetAnsweredInRound | |
| ) = AggregatorV3Interface(targetAssetPriceFeed).latestRoundData(); | |
| if (targetPrice <= 0) revert MerchantLogic.InvalidPriceFeed(); | |
| if (targetUpdatedAt == 0 || block.timestamp - targetUpdatedAt > STALENESS_THRESHOLD) revert MerchantLogic.StalePriceFeed(); | |
| if (targetAnsweredInRound < targetRoundId) revert MerchantLogic.IncompletePriceFeedRound(); |
Rationale for this change
What changes are included in this PR?
Are these changes tested?