fix(amm): apply slippage to exact-output input

This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-17 20:43:36 -03:00
parent a94731881f
commit 0e6a38742d
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
4 changed files with 94 additions and 14 deletions

View File

@ -52,10 +52,16 @@ Rectangle {
readonly property real feeAmount: swapState.feeAmount(parsedSellAmount)
readonly property real minReceivedAmount: swapState.minReceived(parsedBuyAmount, slippageTolerancePercent)
readonly property real maxSentAmount: swapState.maxSent(parsedSellAmount, slippageTolerancePercent)
readonly property real priceImpactPercent: swapState.priceImpactPercent(parsedSellAmount, parsedBuyAmount, sellReserve, buyReserve)
readonly property string swapMode: editingSide === "buy" ? "swap-exact-output" : "swap-exact-input"
readonly property string swapModeText: editingSide === "buy" ? qsTr("Exact output") : qsTr("Exact input")
readonly property bool exactOutput: editingSide === "buy"
readonly property string swapMode: exactOutput ? "swap-exact-output" : "swap-exact-input"
readonly property string swapModeText: exactOutput ? qsTr("Exact output") : qsTr("Exact input")
readonly property string limitLabel: exactOutput ? qsTr("Max sent") : qsTr("Min received")
readonly property string limitText: exactOutput
? swapState.formatTokenAmount(maxSentAmount, sellToken ? sellToken.symbol : "")
: swapState.formatTokenAmount(minReceivedAmount, buyToken ? buyToken.symbol : "")
readonly property bool hasAmount: editingSide === "sell" ? parsedSellInput > 0 : parsedBuyInput > 0
readonly property bool tokensSelected: sellToken !== null && buyToken !== null
@ -102,7 +108,8 @@ Rectangle {
"buyToken": buyToken ? buyToken.symbol : "",
"sellAmount": formatAmountValue(parsedSellAmount),
"buyAmount": formatAmountValue(parsedBuyAmount),
"minReceived": formatAmountValue(minReceivedAmount),
"minReceived": exactOutput ? "" : formatAmountValue(minReceivedAmount),
"maxSent": exactOutput ? formatAmountValue(maxSentAmount) : "",
"feeAmount": swapState.formatTokenAmount(feeAmount, sellToken ? sellToken.symbol : ""),
"priceImpactPercent": swapState.formatPercent(priceImpactPercent),
"priceImpactPercentValue": priceImpactPercent,
@ -211,7 +218,8 @@ Rectangle {
feeText: swapState.formatTokenAmount(root.feeAmount, root.sellToken ? root.sellToken.symbol : "")
priceImpactText: swapState.formatPercent(root.priceImpactPercent)
priceImpactPercent: root.priceImpactPercent
minReceivedText: swapState.formatTokenAmount(root.minReceivedAmount, root.buyToken ? root.buyToken.symbol : "")
limitLabel: root.limitLabel
limitText: root.limitText
}
SlippageToleranceControl {

View File

@ -22,16 +22,22 @@ ColumnLayout {
spacing: 4
Text {
objectName: "swapPayLabel"
Layout.fillWidth: true
text: qsTr("You pay")
text: root.snapshot.swapMode === "swap-exact-output"
? qsTr("You pay at most")
: qsTr("You pay")
color: root.theme.colors.textSecondary
font.pixelSize: 12
}
Text {
objectName: "swapPayAmount"
Layout.fillWidth: true
text: qsTr("%1 %2")
.arg(root.snapshot.sellAmount || "")
.arg(root.snapshot.swapMode === "swap-exact-output"
? root.snapshot.maxSent || ""
: root.snapshot.sellAmount || "")
.arg(root.snapshot.sellToken || "")
color: root.theme.colors.textPrimary
font.bold: true
@ -54,16 +60,22 @@ ColumnLayout {
spacing: 4
Text {
objectName: "swapReceiveLabel"
Layout.fillWidth: true
text: qsTr("You receive at least")
text: root.snapshot.swapMode === "swap-exact-output"
? qsTr("You receive")
: qsTr("You receive at least")
color: root.theme.colors.textSecondary
font.pixelSize: 12
}
Text {
objectName: "swapReceiveAmount"
Layout.fillWidth: true
text: qsTr("%1 %2")
.arg(root.snapshot.minReceived || "")
.arg(root.snapshot.swapMode === "swap-exact-output"
? root.snapshot.buyAmount || ""
: root.snapshot.minReceived || "")
.arg(root.snapshot.buyToken || "")
color: root.theme.colors.textPrimary
font.bold: true
@ -81,8 +93,11 @@ ColumnLayout {
priceImpactText: root.snapshot.priceImpactPercent || ""
priceImpactPercent: Number(root.snapshot.priceImpactPercentValue) || 0
slippageText: root.snapshot.slippageTolerance || ""
minReceivedText: qsTr("%1 %2")
.arg(root.snapshot.minReceived || "")
.arg(root.snapshot.buyToken || "")
limitLabel: root.snapshot.swapMode === "swap-exact-output"
? qsTr("Max sent")
: qsTr("Min received")
limitText: root.snapshot.swapMode === "swap-exact-output"
? qsTr("%1 %2").arg(root.snapshot.maxSent || "").arg(root.snapshot.sellToken || "")
: qsTr("%1 %2").arg(root.snapshot.minReceived || "").arg(root.snapshot.buyToken || "")
}
}

View File

@ -10,7 +10,8 @@ Item {
property string priceImpactText: ""
property real priceImpactPercent: 0
property string slippageText: ""
property string minReceivedText: ""
property string limitLabel: qsTr("Min received")
property string limitText: ""
readonly property color priceImpactColor: {
if (root.priceImpactPercent > 5) return "#F08A76";
@ -126,20 +127,22 @@ Item {
Layout.fillWidth: true
Text {
objectName: "swapLimitLabel"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: root.theme.colors.textSecondary
font.pixelSize: 12
text: qsTr("Min received")
text: root.limitLabel
}
Text {
objectName: "swapLimitValue"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: root.theme.colors.textPrimary
font.bold: true
font.pixelSize: 12
text: root.minReceivedText
text: root.limitText
}
}
}

View File

@ -53,5 +53,59 @@ Item {
tryCompare(dialog, "opened", false)
compare(card.sellInput, "1")
}
function test_exactOutputUsesMaximumInputLimit() {
const page = createTemporaryObject(pageComponent, root)
verify(page)
const card = findChild(page, "swapCard")
const dialog = findChild(page, "swapPreviewDialog")
verify(card)
verify(dialog)
card.setToken("sell", page.tokens[0])
card.setToken("buy", page.tokens[1])
card.slippageTolerancePercent = 10
card.buyInput = "1000"
card.editingSide = "buy"
tryCompare(card, "canSubmit", true)
const snapshot = card.buildSnapshot()
compare(snapshot.swapMode, "swap-exact-output")
compare(snapshot.buyAmount, "1000.00")
compare(snapshot.minReceived, "")
verify(snapshot.maxSent.length > 0)
verify(Number(snapshot.maxSent) > Number(snapshot.sellAmount))
dialog.openWithSnapshot(snapshot)
tryCompare(dialog, "opened", true)
tryCompare(findChild(dialog, "swapPayLabel"), "text", "You pay at most")
compare(findChild(dialog, "swapPayAmount").text, snapshot.maxSent + " " + snapshot.sellToken)
compare(findChild(dialog, "swapReceiveLabel").text, "You receive")
compare(findChild(dialog, "swapReceiveAmount").text, snapshot.buyAmount + " " + snapshot.buyToken)
compare(findChild(dialog, "swapLimitLabel").text, "Max sent")
compare(findChild(dialog, "swapLimitValue").text, snapshot.maxSent + " " + snapshot.sellToken)
}
function test_exactInputUsesMinimumOutputLimit() {
const page = createTemporaryObject(pageComponent, root)
verify(page)
const card = findChild(page, "swapCard")
verify(card)
card.setToken("sell", page.tokens[0])
card.setToken("buy", page.tokens[1])
card.slippageTolerancePercent = 10
card.sellInput = "1"
card.editingSide = "sell"
tryCompare(card, "canSubmit", true)
const snapshot = card.buildSnapshot()
compare(snapshot.swapMode, "swap-exact-input")
compare(snapshot.sellAmount, "1.00")
compare(snapshot.maxSent, "")
verify(snapshot.minReceived.length > 0)
verify(Number(snapshot.minReceived) < Number(snapshot.buyAmount))
}
}
}