Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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: reserves must be non-zero");

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: reserves must be non-zero");

let newAmountOut = outBalance - amountOut;
if (newAmountOut <= 0) {
require(false, "Pool: Desired amount out is greater than pool reserves");
Expand Down
63 changes: 55 additions & 8 deletions sources/contracts/core/liquidity-deposit.tact
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
// 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()`
inline fun buildAmmPoolDataCell(
leftVault: Address,
rightVault: Address, // To be deterministic, rightVault address must be greater than leftVault address
leftSideReserve: Int,
rightSideReserve: Int,
// LP tokens-related field
totalSupply: Int,
jettonContent: Cell?,
): Cell {
return beginCell()
.storeAddress(leftVault)
.storeAddress(rightVault)
.storeCoins(leftSideReserve)
.storeCoins(rightSideReserve)
.storeCoins(totalSupply)
.storeMaybeRef(jettonContent)
.endCell();
}

contract LiquidityDepositContract(
leftVault: Address, // To be deterministic, leftVault address must be less than rightVault address
Expand All @@ -24,19 +44,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 +90,10 @@ 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: buildAmmPoolDataCell(self.leftVault, self.rightVault, 0, 0, 0, null),
},
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);
}
103 changes: 103 additions & 0 deletions sources/tests/liquidity-deposit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {sortAddresses} from "../utils/deployUtils"
// eslint-disable-next-line
import {SendDumpToDevWallet} from "@tondevwallet/traces"
import {ExtendedLPJettonWallet} from "../wrappers/ExtendedLPJettonWallet"
import {randomInt} from "crypto"

describe("Liquidity deposit", () => {
test("Jetton vault should deploy correctly", async () => {
Expand Down Expand Up @@ -561,4 +562,106 @@ describe("Liquidity deposit", () => {
expect(lpBalance).toBeGreaterThan(0n)
},
)
test.each([
{
name: "Jetton->Jetton",
createPool: createJettonAmmPool,
},
{
name: "TON->Jetton",
createPool: createTonJettonAmmPool,
},
])(
"incorrect liquidity provision on same size should revert. Pool is $name",
async ({createPool}) => {
const blockchain = await Blockchain.create()

const {vaultA, liquidityDepositSetup, isSwapped, vaultB} = await createPool(blockchain)

// deploy liquidity deposit contract
const amountA = toNano(1)
const amountB = toNano(2) // 1 a == 2 b ratio

const depositor = vaultB.treasury

const liqSetup = await liquidityDepositSetup(depositor.walletOwner, amountA, amountB)
await liqSetup.deploy()

await vaultA.deploy()

const error =
`LP Deposit: ${isSwapped ? "Right" : "Left"} side cannot be filled again or with different amount` as const
const unsuccessfulLiqProvision = await vaultA.addLiquidity(
liqSetup.liquidityDeposit.address,
BigInt(randomInt(0, 2000000000)), // random amount to trigger error
)

expect(unsuccessfulLiqProvision.transactions).toHaveTransaction({
on: liqSetup.liquidityDeposit.address,
op: LiquidityDepositContract.opcodes.PartHasBeenDeposited,
success: true,
})

expect(await liqSetup.liquidityDeposit.getStatus()).toEqual(0n)

expect(unsuccessfulLiqProvision.transactions).toHaveTransaction({
from: liqSetup.liquidityDeposit.address,
op: LiquidityDepositContract.opcodes.RejectLiquidityPart,
success: true,
})

// add liquidity to vaultA
const firstLiqProvision = await vaultA.addLiquidity(
liqSetup.liquidityDeposit.address,
isSwapped ? amountB : amountA,
)

expect(firstLiqProvision.transactions).toHaveTransaction({
from: vaultA.vault.address,
to: liqSetup.liquidityDeposit.address,
op: LiquidityDepositContract.opcodes.PartHasBeenDeposited,
success: true,
})

const lpState = await liqSetup.liquidityDeposit.getStatus()
expect(lpState).toBeGreaterThan(0n)

// Add the same liquidity again
const secondLiqProvision = await vaultA.addLiquidity(
liqSetup.liquidityDeposit.address,
isSwapped ? amountB : amountA,
)
expect(secondLiqProvision.transactions).toHaveTransaction({
on: liqSetup.liquidityDeposit.address,
success: true, // Commit happened
exitCode: LiquidityDepositContract.errors[error],
})
expect(secondLiqProvision.transactions).toHaveTransaction({
from: liqSetup.liquidityDeposit.address,
op: LiquidityDepositContract.opcodes.RejectLiquidityPart,
success: true,
})
expect(await liqSetup.liquidityDeposit.getStatus()).toEqual(lpState)

// And one more time with an incorrect amount
const thirdLiqProvision = await vaultA.addLiquidity(
liqSetup.liquidityDeposit.address,
isSwapped ? amountB : amountA,
)

expect(thirdLiqProvision.transactions).toHaveTransaction({
on: liqSetup.liquidityDeposit.address,
success: true, // Commit happened
exitCode: LiquidityDepositContract.errors[error],
})

expect(await liqSetup.liquidityDeposit.getStatus()).toEqual(lpState)

expect(thirdLiqProvision.transactions).toHaveTransaction({
from: liqSetup.liquidityDeposit.address,
op: LiquidityDepositContract.opcodes.RejectLiquidityPart,
success: true,
})
},
)
})