mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
222 lines
8.0 KiB
Go
222 lines
8.0 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package stableswap
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAddress(t *testing.T) {
|
|
expected := "0x0400000000000000000000000000000000000060"
|
|
require.Equal(t, expected, ContractAddress.Hex())
|
|
}
|
|
|
|
// encodeGetD builds input for OpGetD: [op] [n:4] [amp:32] [balances:n*32]
|
|
func encodeGetD(n int, amp *big.Int, balances []*big.Int) []byte {
|
|
buf := make([]byte, 1+4+32+n*32)
|
|
buf[0] = OpGetD
|
|
binary.BigEndian.PutUint32(buf[1:5], uint32(n))
|
|
copy(buf[5:37], padTo32(amp.Bytes()))
|
|
for i, b := range balances {
|
|
copy(buf[37+i*32:37+(i+1)*32], padTo32(b.Bytes()))
|
|
}
|
|
return buf
|
|
}
|
|
|
|
func TestGetD_Balanced(t *testing.T) {
|
|
// Two tokens, both with 1e18, A=100
|
|
// D should equal sum of balances for perfectly balanced pool
|
|
bal := big.NewInt(1e18)
|
|
input := encodeGetD(2, big.NewInt(100), []*big.Int{bal, bal})
|
|
|
|
result, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
d := new(big.Int).SetBytes(result)
|
|
expectedD := new(big.Int).Add(bal, bal)
|
|
// D should be very close to sum (within rounding)
|
|
diff := new(big.Int).Sub(d, expectedD)
|
|
diff.Abs(diff)
|
|
require.True(t, diff.Cmp(big.NewInt(2)) <= 0, "D should be ~2e18, got %s (diff %s)", d, diff)
|
|
}
|
|
|
|
func TestGetD_Unbalanced(t *testing.T) {
|
|
// Unbalanced pool: 1e18 and 2e18, A=100
|
|
b0 := big.NewInt(1e18)
|
|
b1 := new(big.Int).Mul(big.NewInt(2), big.NewInt(1e18))
|
|
input := encodeGetD(2, big.NewInt(100), []*big.Int{b0, b1})
|
|
|
|
result, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
d := new(big.Int).SetBytes(result)
|
|
sum := new(big.Int).Add(b0, b1)
|
|
// D should be close to but slightly less than the sum for unbalanced pools
|
|
require.True(t, d.Cmp(sum) <= 0, "D should be <= sum for unbalanced pool")
|
|
require.True(t, d.Sign() > 0, "D should be positive")
|
|
}
|
|
|
|
func TestGetDy_Swap(t *testing.T) {
|
|
// Swap 0.1e18 of token 0 for token 1 in a balanced 2-token pool
|
|
bal := big.NewInt(1e18)
|
|
dx := big.NewInt(1e17) // 0.1e18
|
|
|
|
// Build GetDy input: [op] [i:4] [j:4] [dx:32] [n:4] [amp:32] [bal0:32] [bal1:32]
|
|
n := 2
|
|
input := make([]byte, 1+4+4+32+4+32+n*32)
|
|
input[0] = OpGetDy
|
|
binary.BigEndian.PutUint32(input[1:5], 0)
|
|
binary.BigEndian.PutUint32(input[5:9], 1)
|
|
copy(input[9:41], padTo32(dx.Bytes()))
|
|
binary.BigEndian.PutUint32(input[41:45], uint32(n))
|
|
copy(input[45:77], padTo32(big.NewInt(100).Bytes()))
|
|
copy(input[77:109], padTo32(bal.Bytes()))
|
|
copy(input[109:141], padTo32(bal.Bytes()))
|
|
|
|
result, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
dy := new(big.Int).SetBytes(result)
|
|
// For a balanced StableSwap pool with A=100, swapping 0.1e18 should yield close to 0.1e18
|
|
require.True(t, dy.Cmp(big.NewInt(0)) > 0, "dy should be positive")
|
|
require.True(t, dy.Cmp(dx) <= 0, "dy should be <= dx for stable pool")
|
|
// Should be within 1% for a well-amplified stable pool
|
|
minDy := new(big.Int).Mul(dx, big.NewInt(99))
|
|
minDy.Div(minDy, big.NewInt(100))
|
|
require.True(t, dy.Cmp(minDy) >= 0, "dy should be within 1%% of dx, got %s", dy)
|
|
}
|
|
|
|
func TestGetDy_InvalidIndex(t *testing.T) {
|
|
bal := big.NewInt(1e18)
|
|
n := 2
|
|
input := make([]byte, 1+4+4+32+4+32+n*32)
|
|
input[0] = OpGetDy
|
|
binary.BigEndian.PutUint32(input[1:5], 0)
|
|
binary.BigEndian.PutUint32(input[5:9], 0) // i == j -> invalid
|
|
copy(input[9:41], padTo32(big.NewInt(1e17).Bytes()))
|
|
binary.BigEndian.PutUint32(input[41:45], uint32(n))
|
|
copy(input[45:77], padTo32(big.NewInt(100).Bytes()))
|
|
copy(input[77:109], padTo32(bal.Bytes()))
|
|
copy(input[109:141], padTo32(bal.Bytes()))
|
|
|
|
_, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 100000, false)
|
|
require.ErrorIs(t, err, ErrInvalidIndex)
|
|
}
|
|
|
|
func TestAddLiquidity_Initial(t *testing.T) {
|
|
// First deposit into empty pool
|
|
n := 2
|
|
amt := big.NewInt(1e18)
|
|
// [op] [n:4] [amp:32] [totalSupply:32] [amounts:n*32] [balances:n*32]
|
|
input := make([]byte, 1+4+32+32+2*n*32)
|
|
input[0] = OpAddLiquidity
|
|
binary.BigEndian.PutUint32(input[1:5], uint32(n))
|
|
copy(input[5:37], padTo32(big.NewInt(100).Bytes()))
|
|
// totalSupply = 0 (initial deposit)
|
|
copy(input[69:101], padTo32(amt.Bytes()))
|
|
copy(input[101:133], padTo32(amt.Bytes()))
|
|
// balances = 0 (empty pool)
|
|
|
|
result, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
|
|
lpMinted := new(big.Int).SetBytes(result)
|
|
// D1 for balanced pool with 1e18 each should be ~2e18
|
|
expectedD := new(big.Int).Mul(big.NewInt(2), big.NewInt(1e18))
|
|
diff := new(big.Int).Sub(lpMinted, expectedD)
|
|
diff.Abs(diff)
|
|
require.True(t, diff.Cmp(big.NewInt(2)) <= 0, "initial LP should be ~2e18, got %s", lpMinted)
|
|
}
|
|
|
|
func TestRemoveLiquidity(t *testing.T) {
|
|
n := 2
|
|
bal := big.NewInt(1e18)
|
|
totalSupply := new(big.Int).Mul(big.NewInt(2), big.NewInt(1e18))
|
|
lpAmount := big.NewInt(1e18) // remove half
|
|
|
|
// [op] [n:4] [lpAmount:32] [totalSupply:32] [balances:n*32]
|
|
input := make([]byte, 1+4+32+32+n*32)
|
|
input[0] = OpRemoveLiquidity
|
|
binary.BigEndian.PutUint32(input[1:5], uint32(n))
|
|
copy(input[5:37], padTo32(lpAmount.Bytes()))
|
|
copy(input[37:69], padTo32(totalSupply.Bytes()))
|
|
copy(input[69:101], padTo32(bal.Bytes()))
|
|
copy(input[101:133], padTo32(bal.Bytes()))
|
|
|
|
result, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 100000, false)
|
|
require.NoError(t, err)
|
|
require.Len(t, result, 64)
|
|
|
|
amt0 := new(big.Int).SetBytes(result[:32])
|
|
amt1 := new(big.Int).SetBytes(result[32:64])
|
|
// Removing half LP from balanced pool should return half of each balance
|
|
expected := new(big.Int).Div(bal, big.NewInt(2))
|
|
require.Equal(t, 0, expected.Cmp(amt0), "expected %s, got %s", expected, amt0)
|
|
require.Equal(t, 0, expected.Cmp(amt1), "expected %s, got %s", expected, amt1)
|
|
}
|
|
|
|
func TestInvariantPreserved(t *testing.T) {
|
|
// D before and after swap should be equal (minus rounding)
|
|
bal := big.NewInt(1e18)
|
|
amp := big.NewInt(100)
|
|
n := 2
|
|
|
|
// Get D before
|
|
inputD := encodeGetD(n, amp, []*big.Int{bal, bal})
|
|
resultD, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, inputD, 100000, false)
|
|
require.NoError(t, err)
|
|
dBefore := new(big.Int).SetBytes(resultD)
|
|
|
|
// Simulate swap: add 0.1e18 to token0
|
|
dx := big.NewInt(1e17)
|
|
inputSwap := make([]byte, 1+4+4+32+4+32+n*32)
|
|
inputSwap[0] = OpGetDy
|
|
binary.BigEndian.PutUint32(inputSwap[1:5], 0)
|
|
binary.BigEndian.PutUint32(inputSwap[5:9], 1)
|
|
copy(inputSwap[9:41], padTo32(dx.Bytes()))
|
|
binary.BigEndian.PutUint32(inputSwap[41:45], uint32(n))
|
|
copy(inputSwap[45:77], padTo32(amp.Bytes()))
|
|
copy(inputSwap[77:109], padTo32(bal.Bytes()))
|
|
copy(inputSwap[109:141], padTo32(bal.Bytes()))
|
|
|
|
resultSwap, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, inputSwap, 100000, false)
|
|
require.NoError(t, err)
|
|
dy := new(big.Int).SetBytes(resultSwap)
|
|
|
|
// New balances: bal+dx, bal-dy
|
|
newBal0 := new(big.Int).Add(bal, dx)
|
|
newBal1 := new(big.Int).Sub(bal, dy)
|
|
|
|
// Get D after
|
|
inputDAfter := encodeGetD(n, amp, []*big.Int{newBal0, newBal1})
|
|
resultDAfter, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, inputDAfter, 100000, false)
|
|
require.NoError(t, err)
|
|
dAfter := new(big.Int).SetBytes(resultDAfter)
|
|
|
|
// D should be preserved (within rounding tolerance)
|
|
diff := new(big.Int).Sub(dBefore, dAfter)
|
|
diff.Abs(diff)
|
|
require.True(t, diff.Cmp(big.NewInt(10)) <= 0, "invariant D should be preserved: before=%s, after=%s, diff=%s", dBefore, dAfter, diff)
|
|
}
|
|
|
|
func TestGasInsufficient(t *testing.T) {
|
|
input := encodeGetD(2, big.NewInt(100), []*big.Int{big.NewInt(1e18), big.NewInt(1e18)})
|
|
_, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, input, 1, false)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInvalidInput(t *testing.T) {
|
|
// Empty
|
|
_, _, err := Precompile.Run(nil, ContractAddress, ContractAddress, nil, 100000, false)
|
|
require.ErrorIs(t, err, ErrInvalidInput)
|
|
|
|
// Unknown op
|
|
_, _, err = Precompile.Run(nil, ContractAddress, ContractAddress, []byte{0xFF}, 100000, false)
|
|
require.ErrorIs(t, err, ErrUnknownOp)
|
|
}
|