Skip to content

Commit 60b7eb9

Browse files
authored
fix: removed circular dependency, added better error handling, lp-contract related refactoring (#94)
1 parent 13c8014 commit 60b7eb9

File tree

8 files changed

+269
-19
lines changed

8 files changed

+269
-19
lines changed

sources/contracts/core/amm-pool.tact

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ contract AmmPool(
7878
otherVault: self.leftVault,
7979
amount: msg.rightAmount - expectedRightAmount,
8080
receiver: msg.depositor,
81-
// TODO: Maybe we don't need to forward any payload here?
82-
// Or we can even forward both payloads?
8381
payloadToForward: msg.rightAdditionalParams.payloadOnSuccess,
8482
}.toCell(),
8583
});
@@ -99,8 +97,6 @@ contract AmmPool(
9997
to: self.leftVault,
10098
bounce: false,
10199
body: PayoutFromPool {
102-
// TODO: Maybe we don't need to forward any payload here?
103-
// Or we can even forward both payloads?
104100
payloadToForward: msg.leftAdditionalParams.payloadOnSuccess,
105101
otherVault: self.rightVault,
106102
amount: msg.leftAmount - expectedLeftAmount,
@@ -151,6 +147,7 @@ contract AmmPool(
151147
responseDestination: msg.depositor,
152148
// Thanks to this flag, we can send all TONs in notify
153149
sendAllTonsInNotifyFlag: true,
150+
// due to the flag above, we can use 0 here
154151
forwardTonAmount: 0,
155152
forwardPayload,
156153
}.toCell(),
@@ -421,6 +418,9 @@ contract AmmPool(
421418
require(false, "Pool: vaultAddress must be one of the vaults");
422419
return 0;
423420
}
421+
422+
require(inBalance != 0 && outBalance != 0, "Pool: No liquidity in pool");
423+
424424
let amountInWithFee = muldiv(amountIn, 1000 - self.PoolFee, 1000);
425425
let newAmountIn = inBalance + amountInWithFee;
426426
let newAmountOut = muldiv(outBalance, inBalance, newAmountIn);
@@ -447,6 +447,9 @@ contract AmmPool(
447447
require(false, "Pool: vaultOut must be one of the vaults");
448448
return 0;
449449
}
450+
451+
require(inBalance != 0 && outBalance != 0, "Pool: No liquidity in pool");
452+
450453
let newAmountOut = outBalance - amountOut;
451454
if (newAmountOut <= 0) {
452455
require(false, "Pool: Desired amount out is greater than pool reserves");

sources/contracts/core/liquidity-deposit.tact

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright © 2025 TON Studio
33

4-
import "./amm-pool";
54
import "../utils/math";
65
import "./messages";
6+
import "../vaults/vault-interface";
7+
8+
// TODO This is here until Tact will have something like `dataOf Contract()`
9+
struct AmmPoolData {
10+
leftVault: Address;
11+
rightVault: Address; // To be deterministic, rightVault address must be greater than leftVault address
12+
leftSideReserve: Int as coins;
13+
rightSideReserve: Int as coins;
14+
totalSupply: Int as coins;
15+
jettonContent: Cell?;
16+
}
717

818
contract LiquidityDepositContract(
919
leftVault: Address, // To be deterministic, leftVault address must be less than rightVault address
@@ -24,19 +34,43 @@ contract LiquidityDepositContract(
2434
receive(msg: PartHasBeenDeposited) {
2535
let sender = sender();
2636
if (sender == self.leftVault) {
27-
// TODO maybe here we should check that it is not already filled and revert on errors.
28-
require(msg.amount == self.leftSideAmount, "LP Deposit: Amount must be equal to leftSide");
2937
require(msg.depositor == self.depositor, "LP Deposit: Depositor must be the same");
38+
if ((self.status & 1) != 0 || msg.amount != self.leftSideAmount) {
39+
message(MessageParameters {
40+
mode: SendRemainingValue,
41+
body: RejectLiquidityPart {
42+
depositor: msg.depositor,
43+
amountToReturn: msg.amount,
44+
}.toCell(),
45+
value: 0,
46+
to: sender(),
47+
bounce: false,
48+
});
49+
commit();
50+
require(false, "LP Deposit: Left side cannot be filled again or with different amount");
51+
}
3052
self.leftAdditionalParams = msg.additionalParams;
3153
self.status |= 1;
32-
}
33-
if (sender == self.rightVault) {
34-
// TODO maybe here we should check that it is not already filled and revert on errors.
35-
require(msg.amount == self.rightSideAmount, "LP Deposit: Amount must be equal to rightSide");
54+
} else if (sender == self.rightVault) {
3655
require(msg.depositor == self.depositor, "LP Deposit: Depositor must be the same");
56+
if ((self.status & 2) != 0 || msg.amount != self.rightSideAmount) {
57+
message(MessageParameters {
58+
mode: SendRemainingValue,
59+
body: RejectLiquidityPart {
60+
depositor: msg.depositor,
61+
amountToReturn: msg.amount,
62+
}.toCell(),
63+
value: 0,
64+
to: sender(),
65+
bounce: false,
66+
});
67+
commit();
68+
require(false, "LP Deposit: Right side cannot be filled again or with different amount");
69+
}
3770
self.rightAdditionalParams = msg.additionalParams;
3871
self.status |= 2;
3972
}
73+
require(self.leftVault != self.rightVault, "LP Deposit: Vaults must be different");
4074
// Both sides are filled, we can deposit now.
4175
if (self.status == 3) {
4276
// We must check, that this account was deployed with sorted vault, otherwise it could be a security issue
@@ -46,7 +80,17 @@ contract LiquidityDepositContract(
4680
value: 0,
4781
bounce: false, // 1. To pay storage fees of AmmPool. 2. We will destroy this contract, so bounce does not have sense.
4882
mode: SendRemainingBalance + SendDestroyIfZero, // We don't need to keep this contract alive
49-
init: initOf AmmPool(self.leftVault, self.rightVault, 0, 0, 0, null),
83+
init: StateInit {
84+
code: msg.ammPoolCode,
85+
data: AmmPoolData {
86+
leftVault: self.leftVault,
87+
rightVault: self.rightVault,
88+
leftSideReserve: 0,
89+
rightSideReserve: 0,
90+
totalSupply: 0,
91+
jettonContent: null,
92+
}.toCell(),
93+
},
5094
body: LiquidityDeposit {
5195
depositor: self.depositor,
5296
contractId: self.contractId,

sources/contracts/core/messages.tact

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,6 @@ message(0x178d4519) MintViaJettonTransferInternal {
7171
forwardPayload: Slice as remaining;
7272
}
7373

74-
message(0xe7a3475f) PartHasBeenDeposited {
75-
depositor: Address;
76-
amount: Int as coins;
77-
additionalParams: AdditionalParams;
78-
}
79-
8074
struct AdditionalParams {
8175
minAmountToDeposit: Int as coins;
8276
lpTimeout: Int as uint32;

sources/contracts/vaults/jetton-vault.tact

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,27 @@ contract JettonVault(
160160
});
161161
}
162162

163+
override fun handleRejectLP(msg: RejectLiquidityPart) {
164+
let ctx = context();
165+
// Actually ctx.value - priceOfJettonForward() is always >= 0 because we require ctx.value >= priceOfJettonForward() in LPDepositPart
166+
let maxFwdAmount = max(0, ctx.value - priceOfJettonForward(ctx.readForwardFee(), 1));
167+
message(MessageParameters {
168+
mode: SendRemainingValue,
169+
body: SendViaJettonTransfer {
170+
queryId: 0,
171+
amount: msg.amountToReturn,
172+
destination: msg.depositor,
173+
responseDestination: msg.depositor,
174+
customPayload: null,
175+
forwardTonAmount: maxFwdAmount,
176+
forwardPayload: sliceWithOneZeroBit(),
177+
}.toCell(),
178+
value: 0,
179+
to: self.jettonWallet!!,
180+
bounce: true,
181+
});
182+
}
183+
163184
get fun inited(): Bool {
164185
return self.jettonWallet != null;
165186
}
@@ -247,6 +268,7 @@ inline fun actionHandler(msg: JettonNotifyWithActionRequest): Bool {
247268
depositor: msg.sender,
248269
amount: msg.amount,
249270
additionalParams: action.additionalParams,
271+
ammPoolCode: codeOf AmmPool,
250272
}.toCell(),
251273
});
252274
} else {
@@ -279,6 +301,7 @@ inline fun actionHandler(msg: JettonNotifyWithActionRequest): Bool {
279301
depositor: msg.sender,
280302
amount: msg.amount,
281303
additionalParams: action.additionalParams,
304+
ammPoolCode: codeOf AmmPool,
282305
}.toCell(),
283306
});
284307
}

sources/contracts/vaults/ton-vault.tact

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "../core/amm-pool";
88
import "../utils/utils";
99
import "../utils/gas-constants";
1010
import "../core/lp-jettons/constants";
11+
import "../core/liquidity-deposit";
1112

1213
message(0xf8a7ea5) ReturnJettonsViaJettonTransfer {
1314
queryId: Int as uint64;
@@ -145,6 +146,7 @@ contract TonVault(
145146
additionalParams: msg.additionalParams,
146147
amount: msg.amountIn,
147148
depositor: sender(),
149+
ammPoolCode: codeOf AmmPool,
148150
}.toCell(),
149151
});
150152
} else {
@@ -177,11 +179,22 @@ contract TonVault(
177179
depositor: sender(),
178180
amount: msg.amountIn,
179181
additionalParams: msg.additionalParams,
182+
ammPoolCode: codeOf AmmPool,
180183
}.toCell(),
181184
});
182185
}
183186
}
184187

