Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions sources/contracts/core/amm-pool.tact
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ contract AmmPool(
otherVault: self.leftVault,
amount: msg.rightAmount - expectedRightAmount,
receiver: msg.depositor,
// TODO: Maybe we don't need to forward any payload here?
// Or we can even forward both payloads?
payloadToForward: msg.rightAdditionalParams.payloadOnSuccess,
}.toCell(),
});
Expand All @@ -99,8 +97,6 @@ contract AmmPool(
to: self.leftVault,
bounce: false,
body: PayoutFromPool {
// TODO: Maybe we don't need to forward any payload here?
// Or we can even forward both payloads?
payloadToForward: msg.leftAdditionalParams.payloadOnSuccess,
otherVault: self.rightVault,
amount: msg.leftAmount - expectedLeftAmount,
Expand Down Expand Up @@ -151,6 +147,7 @@ contract AmmPool(
responseDestination: msg.depositor,
// Thanks to this flag, we can send all TONs in notify
sendAllTonsInNotifyFlag: true,
// due to the flag above, we can use 0 here
forwardTonAmount: 0,
forwardPayload,
}.toCell(),
Expand Down Expand Up @@ -421,6 +418,9 @@ contract AmmPool(
require(false, "Pool: vaultAddress must be one of the vaults");
return 0;
}

require(inBalance != 0 && outBalance != 0, "Pool: No liquidity in pool");

let amountInWithFee = muldiv(amountIn, 1000 - self.PoolFee, 1000);
let newAmountIn = inBalance + amountInWithFee;
let newAmountOut = muldiv(outBalance, inBalance, newAmountIn);
Expand All @@ -447,6 +447,9 @@ contract AmmPool(
require(false, "Pool: vaultOut must be one of the vaults");
return 0;
}

require(inBalance != 0 && outBalance != 0, "Pool: No liquidity in pool");

let newAmountOut = outBalance - amountOut;
if (newAmountOut <= 0) {
require(false, "Pool: Desired amount out is greater than pool reserves");
Expand Down
60 changes: 52 additions & 8 deletions sources/contracts/core/liquidity-deposit.tact
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// SPDX-License-Identifier: MIT
// Copyright © 2025 TON Studio

import "./amm-pool";
import "../utils/math";
import "./messages";
import "../vaults/vault-interface";

// TODO This is here until Tact will have something like `dataOf Contract()`
struct AmmPoolData {
leftVault: Address;
rightVault: Address; // To be deterministic, rightVault address must be greater than leftVault address
leftSideReserve: Int as coins;
rightSideReserve: Int as coins;
totalSupply: Int as coins;
jettonContent: Cell?;
}

contract LiquidityDepositContract(
leftVault: Address, // To be deterministic, leftVault address must be less than rightVault address
Expand All @@ -24,19 +34,43 @@ contract LiquidityDepositContract(
receive(msg: PartHasBeenDeposited) {
let sender = sender();
if (sender == self.leftVault) {
// TODO maybe here we should check that it is not already filled and revert on errors.
require(msg.amount == self.leftSideAmount, "LP Deposit: Amount must be equal to leftSide");
require(msg.depositor == self.depositor, "LP Deposit: Depositor must be the same");
if ((self.status & 1) != 0 || msg.amount != self.leftSideAmount) {
message(MessageParameters {
mode: SendRemainingValue,
body: RejectLiquidityPart {
depositor: msg.depositor,
amountToReturn: msg.amount,
}.toCell(),
value: 0,
to: sender(),
bounce: false,
});
commit();
require(false, "LP Deposit: Left side cannot be filled again or with different amount");
}
self.leftAdditionalParams = msg.additionalParams;
self.status |= 1;
}
if (sender == self.rightVault) {
// TODO maybe here we should check that it is not already filled and revert on errors.
require(msg.amount == self.rightSideAmount, "LP Deposit: Amount must be equal to rightSide");
} else if (sender == self.rightVault) {
require(msg.depositor == self.depositor, "LP Deposit: Depositor must be the same");
if ((self.status & 2) != 0 || msg.amount != self.rightSideAmount) {
message(MessageParameters {
mode: SendRemainingValue,
body: RejectLiquidityPart {
depositor: msg.depositor,
amountToReturn: msg.amount,
}.toCell(),
value: 0,
to: sender(),
bounce: false,
});
commit();
require(false, "LP Deposit: Right side cannot be filled again or with different amount");
}
self.rightAdditionalParams = msg.additionalParams;
self.status |= 2;
}
require(self.leftVault != self.rightVault, "LP Deposit: Vaults must be different");
// Both sides are filled, we can deposit now.
if (self.status == 3) {
// We must check, that this account was deployed with sorted vault, otherwise it could be a security issue
Expand All @@ -46,7 +80,17 @@ contract LiquidityDepositContract(
value: 0,
bounce: false, // 1. To pay storage fees of AmmPool. 2. We will destroy this contract, so bounce does not have sense.
mode: SendRemainingBalance + SendDestroyIfZero, // We don't need to keep this contract alive
init: initOf AmmPool(self.leftVault, self.rightVault, 0, 0, 0, null),
init: StateInit {
code: msg.ammPoolCode,
data: AmmPoolData {
leftVault: self.leftVault,
rightVault: self.rightVault,
leftSideReserve: 0,
rightSideReserve: 0,
totalSupply: 0,
jettonContent: null,
}.toCell(),
},
body: LiquidityDeposit {
depositor: self.depositor,
contractId: self.contractId,
Expand Down
6 changes: 0 additions & 6 deletions sources/contracts/core/messages.tact
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,6 @@ message(0x178d4519) MintViaJettonTransferInternal {
forwardPayload: Slice as remaining;
}

message(0xe7a3475f) PartHasBeenDeposited {
depositor: Address;
amount: Int as coins;
additionalParams: AdditionalParams;
}

struct AdditionalParams {
minAmountToDeposit: Int as coins;
lpTimeout: Int as uint32;
Expand Down
23 changes: 23 additions & 0 deletions sources/contracts/vaults/jetton-vault.tact
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,27 @@ contract JettonVault(
});
}

override fun handleRejectLP(msg: RejectLiquidityPart) {
let ctx = context();
// Actually ctx.value - priceOfJettonForward() is always >= 0 because we require ctx.value >= priceOfJettonForward() in LPDepositPart
let maxFwdAmount = max(0, ctx.value - priceOfJettonForward(ctx.readForwardFee(), 1));
message(MessageParameters {
mode: SendRemainingValue,
body: SendViaJettonTransfer {
queryId: 0,
amount: msg.amountToReturn,
destination: msg.depositor,
responseDestination: msg.depositor,
customPayload: null,
forwardTonAmount: maxFwdAmount,
forwardPayload: sliceWithOneZeroBit(),
}.toCell(),
value: 0,
to: self.jettonWallet!!,
bounce: true,
});
}

get fun inited(): Bool {
return self.jettonWallet != null;
}
Expand Down Expand Up @@ -247,6 +268,7 @@ inline fun actionHandler(msg: JettonNotifyWithActionRequest): Bool {
depositor: msg.sender,
amount: msg.amount,
additionalParams: action.additionalParams,
ammPoolCode: codeOf AmmPool,
}.toCell(),
});
} else {
Expand Down Expand Up @@ -279,6 +301,7 @@ inline fun actionHandler(msg: JettonNotifyWithActionRequest): Bool {
depositor: msg.sender,
amount: msg.amount,
additionalParams: action.additionalParams,
ammPoolCode: codeOf AmmPool,
}.toCell(),
});
}
Expand Down
13 changes: 13 additions & 0 deletions sources/contracts/vaults/ton-vault.tact
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "../core/amm-pool";
import "../utils/utils";
import "../utils/gas-constants";
import "../core/lp-jettons/constants";
import "../core/liquidity-deposit";

message(0xf8a7ea5) ReturnJettonsViaJettonTransfer {
queryId: Int as uint64;
Expand Down Expand Up @@ -145,6 +146,7 @@ contract TonVault(
additionalParams: msg.additionalParams,
amount: msg.amountIn,
depositor: sender(),
ammPoolCode: codeOf AmmPool,
}.toCell(),
});
} else {
Expand Down Expand Up @@ -177,11 +179,22 @@ contract TonVault(
depositor: sender(),
amount: msg.amountIn,
additionalParams: msg.additionalParams,
ammPoolCode: codeOf AmmPool,
}.toCell(),
});
}
}

