transfer.gno
10.94 Kb · 334 lines
1package v1
2
3import (
4 "chain/runtime"
5
6 "gno.land/p/nt/ufmt"
7
8 "gno.land/r/gnoswap/common"
9 pl "gno.land/r/gnoswap/pool"
10
11 i256 "gno.land/p/gnoswap/int256"
12 u256 "gno.land/p/gnoswap/uint256"
13)
14
15// safeTransfer performs a token transfer out of the pool while ensuring
16// the pool has sufficient balance and updating internal accounting.
17// This function is typically used during swaps and liquidity removals.
18//
19// Important requirements:
20// - The amount must be a positive value representing tokens to transfer out
21// - The pool must have sufficient balance for the transfer
22// - The transfer amount must fit within int64 range
23//
24// Parameters:
25// - p: the pool instance
26// - to: destination address for the transfer
27// - tokenPath: path identifier of the token to transfer
28// - amount: amount to transfer (positive u256.Uint value)
29// - isToken0: true if transferring token0, false for token1
30//
31// The function will:
32// 1. Check pool has sufficient balance
33// 2. Execute the transfer
34// 3. Subtract the amount from pool's internal balance
35//
36// Panics if any validation fails or if the transfer fails
37func (i *poolV1) safeTransfer(
38 p *pl.Pool,
39 to address,
40 tokenPath string,
41 amount *u256.Uint,
42 isToken0 bool,
43) {
44 token0 := p.BalanceToken0()
45 token1 := p.BalanceToken1()
46
47 if err := validatePoolBalance(token0, token1, amount, isToken0); err != nil {
48 panic(err)
49 }
50 amountInt64 := safeConvertToInt64(amount)
51
52 common.SafeGRC20Transfer(cross, tokenPath, to, amountInt64)
53
54 newBalance, err := updatePoolBalance(token0, token1, amount, isToken0)
55 if err != nil {
56 panic(err)
57 }
58
59 poolBalances := p.Balances()
60
61 if isToken0 {
62 poolBalances.SetToken0(newBalance)
63 } else {
64 poolBalances.SetToken1(newBalance)
65 }
66
67 p.SetBalances(poolBalances)
68}
69
70// safeTransferFrom securely transfers tokens into the pool while ensuring balance consistency.
71//
72// This function performs the following steps:
73// 1. Validates and converts the transfer amount to `int64` using `safeConvertToInt64`.
74// 2. Executes the token transfer using `TransferFrom` via the token teller contract.
75// 3. Verifies that the destination balance reflects the correct amount after transfer.
76// 4. Updates the pool's internal balances (`token0` or `token1`) and validates the updated state.
77//
78// Parameters:
79// - p (*pl.Pool): The pool instance to transfer tokens into.
80// - from (address): Source address for the token transfer.
81// - to (address): Destination address, typically the pool address.
82// - tokenPath (string): Path identifier for the token being transferred.
83// - amount (*u256.Uint): The amount of tokens to transfer (must be a positive value).
84// - isToken0 (bool): A flag indicating whether the token being transferred is token0 (`true`) or token1 (`false`).
85//
86// Panics:
87// - If the `amount` exceeds the int64 range during conversion.
88// - If the token transfer (`TransferFrom`) fails.
89// - If the destination balance after the transfer does not match the expected amount.
90// - If the pool's internal balances (`token0` or `token1`) overflow or become inconsistent.
91//
92// Notes:
93// - The function assumes that the sender (`from`) has approved the pool to spend the specified tokens.
94// - The balance consistency check ensures that no tokens are lost or double-counted during the transfer.
95// - Pool balance updates are performed atomically to ensure internal consistency.
96func (i *poolV1) safeTransferFrom(
97 p *pl.Pool,
98 from, to address,
99 tokenPath string,
100 amount *u256.Uint,
101 isToken0 bool,
102) {
103 amountInt64 := safeConvertToInt64(amount)
104
105 token := common.GetToken(tokenPath)
106 beforeBalance := token.BalanceOf(to)
107
108 common.SafeGRC20TransferFrom(cross, tokenPath, from, to, amountInt64)
109
110 afterBalance := token.BalanceOf(to)
111 if (beforeBalance + amountInt64) != afterBalance {
112 panic(ufmt.Sprintf(
113 "%v. beforeBalance(%d) + amount(%d) != afterBalance(%d)",
114 errTransferFailed, beforeBalance, amountInt64, afterBalance,
115 ))
116 }
117
118 poolBalances := p.Balances()
119
120 // update pool balances
121 if isToken0 {
122 beforeToken0 := poolBalances.Token0().Clone()
123 poolBalances.SetToken0(u256.Zero().Add(poolBalances.Token0(), amount))
124 if poolBalances.Token0().Lt(beforeToken0) {
125 panic(ufmt.Sprintf(
126 "%v. token0(%s) < beforeToken0(%s)",
127 errBalanceUpdateFailed, poolBalances.Token0().ToString(), beforeToken0.ToString(),
128 ))
129 }
130 } else {
131 beforeToken1 := poolBalances.Token1().Clone()
132 poolBalances.SetToken1(u256.Zero().Add(poolBalances.Token1(), amount))
133 if poolBalances.Token1().Lt(beforeToken1) {
134 panic(ufmt.Sprintf(
135 "%v. token1(%s) < beforeToken1(%s)",
136 errBalanceUpdateFailed, poolBalances.Token1().ToString(), beforeToken1.ToString(),
137 ))
138 }
139 }
140
141 p.SetBalances(poolBalances)
142}
143
144// safeSwapCallback executes a swap callback and validates the token payment.
145//
146// This function implements the flash swap pattern where the pool first sends output tokens
147// to the recipient, then invokes the callback to receive input tokens from the caller.
148// The callback is responsible for transferring the required input tokens to the pool.
149//
150// Callback Signature:
151//
152// func(cur realm, amount0Delta, amount1Delta int64, _ *pool.CallbackMarker) error
153//
154// Delta Convention (following Uniswap V3 standard):
155// - Positive delta: Amount the pool must RECEIVE (input token)
156// - Negative delta: Amount the pool has SENT (output token)
157//
158// For zeroForOne swaps (token0 → token1):
159// - amount0Delta > 0: Pool receives token0 (input)
160// - amount1Delta < 0: Pool sends token1 (output)
161//
162// For oneForZero swaps (token1 → token0):
163// - amount0Delta < 0: Pool sends token0 (output)
164// - amount1Delta > 0: Pool receives token1 (input)
165//
166// Parameters:
167// - p: The pool instance
168// - tokenPath: Path of the input token that pool must receive
169// - amountInInt256: Amount pool must receive (positive i256.Int)
170// - amountOutInt256: Amount pool has sent (negative i256.Int)
171// - zeroForOne: Swap direction (true = token0 to token1)
172// - swapCallback: Callback function that must transfer input tokens to pool
173//
174// The callback needed:
175// 1. Verify the caller is the legitimate pool contract address to prevent unauthorized invocations
176// 2. Transfer at least `amountIn` of input tokens to the pool
177// 3. Return nil on success, or an error to revert the swap
178//
179// Example callback implementation:
180//
181// func(cur realm, amount0Delta, amount1Delta int64, _ *pool.CallbackMarker) error {
182// // Security check: ensure this callback is invoked by the legitimate pool
183// caller := runtime.PreviousRealm().Address()
184// poolAddr := chain.PackageAddress("gno.land/r/gnoswap/pool")
185//
186// if caller != poolAddr {
187// panic("unauthorized caller")
188// }
189//
190// if amount0Delta > 0 {
191// // Transfer token0 to pool
192// token0.Transfer(cross, poolAddr, amount0Delta)
193// }
194// if amount1Delta > 0 {
195// // Transfer token1 to pool
196// token1.Transfer(cross, poolAddr, amount1Delta)
197// }
198// return nil
199// }
200func (i *poolV1) safeSwapCallback(
201 p *pl.Pool,
202 tokenPath string,
203 amountInInt256 *i256.Int,
204 amountOutInt256 *i256.Int,
205 zeroForOne bool,
206 swapCallback func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error,
207) {
208 if swapCallback == nil {
209 panic(makeErrorWithDetails(
210 errInvalidInput,
211 "swapCallback is nil",
212 ))
213 }
214
215 currentAddress := runtime.CurrentRealm().Address()
216
217 amountIn := amountInInt256.Int64()
218 amountOut := amountOutInt256.Int64()
219 balanceBefore := common.BalanceOf(tokenPath, currentAddress)
220
221 // Make callback to the calling contract
222 // The contract should transfer tokens to pool within this callback
223 // Following Uniswap V3 convention:
224 // - Positive delta: amount the pool must receive (input token)
225 // - Negative delta: amount the pool has sent (output token)
226 var amount0Delta, amount1Delta, beforeTokenBalance int64
227
228 if zeroForOne {
229 // zeroForOne: pool receives token0 (positive), sends token1 (negative)
230 amount0Delta = amountIn
231 amount1Delta = amountOut
232 beforeTokenBalance = p.BalanceToken0().Int64()
233 } else {
234 // !zeroForOne: pool receives token1 (positive), sends token0 (negative)
235 amount0Delta = amountOut
236 amount1Delta = amountIn
237 beforeTokenBalance = p.BalanceToken1().Int64()
238 }
239
240 // execute swap callback
241 // CallbackMarker is created by the pool module to enforce type-system level validation.
242 err := swapCallback(cross, amount0Delta, amount1Delta, &pl.CallbackMarker{})
243 if err != nil {
244 panic(err)
245 }
246
247 balanceAfter := common.BalanceOf(tokenPath, currentAddress)
248 balanceIncrease := safeSubInt64(balanceAfter, balanceBefore)
249
250 // check insufficient payment by swap callback
251 if balanceIncrease < amountIn {
252 panic(makeErrorWithDetails(
253 errInsufficientPayment,
254 ufmt.Sprintf("insufficient payment: expected %d, received %d", amountIn, balanceIncrease),
255 ))
256 }
257
258 // check overflow update pool balance
259 resultTokenBalance := safeAddInt64(beforeTokenBalance, amountIn)
260 resultTokenBalanceUint := u256.NewUintFromInt64(resultTokenBalance)
261
262 poolBalances := p.Balances()
263
264 if zeroForOne {
265 poolBalances.SetToken0(resultTokenBalanceUint)
266 } else {
267 poolBalances.SetToken1(resultTokenBalanceUint)
268 }
269
270 p.SetBalances(poolBalances)
271}
272
273// validatePoolBalance checks if the pool has sufficient balance of either token0 and token1
274// before proceeding with a transfer. This prevents the pool won't go into a negative balance.
275func validatePoolBalance(token0, token1, amount *u256.Uint, isToken0 bool) error {
276 if token0 == nil || token1 == nil || amount == nil {
277 return ufmt.Errorf(
278 "%v. token0(%s) or token1(%s) or amount(%s) is nil",
279 errTransferFailed, token0.ToString(), token1.ToString(), amount.ToString(),
280 )
281 }
282
283 if isToken0 {
284 if token0.Lt(amount) {
285 return ufmt.Errorf(
286 "%v. token0(%s) >= amount(%s)",
287 errTransferFailed, token0.ToString(), amount.ToString(),
288 )
289 }
290 return nil
291 }
292 if token1.Lt(amount) {
293 return ufmt.Errorf(
294 "%v. token1(%s) >= amount(%s)",
295 errTransferFailed, token1.ToString(), amount.ToString(),
296 )
297 }
298 return nil
299}
300
301// updatePoolBalance calculates the new balance after a transfer and validate.
302// It ensures the resulting balance won't be negative or overflow.
303func updatePoolBalance(
304 token0, token1, amount *u256.Uint,
305 isToken0 bool,
306) (*u256.Uint, error) {
307 var overflow bool
308 var newBalance *u256.Uint
309
310 if isToken0 {
311 newBalance, overflow = u256.Zero().SubOverflow(token0, amount)
312 if isBalanceOverflowOrNegative(overflow, newBalance) {
313 return nil, ufmt.Errorf(
314 "%v. cannot decrease, token0(%s) - amount(%s)",
315 errBalanceUpdateFailed, token0.ToString(), amount.ToString(),
316 )
317 }
318 return newBalance, nil
319 }
320
321 newBalance, overflow = u256.Zero().SubOverflow(token1, amount)
322 if isBalanceOverflowOrNegative(overflow, newBalance) {
323 return nil, ufmt.Errorf(
324 "%v. cannot decrease, token1(%s) - amount(%s)",
325 errBalanceUpdateFailed, token1.ToString(), amount.ToString(),
326 )
327 }
328 return newBalance, nil
329}
330
331// isBalanceOverflowOrNegative checks if the balance calculation resulted in an overflow or negative value.
332func isBalanceOverflowOrNegative(overflow bool, newBalance *u256.Uint) bool {
333 return overflow || newBalance.Lt(zero)
334}