mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
343 lines
9.4 KiB
Go
343 lines
9.4 KiB
Go
package gateway
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
)
|
|
|
|
// Permit2 canonical address (same on all EVM chains)
|
|
const Permit2Address = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
|
|
|
|
// ERC20 function selectors
|
|
var (
|
|
// approve(address,uint256) = 0x095ea7b3
|
|
selectorERC20Approve = [4]byte{0x09, 0x5e, 0xa7, 0xb3}
|
|
|
|
// allowance(address,address) = 0xdd62ed3e
|
|
selectorERC20Allowance = [4]byte{0xdd, 0x62, 0xed, 0x3e}
|
|
)
|
|
|
|
// Permit2 function selectors
|
|
var (
|
|
// approve(address,address,uint160,uint48) = 0x87517c45
|
|
selectorPermit2Approve = [4]byte{0x87, 0x51, 0x7c, 0x45}
|
|
|
|
// allowance(address,address,address) = 0x927da105
|
|
selectorPermit2Allowance = [4]byte{0x92, 0x7d, 0xa1, 0x05}
|
|
)
|
|
|
|
// Gas estimates for approval operations
|
|
const (
|
|
gasERC20Approve uint64 = 50_000
|
|
gasPermit2Approve uint64 = 60_000
|
|
)
|
|
|
|
// MaxUint256 is the maximum approval amount (2^256 - 1)
|
|
var MaxUint256 = func() *big.Int {
|
|
v := new(big.Int).Lsh(big.NewInt(1), 256)
|
|
v.Sub(v, big.NewInt(1))
|
|
return v
|
|
}()
|
|
|
|
// MaxUint160 is the maximum Permit2 allowance amount (2^160 - 1)
|
|
var MaxUint160 = func() *big.Int {
|
|
v := new(big.Int).Lsh(big.NewInt(1), 160)
|
|
v.Sub(v, big.NewInt(1))
|
|
return v
|
|
}()
|
|
|
|
// MaxUint48 is the maximum Permit2 expiration (2^48 - 1)
|
|
var MaxUint48 = func() *big.Int {
|
|
v := new(big.Int).Lsh(big.NewInt(1), 48)
|
|
v.Sub(v, big.NewInt(1))
|
|
return v
|
|
}()
|
|
|
|
// --- Request/Response types ---
|
|
|
|
// ApprovalRequest is the JSON body for approval check/build endpoints.
|
|
type ApprovalRequest struct {
|
|
TokenAddress string `json:"tokenAddress"`
|
|
Owner string `json:"owner"`
|
|
Spender string `json:"spender"`
|
|
Amount string `json:"amount"`
|
|
ChainID uint64 `json:"chainId"`
|
|
}
|
|
|
|
// ApprovalResponse is returned from the approval check endpoint.
|
|
type ApprovalResponse struct {
|
|
Approved bool `json:"approved"`
|
|
Allowance string `json:"allowance"`
|
|
NeedsApproval bool `json:"needsApproval"`
|
|
ApproveTx *UnsignedTxResponse `json:"approveTx,omitempty"`
|
|
}
|
|
|
|
// Permit2Request is the JSON body for Permit2 check/build endpoints.
|
|
type Permit2Request struct {
|
|
TokenAddress string `json:"tokenAddress"`
|
|
Owner string `json:"owner"`
|
|
Spender string `json:"spender"`
|
|
Amount string `json:"amount"`
|
|
Deadline int64 `json:"deadline"`
|
|
ChainID uint64 `json:"chainId"`
|
|
}
|
|
|
|
// Permit2Response is returned from the Permit2 check endpoint.
|
|
type Permit2Response struct {
|
|
Permit2Approved bool `json:"permit2Approved"`
|
|
Permit2Allowance string `json:"permit2Allowance"`
|
|
SignatureRequest *EIP712SignRequest `json:"signatureRequest,omitempty"`
|
|
}
|
|
|
|
// EIP712SignRequest contains the typed data for the user to sign.
|
|
type EIP712SignRequest struct {
|
|
Types map[string][]EIP712Field `json:"types"`
|
|
PrimaryType string `json:"primaryType"`
|
|
Domain EIP712Domain `json:"domain"`
|
|
Message map[string]interface{} `json:"message"`
|
|
}
|
|
|
|
// --- Validation ---
|
|
|
|
func (r *ApprovalRequest) Validate() error {
|
|
if r.TokenAddress == "" {
|
|
return fmt.Errorf("tokenAddress is required")
|
|
}
|
|
if !isValidHexAddress(r.TokenAddress) {
|
|
return fmt.Errorf("invalid tokenAddress")
|
|
}
|
|
if r.Owner == "" {
|
|
return fmt.Errorf("owner is required")
|
|
}
|
|
if !isValidHexAddress(r.Owner) {
|
|
return fmt.Errorf("invalid owner address")
|
|
}
|
|
if r.Spender == "" {
|
|
return fmt.Errorf("spender is required")
|
|
}
|
|
if !isValidHexAddress(r.Spender) {
|
|
return fmt.Errorf("invalid spender address")
|
|
}
|
|
if r.Amount == "" {
|
|
return fmt.Errorf("amount is required")
|
|
}
|
|
amt := parseBigIntStr(r.Amount)
|
|
if amt == nil || amt.Sign() <= 0 {
|
|
return fmt.Errorf("amount must be a positive integer")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Permit2Request) Validate() error {
|
|
if r.TokenAddress == "" {
|
|
return fmt.Errorf("tokenAddress is required")
|
|
}
|
|
if !isValidHexAddress(r.TokenAddress) {
|
|
return fmt.Errorf("invalid tokenAddress")
|
|
}
|
|
if r.Owner == "" {
|
|
return fmt.Errorf("owner is required")
|
|
}
|
|
if !isValidHexAddress(r.Owner) {
|
|
return fmt.Errorf("invalid owner address")
|
|
}
|
|
if r.Spender == "" {
|
|
return fmt.Errorf("spender is required")
|
|
}
|
|
if !isValidHexAddress(r.Spender) {
|
|
return fmt.Errorf("invalid spender address")
|
|
}
|
|
if r.Amount == "" {
|
|
return fmt.Errorf("amount is required")
|
|
}
|
|
amt := parseBigIntStr(r.Amount)
|
|
if amt == nil || amt.Sign() <= 0 {
|
|
return fmt.Errorf("amount must be a positive integer")
|
|
}
|
|
// Permit2 amounts are uint160; reject anything larger
|
|
if amt.Cmp(MaxUint160) > 0 {
|
|
return fmt.Errorf("amount exceeds uint160 max for Permit2")
|
|
}
|
|
if r.Deadline <= 0 {
|
|
return fmt.Errorf("deadline is required")
|
|
}
|
|
// Permit2 expiration is uint48; reject anything larger
|
|
if big.NewInt(r.Deadline).Cmp(MaxUint48) > 0 {
|
|
return fmt.Errorf("deadline exceeds uint48 max for Permit2")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Calldata builders ---
|
|
|
|
// BuildERC20ApproveCalldata encodes ERC20 approve(spender, amount).
|
|
//
|
|
// ABI layout (after 4-byte selector):
|
|
//
|
|
// [0:32] spender (address, left-padded to 32 bytes)
|
|
// [32:64] amount (uint256)
|
|
func BuildERC20ApproveCalldata(spender string, amount *big.Int) []byte {
|
|
data := make([]byte, 68) // 4 selector + 2*32 params
|
|
copy(data[0:4], selectorERC20Approve[:])
|
|
|
|
// spender address
|
|
sb := addrBytes(spender)
|
|
copy(data[4+12:4+32], sb)
|
|
|
|
// amount (uint256)
|
|
ab := amount.Bytes()
|
|
copy(data[36+32-len(ab):36+32], ab)
|
|
|
|
return data
|
|
}
|
|
|
|
// BuildERC20AllowanceCalldata encodes ERC20 allowance(owner, spender).
|
|
func BuildERC20AllowanceCalldata(owner, spender string) []byte {
|
|
data := make([]byte, 68)
|
|
copy(data[0:4], selectorERC20Allowance[:])
|
|
|
|
ob := addrBytes(owner)
|
|
copy(data[4+12:4+32], ob)
|
|
|
|
sb := addrBytes(spender)
|
|
copy(data[36+12:36+32], sb)
|
|
|
|
return data
|
|
}
|
|
|
|
// BuildPermit2ApproveCalldata encodes Permit2 approve(token, spender, amount, expiration).
|
|
//
|
|
// ABI layout (after 4-byte selector):
|
|
//
|
|
// [0:32] token (address)
|
|
// [32:64] spender (address)
|
|
// [64:96] amount (uint160)
|
|
// [96:128] expiration (uint48)
|
|
func BuildPermit2ApproveCalldata(token, spender string, amount *big.Int, expiration int64) []byte {
|
|
data := make([]byte, 132) // 4 + 4*32
|
|
copy(data[0:4], selectorPermit2Approve[:])
|
|
|
|
// token address
|
|
tb := addrBytes(token)
|
|
copy(data[4+12:4+32], tb)
|
|
|
|
// spender address
|
|
sb := addrBytes(spender)
|
|
copy(data[36+12:36+32], sb)
|
|
|
|
// amount (uint160, fits in 20 bytes but padded to 32)
|
|
ab := amount.Bytes()
|
|
copy(data[68+32-len(ab):68+32], ab)
|
|
|
|
// expiration (uint48, fits in 6 bytes but padded to 32)
|
|
exp := big.NewInt(expiration)
|
|
eb := exp.Bytes()
|
|
copy(data[100+32-len(eb):100+32], eb)
|
|
|
|
return data
|
|
}
|
|
|
|
// BuildPermit2AllowanceCalldata encodes Permit2 allowance(owner, token, spender).
|
|
func BuildPermit2AllowanceCalldata(owner, token, spender string) []byte {
|
|
data := make([]byte, 100) // 4 + 3*32
|
|
copy(data[0:4], selectorPermit2Allowance[:])
|
|
|
|
ob := addrBytes(owner)
|
|
copy(data[4+12:4+32], ob)
|
|
|
|
tb := addrBytes(token)
|
|
copy(data[36+12:36+32], tb)
|
|
|
|
sb := addrBytes(spender)
|
|
copy(data[68+12:68+32], sb)
|
|
|
|
return data
|
|
}
|
|
|
|
// --- Approval transaction builders ---
|
|
|
|
// BuildApproveTx builds an unsigned ERC20 approve transaction.
|
|
func BuildApproveTx(req ApprovalRequest) (*UnsignedTxResponse, error) {
|
|
if err := (&req).Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
amount := parseBigIntStr(req.Amount)
|
|
calldata := BuildERC20ApproveCalldata(req.Spender, amount)
|
|
|
|
return &UnsignedTxResponse{
|
|
To: strings.ToLower(req.TokenAddress),
|
|
Data: "0x" + hex.EncodeToString(calldata),
|
|
Value: "0",
|
|
GasLimit: gasERC20Approve,
|
|
ChainID: req.ChainID,
|
|
}, nil
|
|
}
|
|
|
|
// BuildPermit2ApproveTx builds an unsigned Permit2 approve transaction.
|
|
// The user calls this on the Permit2 contract to grant a sub-allowance.
|
|
func BuildPermit2ApproveTx(req Permit2Request) (*UnsignedTxResponse, error) {
|
|
if err := (&req).Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
amount := parseBigIntStr(req.Amount)
|
|
calldata := BuildPermit2ApproveCalldata(req.TokenAddress, req.Spender, amount, req.Deadline)
|
|
|
|
return &UnsignedTxResponse{
|
|
To: strings.ToLower(Permit2Address),
|
|
Data: "0x" + hex.EncodeToString(calldata),
|
|
Value: "0",
|
|
GasLimit: gasPermit2Approve,
|
|
ChainID: req.ChainID,
|
|
}, nil
|
|
}
|
|
|
|
// BuildPermit2SignatureRequest builds the EIP-712 typed data for a Permit2
|
|
// PermitSingle signature. The user signs this off-chain, and the spender
|
|
// submits the permit on-chain as part of the swap flow.
|
|
func BuildPermit2SignatureRequest(req Permit2Request) (*EIP712SignRequest, error) {
|
|
if err := (&req).Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &EIP712SignRequest{
|
|
Types: map[string][]EIP712Field{
|
|
"EIP712Domain": {
|
|
{Name: "name", Type: "string"},
|
|
{Name: "chainId", Type: "uint256"},
|
|
{Name: "verifyingContract", Type: "address"},
|
|
},
|
|
"PermitSingle": {
|
|
{Name: "details", Type: "PermitDetails"},
|
|
{Name: "spender", Type: "address"},
|
|
{Name: "sigDeadline", Type: "uint256"},
|
|
},
|
|
"PermitDetails": {
|
|
{Name: "token", Type: "address"},
|
|
{Name: "amount", Type: "uint160"},
|
|
{Name: "expiration", Type: "uint48"},
|
|
{Name: "nonce", Type: "uint48"},
|
|
},
|
|
},
|
|
PrimaryType: "PermitSingle",
|
|
Domain: EIP712Domain{
|
|
Name: "Permit2",
|
|
ChainID: fmt.Sprintf("%d", req.ChainID),
|
|
VerifyingContract: Permit2Address,
|
|
},
|
|
Message: map[string]interface{}{
|
|
"details": map[string]interface{}{
|
|
"token": strings.ToLower(req.TokenAddress),
|
|
"amount": req.Amount,
|
|
"expiration": fmt.Sprintf("%d", req.Deadline),
|
|
"nonce": "0",
|
|
},
|
|
"spender": strings.ToLower(req.Spender),
|
|
"sigDeadline": fmt.Sprintf("%d", req.Deadline),
|
|
},
|
|
}, nil
|
|
}
|