override fun handleRejectLP(msg: RejectLiquidityPart) {
message(MessageParameters {
mode: SendRemainingValue,
body: null,
value: msg.amountToReturn,
to: msg.depositor,
bounce: true,
});
}

receive() {
cashback(sender());
}
Expand Down
24 changes: 23 additions & 1 deletion sources/contracts/vaults/vault-interface.tact
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,32 @@ message(0x1b434676) AddLiquidityPartTon {
additionalParams: AdditionalParams;
}

message(0xe7a3475f) PartHasBeenDeposited {
depositor: Address;
amount: Int as coins;
additionalParams: AdditionalParams;
// This field is needed to not add a (LP-Deposit -> AmmPool) dependency
ammPoolCode: Cell;
}

// This message is used when PartHasBeenDeposited is rejected
// We don't forward any payload in this message because PartHasBeenDeposited may occur only as the result of incorrect
// user actions.
message(0xe3b122ab) RejectLiquidityPart {
depositor: Address;
amountToReturn: Int as coins;
}

trait VaultInterface {
receive(msg: PayoutFromPool) {
self.handlePayout(msg);
}

abstract fun handlePayout(msg: PayoutFromPool);
abstract inline fun handlePayout(msg: PayoutFromPool);

receive(msg: RejectLiquidityPart) {
self.handleRejectLP(msg);
}

abstract inline fun handleRejectLP(msg: RejectLiquidityPart);
}
48 changes: 48 additions & 0 deletions sources/tests/amm-pool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,4 +731,52 @@ describe("Amm pool", () => {
expect(parsedRefundTx.payloadToForward).toEqualCell(payloadOnFailure)
})
})
test("Amm pool get-methods throw, when there are no reserves", async () => {
const blockchain = await Blockchain.create()

const randomDeployer = await blockchain.treasury(randomAddress().toString())
const firstVault = randomAddress()
const secondVault = randomAddress()
const ammPool = blockchain.openContract(
await AmmPool.fromInit(firstVault, secondVault, 0n, 0n, 0n, null),
)

await ammPool.send(randomDeployer.getSender(), {value: toNano(0.01)}, null)

try {
await ammPool.getExpectedOut(firstVault, BigInt(randomInt(0, 100)))
} catch (e) {
if (!(e instanceof GetMethodError)) {
throw e
}
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
}

try {
await ammPool.getExpectedOut(secondVault, BigInt(randomInt(0, 100)))
} catch (e) {
if (!(e instanceof GetMethodError)) {
throw e
}
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
}

try {
await ammPool.getNeededInToGetX(firstVault, BigInt(randomInt(0, 100)))
} catch (e) {
if (!(e instanceof GetMethodError)) {
throw e
}
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
}

try {
await ammPool.getNeededInToGetX(secondVault, BigInt(randomInt(0, 100)))
} catch (e) {
if (!(e instanceof GetMethodError)) {
throw e
}
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
}
})
})
Loading