Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}