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