Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}