mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
An adversarial pass over the calldata found three more ways a crafted quote became a signable transaction. A hop address was taken as given. An address is twenty bytes and the path packs it against a uint24 with nothing marking the boundary, so a short one shifted every byte after it — and the path still parsed, as a route through pools nobody named. Width is the only thing separating the two, so both hop addresses are now checked the way the swapper already was. A quote says which chain priced it, and that was ignored. Building this chain's router call from another chain's quote addresses pools by the same numbers on a book that never quoted them, and these chains share deployment addresses, so it is a real trade at a price nobody was shown. Slippage is a percentage of an amount and lives between none of it and all of it. Below zero the factor inverted and the bound grew a minus sign; above a hundred it ran past a word. Clamping the percentage bounds both directions at one place, where the alternative was clamping every product it reaches. IsAddress is exported: whether a chain has an address to address a transaction to is one question, and the process that mounts these handlers has to ask it. Co-authored-by: Hanzo Dev <[email protected]>
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math/big"
|
|
"net/http"
|
|
"strconv"
|
|
)
|
|
|
|
// Whether the router may already spend this token, and the transaction that
|
|
// lets it if not.
|
|
//
|
|
// An ERC-20 moves only on the owner's say-so, so a swap of one is two
|
|
// transactions: grant, then trade. Asking first is what keeps the swap form
|
|
// from showing a second signature prompt to someone who granted it months ago,
|
|
// and from omitting one for someone who never did.
|
|
//
|
|
// Two cases need no approval and both answer null rather than an amount of
|
|
// zero: the chain's own coin, which is sent with the call and never spent from
|
|
// a balance, and an allowance that already covers the trade.
|
|
|
|
// unlimited is what an approval grants: every value a word holds. A person
|
|
// approves a router once and trades against it for as long as they use it; an
|
|
// approval sized to the trade in hand would put a signature in front of every
|
|
// swap after it.
|
|
var unlimited = new(big.Int).Set(maxWord)
|
|
|
|
var (
|
|
selAllowance = selector("allowance(address,address)")
|
|
selApprove = selector("approve(address,uint256)")
|
|
)
|
|
|
|
type approvalRequest struct {
|
|
WalletAddress string `json:"walletAddress"`
|
|
Token string `json:"token"`
|
|
Amount string `json:"amount"`
|
|
ChainID *int64 `json:"chainId"`
|
|
}
|
|
|
|
type approvalResponse struct {
|
|
RequestID string `json:"requestId"`
|
|
Approval *approvalTx `json:"approval"`
|
|
}
|
|
|
|
type approvalTx struct {
|
|
To string `json:"to"`
|
|
From string `json:"from"`
|
|
Data string `json:"data"`
|
|
Value string `json:"value"`
|
|
ChainID int64 `json:"chainId"`
|
|
}
|
|
|
|
// HandleCheckApproval answers whether the router can spend a token on the
|
|
// caller's behalf. router is the same SwapRouter02 HandleSwap addresses — the
|
|
// approval has to name the contract that will actually do the spending, so the
|
|
// two endpoints read one address or the grant is useless.
|
|
func HandleCheckApproval(chainID int64, rpc, router string) http.HandlerFunc {
|
|
n := dial(rpc)
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
|
var req approvalRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
badQuote(w, "body must be JSON")
|
|
return
|
|
}
|
|
|
|
if !IsAddress(req.WalletAddress) {
|
|
badQuote(w, "walletAddress must be an address")
|
|
return
|
|
}
|
|
if req.Token == "" {
|
|
badQuote(w, "token is required")
|
|
return
|
|
}
|
|
amount, ok := parseWei(req.Amount)
|
|
if !ok {
|
|
badQuote(w, "amount must be a decimal wei string")
|
|
return
|
|
}
|
|
if req.ChainID != nil && *req.ChainID != chainID {
|
|
badQuote(w, "unsupported chainId "+strconv.FormatInt(*req.ChainID, 10))
|
|
return
|
|
}
|
|
|
|
wallet := checksumAddress(req.WalletAddress)
|
|
none := approvalResponse{RequestID: requestID()}
|
|
|
|
// The coin itself is not an ERC-20 and has nothing to approve.
|
|
if isNativeAddr(req.Token) {
|
|
writeJSON(w, http.StatusOK, none)
|
|
return
|
|
}
|
|
if !IsAddress(req.Token) {
|
|
badQuote(w, "token must be an address")
|
|
return
|
|
}
|
|
token := checksumAddress(req.Token)
|
|
|
|
res, err := n.call(r.Context(), []ethCall{{
|
|
To: token,
|
|
Data: selAllowance + addrWord(wallet) + addrWord(router),
|
|
}})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{
|
|
"errorCode": "INTERNAL_ERROR",
|
|
"detail": "Internal server error",
|
|
})
|
|
return
|
|
}
|
|
|
|
// A token that answers nothing has granted nothing. Reading silence as
|
|
// an allowance would skip the approval and leave the swap to revert.
|
|
allowance := new(big.Int)
|
|
if got := words(res[0]); len(got) > 0 {
|
|
allowance = got[0]
|
|
}
|
|
if allowance.Cmp(amount) >= 0 {
|
|
writeJSON(w, http.StatusOK, none)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, approvalResponse{
|
|
RequestID: requestID(),
|
|
Approval: &approvalTx{
|
|
To: token,
|
|
From: wallet,
|
|
Data: selApprove + addrWord(router) + numWord(unlimited),
|
|
Value: "0",
|
|
ChainID: chainID,
|
|
},
|
|
})
|
|
}
|
|
}
|