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