188+
override fun handleRejectLP(msg: RejectLiquidityPart) {
189+
message(MessageParameters {
190+
mode: SendRemainingValue,
191+
body: null,
192+
value: msg.amountToReturn,
193+
to: msg.depositor,
194+
bounce: true,
195+
});
196+
}
197+
185198
receive() {
186199
cashback(sender());
187200
}

sources/contracts/vaults/vault-interface.tact

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,32 @@ message(0x1b434676) AddLiquidityPartTon {
8080
additionalParams: AdditionalParams;
8181
}
8282

83+
message(0xe7a3475f) PartHasBeenDeposited {
84+
depositor: Address;
85+
amount: Int as coins;
86+
additionalParams: AdditionalParams;
87+
// This field is needed to not add a (LP-Deposit -> AmmPool) dependency
88+
ammPoolCode: Cell;
89+
}
90+
91+
// This message is used when PartHasBeenDeposited is rejected
92+
// We don't forward any payload in this message because PartHasBeenDeposited may occur only as the result of incorrect
93+
// user actions.
94+
message(0xe3b122ab) RejectLiquidityPart {
95+
depositor: Address;
96+
amountToReturn: Int as coins;
97+
}
98+
8399
trait VaultInterface {
84100
receive(msg: PayoutFromPool) {
85101
self.handlePayout(msg);
86102
}
87103

88-
abstract fun handlePayout(msg: PayoutFromPool);
104+
abstract inline fun handlePayout(msg: PayoutFromPool);
105+
106+
receive(msg: RejectLiquidityPart) {
107+
self.handleRejectLP(msg);
108+
}
109+
110+
abstract inline fun handleRejectLP(msg: RejectLiquidityPart);
89111
}

sources/tests/amm-pool.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,4 +731,52 @@ describe("Amm pool", () => {
731731
expect(parsedRefundTx.payloadToForward).toEqualCell(payloadOnFailure)
732732
})
733733
})
734+
test("Amm pool get-methods throw, when there are no reserves", async () => {
735+
const blockchain = await Blockchain.create()
736+
737+
const randomDeployer = await blockchain.treasury(randomAddress().toString())
738+
const firstVault = randomAddress()
739+
const secondVault = randomAddress()
740+
const ammPool = blockchain.openContract(
741+
await AmmPool.fromInit(firstVault, secondVault, 0n, 0n, 0n, null),
742+
)
743+
744+
await ammPool.send(randomDeployer.getSender(), {value: toNano(0.01)}, null)
745+
746+
try {
747+
await ammPool.getExpectedOut(firstVault, BigInt(randomInt(0, 100)))
748+
} catch (e) {
749+
if (!(e instanceof GetMethodError)) {
750+
throw e
751+
}
752+
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
753+
}
754+
755+
try {
756+
await ammPool.getExpectedOut(secondVault, BigInt(randomInt(0, 100)))
757+
} catch (e) {
758+
if (!(e instanceof GetMethodError)) {
759+
throw e
760+
}
761+
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
762+
}
763+
764+
try {
765+
await ammPool.getNeededInToGetX(firstVault, BigInt(randomInt(0, 100)))
766+
} catch (e) {
767+
if (!(e instanceof GetMethodError)) {
768+
throw e
769+
}
770+
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
771+
}
772+
773+
try {
774+
await ammPool.getNeededInToGetX(secondVault, BigInt(randomInt(0, 100)))
775+
} catch (e) {
776+
if (!(e instanceof GetMethodError)) {
777+
throw e
778+
}
779+
expect(e.exitCode).toEqual(AmmPool.errors["Pool: No liquidity in pool"])
780+
}
781+
})
734782
})

0 commit comments

Comments
 (0)