From b09aade2086200a37d38ac875843eab3ff2591e7 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:07:12 +0200 Subject: [PATCH 01/18] feat: bump openapi version --- codegen/openapi/aggregation-openapi.json | 130 ++++++++++-------- .../aggregation/aggregation_types.gen.go | 17 ++- 2 files changed, 86 insertions(+), 61 deletions(-) diff --git a/codegen/openapi/aggregation-openapi.json b/codegen/openapi/aggregation-openapi.json index 8edf3821..6890eba2 100644 --- a/codegen/openapi/aggregation-openapi.json +++ b/codegen/openapi/aggregation-openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "paths": { - "/v5.2/1/quote": { + "/v6.0/1/quote": { "get": { "operationId": "getQuote", "summary": "Find the best quote to swap with 1inch Router", @@ -12,8 +12,7 @@ "in": "query", "example": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -22,8 +21,7 @@ "in": "query", "example": "0x111111111117dc0aa78b770fa6a738034120c302", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -32,8 +30,7 @@ "in": "query", "example": "10000000000000000", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -152,6 +149,16 @@ "type": "string", "x-go-type-skip-optional-pointer": true } + }, + { + "name": "excludedProtocols", + "required": false, + "in": "query", + "description": "excluded supported liquidity sources", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": true + } } ], "responses": { @@ -181,7 +188,7 @@ ] } }, - "/v5.2/1/swap": { + "/v6.0/1/swap": { "get": { "operationId": "getSwap", "summary": "Generate calldata to swap on 1inch Router", @@ -192,8 +199,7 @@ "in": "query", "example": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -202,8 +208,7 @@ "in": "query", "example": "0x111111111117dc0aa78b770fa6a738034120c302", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -212,8 +217,7 @@ "in": "query", "example": "10000000000000000", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -222,8 +226,16 @@ "in": "query", "description": "The address that calls the 1inch contract", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" + } + }, + { + "name": "origin", + "required": true, + "in": "query", + "description": "An EOA address that initiate the transaction", + "schema": { + "type": "string" } }, { @@ -235,8 +247,7 @@ "schema": { "minimum": 0, "maximum": 50, - "type": "number", - "x-go-type-skip-optional-pointer": true + "type": "number" } }, { @@ -356,6 +367,16 @@ "x-go-type-skip-optional-pointer": true } }, + { + "name": "excludedProtocols", + "required": false, + "in": "query", + "description": "excluded supported liquidity sources", + "schema": { + "type": "string", + "x-go-type-skip-optional-pointer": true + } + }, { "name": "permit", "required": false, @@ -404,6 +425,16 @@ "type": "boolean", "x-go-type-skip-optional-pointer": true } + }, + { + "name": "usePermit2", + "required": false, + "in": "query", + "description": "Enable this flag in case you did an approval to permit2 smart contract", + "schema": { + "type": "boolean", + "x-go-type-skip-optional-pointer": true + } } ], "responses": { @@ -433,7 +464,7 @@ ] } }, - "/v5.2/1/approve/spender": { + "/v6.0/1/approve/spender": { "get": { "operationId": "getSpender", "summary": "Address of the 1inch Router that is trusted to spend funds for the swap", @@ -455,7 +486,7 @@ ] } }, - "/v5.2/1/approve/transaction": { + "/v6.0/1/approve/transaction": { "get": { "operationId": "getApprove", "summary": "Generate approve calldata to allow 1inch Router to perform a swap", @@ -467,8 +498,7 @@ "example": "0x111111111117dc0aa78b770fa6a738034120c302", "description": "Token address you want to swap", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -500,7 +530,7 @@ ] } }, - "/v5.2/1/approve/allowance": { + "/v6.0/1/approve/allowance": { "get": { "operationId": "getAllowance", "summary": "Get the number of tokens that the 1inch Router is allowed to swap", @@ -512,8 +542,7 @@ "example": "0x111111111117dc0aa78b770fa6a738034120c302", "description": "Token address you want to swap", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } }, { @@ -522,8 +551,7 @@ "in": "query", "description": "Wallet address for which you want to check", "schema": { - "type": "string", - "x-go-type-skip-optional-pointer": true + "type": "string" } } ], @@ -544,7 +572,7 @@ ] } }, - "/v5.2/1/liquidity-sources": { + "/v6.0/1/liquidity-sources": { "get": { "operationId": "getLiquiditySources", "summary": "List of liquidity sources that are available for routing in the 1inch Aggregation Protocol", @@ -566,7 +594,7 @@ ] } }, - "/v5.2/1/tokens": { + "/v6.0/1/tokens": { "get": { "operationId": "getTokens", "summary": "List of tokens that are available for swap in the 1inch Aggregation protocol", @@ -577,7 +605,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TokensResponse" + "type": "object", + "properties": { + "tokens": { + "type": "object", + "properties": { + "0x111111111117dc0aa78b770fa6a738034120c302": { + "$ref": "#/components/schemas/TokenInfo" + } + } + } + } } } } @@ -592,28 +630,13 @@ "info": { "title": "1inch Swap API", "description": "\n

Ethereum Network

\nUsing 1inch Swap API, you can find the best route to exchange assets and make the exchange.\n

\nStep by step:\n1. Lookup addresses of tokens you want to swap, for example ‘0xxx’ , ‘0xxxx’ for DAI -> 1INCH\n2. Check for allowance of 1inch router contract to spend source asset (/approve/allowance)\n3. If necessary, give approval for 1inch router to spend source token (/approve/transaction)\n4. Monitor the best exchange route using (/quote)\n5. When you ready use to perform swap (/swap)", - "version": "5.2", + "version": "6.0", "contact": {} }, "tags": [], "servers": [], "components": { "schemas": { - "TokensResponse": { - "type": "object", - "properties": { - "tokens": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/TokenInfo" - }, - "x-go-type-skip-optional-pointer": true - } - }, - "required": [ - "tokens" - ] - }, "TokenInfo": { "type": "object", "properties": { @@ -679,12 +702,10 @@ "type": "object", "properties": { "fromToken": { - "$ref": "#/components/schemas/TokenInfo", - "x-go-type-skip-optional-pointer": true + "$ref": "#/components/schemas/TokenInfo" }, "toToken": { - "$ref": "#/components/schemas/TokenInfo", - "x-go-type-skip-optional-pointer": true + "$ref": "#/components/schemas/TokenInfo" }, "toAmount": { "type": "string", @@ -829,12 +850,10 @@ "type": "object", "properties": { "fromToken": { - "$ref": "#/components/schemas/TokenInfo", - "x-go-type-skip-optional-pointer": true + "$ref": "#/components/schemas/TokenInfo" }, "toToken": { - "$ref": "#/components/schemas/TokenInfo", - "x-go-type-skip-optional-pointer": true + "$ref": "#/components/schemas/TokenInfo" }, "toAmount": { "type": "string", @@ -857,8 +876,7 @@ "x-go-type-skip-optional-pointer": true }, "tx": { - "$ref": "#/components/schemas/TransactionData", - "x-go-type-skip-optional-pointer": true + "$ref": "#/components/schemas/TransactionData" } }, "required": [ diff --git a/sdk-clients/aggregation/aggregation_types.gen.go b/sdk-clients/aggregation/aggregation_types.gen.go index 7e8d40b2..36b1192b 100644 --- a/sdk-clients/aggregation/aggregation_types.gen.go +++ b/sdk-clients/aggregation/aggregation_types.gen.go @@ -167,11 +167,6 @@ type TokenInfo struct { Tags []string `json:"tags,omitempty"` } -// TokensResponse defines model for TokensResponse. -type TokensResponse struct { - Tokens map[string]TokenInfo `json:"tokens"` -} - // TransactionData defines model for TransactionData. type TransactionData struct { Data string `json:"data"` @@ -228,6 +223,9 @@ type GetQuoteParams struct { // IncludeGas Return approximated gas in response IncludeGas bool `url:"includeGas,omitempty" json:"includeGas,omitempty"` ConnectorTokens string `url:"connectorTokens,omitempty" json:"connectorTokens,omitempty"` + + // ExcludedProtocols excluded supported liquidity sources + ExcludedProtocols string `url:"excludedProtocols,omitempty" json:"excludedProtocols,omitempty"` } // GetSwapParams defines parameters for GetSwap. @@ -239,6 +237,9 @@ type GetSwapParams struct { // From The address that calls the 1inch contract From string `url:"from" json:"from"` + // Origin An EOA address that initiate the transaction + Origin string `url:"origin" json:"origin"` + // Slippage min: 0; max: 50 Slippage float32 `url:"slippage" json:"slippage"` @@ -265,6 +266,9 @@ type GetSwapParams struct { IncludeGas bool `url:"includeGas,omitempty" json:"includeGas,omitempty"` ConnectorTokens string `url:"connectorTokens,omitempty" json:"connectorTokens,omitempty"` + // ExcludedProtocols excluded supported liquidity sources + ExcludedProtocols string `url:"excludedProtocols,omitempty" json:"excludedProtocols,omitempty"` + // Permit https://eips.ethereum.org/EIPS/eip-2612 Permit string `url:"permit,omitempty" json:"permit,omitempty"` @@ -277,4 +281,7 @@ type GetSwapParams struct { // DisableEstimate Enable this flag to disable onchain simulation DisableEstimate bool `url:"disableEstimate,omitempty" json:"disableEstimate,omitempty"` + + // UsePermit2 Enable this flag in case you did an approval to permit2 smart contract + UsePermit2 bool `url:"usePermit2,omitempty" json:"usePermit2,omitempty"` } From 0eb2b09cebfa0ff016b28aba7005059d0d47900a Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:07:42 +0200 Subject: [PATCH 02/18] feat: add approve call data generation for wallet pkg --- common/wallet.go | 4 +++- internal/web3-provider/approve.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 internal/web3-provider/approve.go diff --git a/common/wallet.go b/common/wallet.go index 47bd438c..78abe468 100644 --- a/common/wallet.go +++ b/common/wallet.go @@ -25,7 +25,9 @@ type Wallet interface { GetContractDetailsForPermit(ctx context.Context, token gethCommon.Address, spender gethCommon.Address, amount *big.Int, deadline int64) (*ContractPermitData, error) GetContractDetailsForPermitDaiLike(ctx context.Context, token gethCommon.Address, spender gethCommon.Address, deadline int64) (*ContractPermitDataDaiLike, error) TokenPermit(cd ContractPermitData) (string, error) - TokenPermitDaiLike(cd ContractPermitDataDaiLike) (string, error) + TokenPermitDaiLike(cd ContractPermitDataDaiLike) (string, error + + GenerateApproveCallData(addressTo string, amount uint64) (string, error) IsEIP1559Applicable() bool ChainId() int64 diff --git a/internal/web3-provider/approve.go b/internal/web3-provider/approve.go new file mode 100644 index 00000000..de658585 --- /dev/null +++ b/internal/web3-provider/approve.go @@ -0,0 +1,19 @@ +package web3_provider + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +func (w Wallet) GenerateApproveCallData(addressTo string, amount uint64) (string, error) { + spenderAddress := common.HexToAddress(addressTo) + + callData, err := w.erc20ABI.Pack("approve", spenderAddress, big.NewInt(int64(amount))) + if err != nil { + return "", fmt.Errorf("failed to pack ABI data: %v", err) + } + + return common.Bytes2Hex(callData), nil +} From 424020626ae05d2f72f51fa3b60f6ca8f8b29b42 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:08:08 +0200 Subject: [PATCH 03/18] fix(example): wrong network fix --- sdk-clients/aggregation/examples/swap/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-clients/aggregation/examples/swap/main.go b/sdk-clients/aggregation/examples/swap/main.go index c76d9d57..1038dd31 100644 --- a/sdk-clients/aggregation/examples/swap/main.go +++ b/sdk-clients/aggregation/examples/swap/main.go @@ -27,7 +27,7 @@ func main() { config, err := aggregation.NewConfiguration(aggregation.ConfigurationParams{ NodeUrl: nodeUrl, PrivateKey: privateKey, - ChainId: constants.EthereumChainId, + ChainId: constants.PolygonChainId, ApiUrl: "https://api.1inch.dev", ApiKey: devPortalToken, }) From 2267fe3bb2ec13303e09cac07bbedbe59c30bc39 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:08:24 +0200 Subject: [PATCH 04/18] feat(example): permit2 template --- .../examples/swap-with-permit2/main.go | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 sdk-clients/aggregation/examples/swap-with-permit2/main.go diff --git a/sdk-clients/aggregation/examples/swap-with-permit2/main.go b/sdk-clients/aggregation/examples/swap-with-permit2/main.go new file mode 100644 index 00000000..f6647705 --- /dev/null +++ b/sdk-clients/aggregation/examples/swap-with-permit2/main.go @@ -0,0 +1,136 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/big" + "os" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/1inch/1inch-sdk-go/constants" + "github.com/1inch/1inch-sdk-go/sdk-clients/aggregation" +) + +/* +This example demonstrates how to swap tokens on the PolygonChainId network using the 1inch SDK. +The only thing you need to provide is your wallet address, wallet key, and dev portal token. +This can be done through your environment, or you can directly set them in the variables below +*/ + +var ( + privateKey = os.Getenv("WALLET_KEY") + nodeUrl = os.Getenv("NODE_URL") + devPortalToken = os.Getenv("DEV_PORTAL_TOKEN") +) + +const ( + PolygonFRAX = "0x45c32fa6df82ead1e2ef74d17b76547eddfaff89" + PolygonWeth = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619" +) + +func main() { + config, err := aggregation.NewConfiguration(aggregation.ConfigurationParams{ + NodeUrl: nodeUrl, + PrivateKey: privateKey, + ChainId: constants.PolygonChainId, + ApiUrl: "https://api.1inch.dev", + ApiKey: devPortalToken, + }) + if err != nil { + log.Fatalf("Failed to create configuration: %v\n", err) + } + client, err := aggregation.NewClient(config) + if err != nil { + log.Fatalf("Failed to create client: %v\n", err) + } + ctx := context.Background() + + amountToSwap := big.NewInt(1e17) + + allowanceData, err := client.GetApproveAllowance(ctx, aggregation.GetAllowanceParams{ + TokenAddress: PolygonFRAX, + WalletAddress: client.Wallet.Address().Hex(), + }) + if err != nil { + log.Fatalf("Failed to get allowance: %v\n", err) + } + + allowance := new(big.Int) + allowance.SetString(allowanceData.Allowance, 10) + + cmp := amountToSwap.Cmp(allowance) + + var permit string + + if cmp > 0 { + spender, err := client.GetApproveSpender(ctx) + if err != nil { + panic(err) + } + now := time.Now() + twoDaysLater := now.Add(time.Hour * 24 * 2) + + permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(PolygonFRAX), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) + if err != nil { + log.Fatalf("Failed to get permit data: %v\n", err) + } + + permit, err = client.Wallet.TokenPermit(*permitData) + if err != nil { + log.Fatalf("Failed to sign permit: %v\n", err) + } + } + + swapParams := aggregation.GetSwapParams{ + Src: PolygonFRAX, + Dst: PolygonWeth, + Amount: amountToSwap.String(), + From: client.Wallet.Address().Hex(), + Slippage: 1, + } + if permit != "" { + swapParams.Permit = permit + } + swapData, err := client.GetSwap(ctx, swapParams) + if err != nil { + log.Fatalf("Failed to get swap data: %v\n", err) + } + + builder := client.TxBuilder.New() + + tx, err := builder.SetData(swapData.TxNormalized.Data).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) + if err != nil { + log.Fatalf("Failed to build transaction: %v\n", err) + } + signedTx, err := client.Wallet.Sign(tx) + if err != nil { + log.Fatalf("Failed to sign transaction: %v\n", err) + } + + err = client.Wallet.BroadcastTransaction(ctx, signedTx) + if err != nil { + log.Fatalf("Failed to broadcast transaction: %v\n", err) + } + + // Waiting for transaction, just an example of it + fmt.Printf("Transaction has been broadcast. View it on Polygonscan here: %v\n", fmt.Sprintf("https://polygonscan.com/tx/%v", signedTx.Hash().Hex())) + for { + receipt, err := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) + if receipt != nil { + fmt.Println("Transaction complete!") + return + } + if err != nil { + fmt.Println("Waiting for transaction to be mined") + } + select { + case <-time.After(1000 * time.Millisecond): // check again after a delay + case <-ctx.Done(): + fmt.Println("Context cancelled") + return + } + } +} From ba98544a8772bc45436df2f512c8c9edf663f1e8 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:10:06 +0200 Subject: [PATCH 05/18] fix(wallet): interface typing --- common/wallet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/wallet.go b/common/wallet.go index 78abe468..7137cdcd 100644 --- a/common/wallet.go +++ b/common/wallet.go @@ -25,7 +25,7 @@ type Wallet interface { GetContractDetailsForPermit(ctx context.Context, token gethCommon.Address, spender gethCommon.Address, amount *big.Int, deadline int64) (*ContractPermitData, error) GetContractDetailsForPermitDaiLike(ctx context.Context, token gethCommon.Address, spender gethCommon.Address, deadline int64) (*ContractPermitDataDaiLike, error) TokenPermit(cd ContractPermitData) (string, error) - TokenPermitDaiLike(cd ContractPermitDataDaiLike) (string, error + TokenPermitDaiLike(cd ContractPermitDataDaiLike) (string, error) GenerateApproveCallData(addressTo string, amount uint64) (string, error) From 2d16762fe240f426ea15c9ff62165037d1b8821b Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Mon, 17 Jun 2024 13:29:02 +0200 Subject: [PATCH 06/18] chore: example logic --- common/wallet.go | 2 ++ internal/web3-provider/approve.go | 30 +++++++++++++++++ .../examples/swap-with-permit2/main.go | 33 ++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/common/wallet.go b/common/wallet.go index 7137cdcd..0257a64f 100644 --- a/common/wallet.go +++ b/common/wallet.go @@ -29,6 +29,8 @@ type Wallet interface { GenerateApproveCallData(addressTo string, amount uint64) (string, error) + TokenAllowance(ctx context.Context, tokenAddress string, spenderAddress string) (*big.Int, error) + IsEIP1559Applicable() bool ChainId() int64 //TokenApprove() diff --git a/internal/web3-provider/approve.go b/internal/web3-provider/approve.go index de658585..8e3815b2 100644 --- a/internal/web3-provider/approve.go +++ b/internal/web3-provider/approve.go @@ -1,9 +1,11 @@ package web3_provider import ( + "context" "fmt" "math/big" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" ) @@ -17,3 +19,31 @@ func (w Wallet) GenerateApproveCallData(addressTo string, amount uint64) (string return common.Bytes2Hex(callData), nil } + +func (w Wallet) TokenAllowance(ctx context.Context, tokenAddress string, spenderAddress string) (*big.Int, error) { + token := common.HexToAddress(tokenAddress) + spender := common.HexToAddress(spenderAddress) + + callData, err := w.erc20ABI.Pack("allowance", w.address, spender) + if err != nil { + return nil, fmt.Errorf("failed to pack ABI data: %v", err) + } + + msg := ethereum.CallMsg{ + To: &token, + Data: callData, + } + + result, err := w.ethClient.CallContract(ctx, msg, nil) + if err != nil { + return nil, fmt.Errorf("failed to call contract: %v", err) + } + + var allowance *big.Int + err = w.erc20ABI.UnpackIntoInterface(&allowance, "allowance", result) + if err != nil { + return nil, fmt.Errorf("failed to unpack result: %v", err) + } + + return allowance, nil +} diff --git a/sdk-clients/aggregation/examples/swap-with-permit2/main.go b/sdk-clients/aggregation/examples/swap-with-permit2/main.go index f6647705..d370c56a 100644 --- a/sdk-clients/aggregation/examples/swap-with-permit2/main.go +++ b/sdk-clients/aggregation/examples/swap-with-permit2/main.go @@ -31,6 +31,10 @@ const ( PolygonWeth = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619" ) +const ( + UniswapPermit2Polygon = "0x000000000022D473030F116dDEE9F6B43aC78BA3" +) + func main() { config, err := aggregation.NewConfiguration(aggregation.ConfigurationParams{ NodeUrl: nodeUrl, @@ -65,6 +69,33 @@ func main() { var permit string + perimit2allowance, err := client.Wallet.TokenAllowance(ctx, PolygonFRAX, UniswapPermit2Polygon) + if err != nil { + log.Fatalf("Failed to get allowance: %v\n", err) + } + + cmp2 := allowance.Cmp(perimit2allowance) + if cmp2 > 0 { + approveData, err := client.Wallet.GenerateApproveCallData(UniswapPermit2Polygon, amountToSwap.Uint64()) + if err != nil { + log.Fatalf("Failed to generate call data: %v\n", err) + } + builder := client.TxBuilder.New() + + tx, err := builder.SetData(approveData).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) + if err != nil { + log.Fatalf("Failed to build transaction: %v\n", err) + } + signedTx, err := client.Wallet.Sign(tx) + if err != nil { + log.Fatalf("Failed to sign transaction: %v\n", err) + } + + err = client.Wallet.BroadcastTransaction(ctx, signedTx) + if err != nil { + log.Fatalf("Failed to broadcast transaction: %v\n", err) + } + } if cmp > 0 { spender, err := client.GetApproveSpender(ctx) if err != nil { @@ -73,7 +104,7 @@ func main() { now := time.Now() twoDaysLater := now.Add(time.Hour * 24 * 2) - permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(PolygonFRAX), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) + permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(UniswapPermit2Polygon), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) if err != nil { log.Fatalf("Failed to get permit data: %v\n", err) } From cd7395875b2f2fc13a62d7775715229d8555f682 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:25:39 +0200 Subject: [PATCH 07/18] chore: fixes and debug --- common/wallet.go | 6 - internal/web3-provider/approve.go | 2 +- .../aggregation/aggregation_types.gen.go | 5 + .../examples/swap-with-permit2/main.go | 160 +++++++++--------- 4 files changed, 90 insertions(+), 83 deletions(-) diff --git a/common/wallet.go b/common/wallet.go index 0257a64f..b110714d 100644 --- a/common/wallet.go +++ b/common/wallet.go @@ -33,12 +33,6 @@ type Wallet interface { IsEIP1559Applicable() bool ChainId() int64 - //TokenApprove() - - // view functions - //TokenBalance() - //TokenAllowance() - } type ContractPermitData struct { diff --git a/internal/web3-provider/approve.go b/internal/web3-provider/approve.go index 8e3815b2..f6f68638 100644 --- a/internal/web3-provider/approve.go +++ b/internal/web3-provider/approve.go @@ -17,7 +17,7 @@ func (w Wallet) GenerateApproveCallData(addressTo string, amount uint64) (string return "", fmt.Errorf("failed to pack ABI data: %v", err) } - return common.Bytes2Hex(callData), nil + return "0x" + common.Bytes2Hex(callData), nil } func (w Wallet) TokenAllowance(ctx context.Context, tokenAddress string, spenderAddress string) (*big.Int, error) { diff --git a/sdk-clients/aggregation/aggregation_types.gen.go b/sdk-clients/aggregation/aggregation_types.gen.go index 36b1192b..600dd59c 100644 --- a/sdk-clients/aggregation/aggregation_types.gen.go +++ b/sdk-clients/aggregation/aggregation_types.gen.go @@ -167,6 +167,11 @@ type TokenInfo struct { Tags []string `json:"tags,omitempty"` } +// TokensResponse defines model for TokensResponse. +type TokensResponse struct { + Tokens map[string]TokenInfo `json:"tokens"` +} + // TransactionData defines model for TransactionData. type TransactionData struct { Data string `json:"data"` diff --git a/sdk-clients/aggregation/examples/swap-with-permit2/main.go b/sdk-clients/aggregation/examples/swap-with-permit2/main.go index d370c56a..68fbf05f 100644 --- a/sdk-clients/aggregation/examples/swap-with-permit2/main.go +++ b/sdk-clients/aggregation/examples/swap-with-permit2/main.go @@ -2,11 +2,10 @@ package main import ( "context" - "fmt" + "encoding/hex" "log" "math/big" "os" - "time" "github.com/ethereum/go-ethereum/common" @@ -65,26 +64,34 @@ func main() { allowance := new(big.Int) allowance.SetString(allowanceData.Allowance, 10) - cmp := amountToSwap.Cmp(allowance) - - var permit string + //cmp := amountToSwap.Cmp(allowance) + // + //var permit string perimit2allowance, err := client.Wallet.TokenAllowance(ctx, PolygonFRAX, UniswapPermit2Polygon) if err != nil { log.Fatalf("Failed to get allowance: %v\n", err) } - cmp2 := allowance.Cmp(perimit2allowance) + cmp2 := amountToSwap.Cmp(perimit2allowance) if cmp2 > 0 { - approveData, err := client.Wallet.GenerateApproveCallData(UniswapPermit2Polygon, amountToSwap.Uint64()) + approveDataString, err := client.Wallet.GenerateApproveCallData(UniswapPermit2Polygon, amountToSwap.Uint64()) if err != nil { log.Fatalf("Failed to generate call data: %v\n", err) } + + approveData, err := hex.DecodeString(approveDataString[2:]) + if err != nil { + log.Fatalf("Failed to decode approveDataString: %v\n", err) + } + + toAddressFrax := common.HexToAddress(PolygonFRAX) + builder := client.TxBuilder.New() - tx, err := builder.SetData(approveData).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) + tx, err := builder.SetData(approveData).SetTo(&toAddressFrax).SetGas(21_000 * 5).Build(ctx) if err != nil { - log.Fatalf("Failed to build transaction: %v\n", err) + log.Fatalf("Failed to build approval transaction: %v\n", err) } signedTx, err := client.Wallet.Sign(tx) if err != nil { @@ -96,72 +103,73 @@ func main() { log.Fatalf("Failed to broadcast transaction: %v\n", err) } } - if cmp > 0 { - spender, err := client.GetApproveSpender(ctx) - if err != nil { - panic(err) - } - now := time.Now() - twoDaysLater := now.Add(time.Hour * 24 * 2) - - permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(UniswapPermit2Polygon), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) - if err != nil { - log.Fatalf("Failed to get permit data: %v\n", err) - } - - permit, err = client.Wallet.TokenPermit(*permitData) - if err != nil { - log.Fatalf("Failed to sign permit: %v\n", err) - } - } - - swapParams := aggregation.GetSwapParams{ - Src: PolygonFRAX, - Dst: PolygonWeth, - Amount: amountToSwap.String(), - From: client.Wallet.Address().Hex(), - Slippage: 1, - } - if permit != "" { - swapParams.Permit = permit - } - swapData, err := client.GetSwap(ctx, swapParams) - if err != nil { - log.Fatalf("Failed to get swap data: %v\n", err) - } - - builder := client.TxBuilder.New() - - tx, err := builder.SetData(swapData.TxNormalized.Data).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) - if err != nil { - log.Fatalf("Failed to build transaction: %v\n", err) - } - signedTx, err := client.Wallet.Sign(tx) - if err != nil { - log.Fatalf("Failed to sign transaction: %v\n", err) - } - err = client.Wallet.BroadcastTransaction(ctx, signedTx) - if err != nil { - log.Fatalf("Failed to broadcast transaction: %v\n", err) - } - - // Waiting for transaction, just an example of it - fmt.Printf("Transaction has been broadcast. View it on Polygonscan here: %v\n", fmt.Sprintf("https://polygonscan.com/tx/%v", signedTx.Hash().Hex())) - for { - receipt, err := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) - if receipt != nil { - fmt.Println("Transaction complete!") - return - } - if err != nil { - fmt.Println("Waiting for transaction to be mined") - } - select { - case <-time.After(1000 * time.Millisecond): // check again after a delay - case <-ctx.Done(): - fmt.Println("Context cancelled") - return - } - } + //if cmp > 0 { + // spender, err := client.GetApproveSpender(ctx) + // if err != nil { + // panic(err) + // } + // now := time.Now() + // twoDaysLater := now.Add(time.Hour * 24 * 2) + // + // permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(UniswapPermit2Polygon), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) + // if err != nil { + // log.Fatalf("Failed to get permit data: %v\n", err) + // } + // + // permit, err = client.Wallet.TokenPermit(*permitData) + // if err != nil { + // log.Fatalf("Failed to sign permit: %v\n", err) + // } + //} + // + //swapParams := aggregation.GetSwapParams{ + // Src: PolygonFRAX, + // Dst: PolygonWeth, + // Amount: amountToSwap.String(), + // From: client.Wallet.Address().Hex(), + // Slippage: 1, + //} + //if permit != "" { + // swapParams.Permit = permit + //} + //swapData, err := client.GetSwap(ctx, swapParams) + //if err != nil { + // log.Fatalf("Failed to get swap data: %v\n", err) + //} + // + //builder := client.TxBuilder.New() + // + //tx, err := builder.SetData(swapData.TxNormalized.Data).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) + //if err != nil { + // log.Fatalf("Failed to build transaction: %v\n", err) + //} + //signedTx, err := client.Wallet.Sign(tx) + //if err != nil { + // log.Fatalf("Failed to sign transaction: %v\n", err) + //} + // + //err = client.Wallet.BroadcastTransaction(ctx, signedTx) + //if err != nil { + // log.Fatalf("Failed to broadcast transaction: %v\n", err) + //} + // + //// Waiting for transaction, just an example of it + //fmt.Printf("Transaction has been broadcast. View it on Polygonscan here: %v\n", fmt.Sprintf("https://polygonscan.com/tx/%v", signedTx.Hash().Hex())) + //for { + // receipt, err := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) + // if receipt != nil { + // fmt.Println("Transaction complete!") + // return + // } + // if err != nil { + // fmt.Println("Waiting for transaction to be mined") + // } + // select { + // case <-time.After(1000 * time.Millisecond): // check again after a delay + // case <-ctx.Done(): + // fmt.Println("Context cancelled") + // return + // } + //} } From bbc7dabad02a82d172ccf7b7d7e7d21e71de8007 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:26:42 +0200 Subject: [PATCH 08/18] feat: setup permit2 hash for single transfer contract --- internal/web3-provider/permit2.go | 205 ++++++++++++++++++ internal/web3-provider/permit2_test.go | 39 ++++ .../examples/swap-with-permit2/main.go | 178 ++++++++------- 3 files changed, 344 insertions(+), 78 deletions(-) create mode 100644 internal/web3-provider/permit2.go create mode 100644 internal/web3-provider/permit2_test.go diff --git a/internal/web3-provider/permit2.go b/internal/web3-provider/permit2.go new file mode 100644 index 00000000..4752e0ed --- /dev/null +++ b/internal/web3-provider/permit2.go @@ -0,0 +1,205 @@ +package web3_provider + +import ( + "bytes" + "math/big" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + PERMIT2_DOMAIN_NAME = "Permit2" +) + +var ( + MaxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) + MaxSignatureTransferAmount = MaxUint256 + MaxUnorderedNonce = MaxUint256 + MaxSigDeadline = MaxUint256 +) + +type TypedDataDomain struct { + Name string + ChainId int + VerifyingContract string +} + +type TypedDataField struct { + Name string + Type string +} + +type TokenPermissions struct { + Token string + Amount *big.Int +} + +type PermitTransferFrom struct { + Permitted TokenPermissions + Spender string + Nonce *big.Int + Deadline *big.Int +} + +type PermitBatchTransferFrom struct { + Permitted []TokenPermissions + Spender string + Nonce *big.Int + Deadline *big.Int +} + +type Witness struct { + Witness interface{} + WitnessTypeName string + WitnessType map[string][]TypedDataField +} + +type PermitTransferFromData struct { + Domain TypedDataDomain + Types map[string][]TypedDataField + Values PermitTransferFrom +} + +var TOKEN_PERMISSIONS = []TypedDataField{ + {Name: "token", Type: "address"}, + {Name: "amount", Type: "uint256"}, +} + +var PERMIT_TRANSFER_FROM_TYPES = map[string][]TypedDataField{ + "PermitTransferFrom": { + {Name: "permitted", Type: "TokenPermissions"}, + {Name: "spender", Type: "address"}, + {Name: "nonce", Type: "uint256"}, + {Name: "deadline", Type: "uint256"}, + }, + "TokenPermissions": TOKEN_PERMISSIONS, +} + +func permit2Domain(permit2Address string, chainId int) TypedDataDomain { + return TypedDataDomain{ + Name: PERMIT2_DOMAIN_NAME, + ChainId: chainId, + VerifyingContract: permit2Address, + } +} + +func permitTransferFromWithWitnessType(witness Witness) map[string][]TypedDataField { + return map[string][]TypedDataField{ + "PermitWitnessTransferFrom": { + {Name: "permitted", Type: "TokenPermissions"}, + {Name: "spender", Type: "address"}, + {Name: "nonce", Type: "uint256"}, + {Name: "deadline", Type: "uint256"}, + {Name: "witness", Type: witness.WitnessTypeName}, + }, + "TokenPermissions": TOKEN_PERMISSIONS, + witness.WitnessTypeName: witness.WitnessType[witness.WitnessTypeName], + } +} + +func validateTokenPermissions(permissions TokenPermissions) { + if MaxSignatureTransferAmount.Cmp(permissions.Amount) < 0 { + panic("AMOUNT_OUT_OF_RANGE") + } +} + +func getPermitTransferFromData( + permit PermitTransferFrom, + permit2Address string, + chainId int, + witness *Witness, +) PermitTransferFromData { + if MaxSigDeadline.Cmp(permit.Deadline) < 0 { + panic("SIG_DEADLINE_OUT_OF_RANGE") + } + if MaxUnorderedNonce.Cmp(permit.Nonce) < 0 { + panic("NONCE_OUT_OF_RANGE") + } + + validateTokenPermissions(permit.Permitted) + + domain := permit2Domain(permit2Address, chainId) + var types map[string][]TypedDataField + var values interface{} + + if witness != nil { + types = permitTransferFromWithWitnessType(*witness) + values = struct { + PermitTransferFrom + Witness interface{} + }{permit, witness.Witness} + } else { + types = PERMIT_TRANSFER_FROM_TYPES + values = permit + } + + return PermitTransferFromData{ + Domain: domain, + Types: types, + Values: values.(PermitTransferFrom), + } +} + +func hashPermitTransferFrom( + permit PermitTransferFrom, + permit2Address string, + chainId int, + witness *Witness, +) string { + permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) + domainHash := hashDomain(permitData.Domain) + structHash := hashStruct(permitData.Types, permitData.Values) + + finalHash := crypto.Keccak256Hash(append([]byte{0x19, 0x01}, append(domainHash, structHash...)...)).Hex() + return finalHash +} + +func hashDomain(domain TypedDataDomain) []byte { + var buffer bytes.Buffer + + buffer.Write(crypto.Keccak256([]byte("EIP712Domain(string name,uint256 chainId,address verifyingContract)"))) + + nameHash := crypto.Keccak256([]byte(domain.Name)) + buffer.Write(nameHash) + + chainIdBytes := common.LeftPadBytes(new(big.Int).SetInt64(int64(domain.ChainId)).Bytes(), 32) + buffer.Write(chainIdBytes) + + addressBytes := common.HexToAddress(domain.VerifyingContract).Bytes() + buffer.Write(addressBytes) + + return crypto.Keccak256(buffer.Bytes()) +} + +func hashStruct(types map[string][]TypedDataField, values PermitTransferFrom) []byte { + var buffer bytes.Buffer + + primaryType := "" + for t := range types { + primaryType = t + break + } + + buffer.Write(crypto.Keccak256([]byte(primaryType))) + + value := reflect.ValueOf(values) + for _, field := range types[primaryType] { + fieldName := field.Name + fieldValue := value.FieldByName(strings.Title(fieldName)) + + if field.Type == "address" { + buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) + } else if field.Type == "uint256" { + buffer.Write(common.LeftPadBytes(fieldValue.Interface().(*big.Int).Bytes(), 32)) + } else if field.Type == "TokenPermissions" { + tokenPerm := fieldValue.Interface().(TokenPermissions) + buffer.Write(common.HexToAddress(tokenPerm.Token).Bytes()) + buffer.Write(common.LeftPadBytes(tokenPerm.Amount.Bytes(), 32)) + } + } + + return crypto.Keccak256(buffer.Bytes()) +} diff --git a/internal/web3-provider/permit2_test.go b/internal/web3-provider/permit2_test.go new file mode 100644 index 00000000..d6a7289b --- /dev/null +++ b/internal/web3-provider/permit2_test.go @@ -0,0 +1,39 @@ +package web3_provider + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestHashPermitTransferFrom_MaxValues(t *testing.T) { + permit := PermitTransferFrom{ + Permitted: TokenPermissions{ + Token: "0x0000000000000000000000000000000000000000", + Amount: MaxSignatureTransferAmount, + }, + Spender: "0x0000000000000000000000000000000000000000", + Nonce: MaxUnorderedNonce, + Deadline: MaxSigDeadline, + } + permit2Address := "0x0000000000000000000000000000000000000000" + chainId := 1 + + // Call the hash function + hash := hashPermitTransferFrom(permit, permit2Address, chainId, nil) + + // Define the expected hash value + expectedHash := "0x99e8cd5cd187c1dcb3c9cb41664cb12c1a3a76143d21b16f7880f4839d2b2ad4" + + // Convert expectedHash to bytes for comparison + expectedHashBytes := common.Hex2Bytes(expectedHash) + + // Convert hash to bytes for comparison + hashBytes := common.Hex2Bytes(hash) + + // Compare the hashes + if !bytes.Equal(hashBytes, expectedHashBytes) { + t.Errorf("Expected hash %s, but got %s", expectedHash, hash) + } +} diff --git a/sdk-clients/aggregation/examples/swap-with-permit2/main.go b/sdk-clients/aggregation/examples/swap-with-permit2/main.go index 68fbf05f..e1def111 100644 --- a/sdk-clients/aggregation/examples/swap-with-permit2/main.go +++ b/sdk-clients/aggregation/examples/swap-with-permit2/main.go @@ -3,9 +3,11 @@ package main import ( "context" "encoding/hex" + "fmt" "log" "math/big" "os" + "time" "github.com/ethereum/go-ethereum/common" @@ -53,16 +55,16 @@ func main() { amountToSwap := big.NewInt(1e17) - allowanceData, err := client.GetApproveAllowance(ctx, aggregation.GetAllowanceParams{ - TokenAddress: PolygonFRAX, - WalletAddress: client.Wallet.Address().Hex(), - }) - if err != nil { - log.Fatalf("Failed to get allowance: %v\n", err) - } - - allowance := new(big.Int) - allowance.SetString(allowanceData.Allowance, 10) + //allowanceData, err := client.GetApproveAllowance(ctx, aggregation.GetAllowanceParams{ + // TokenAddress: PolygonFRAX, + // WalletAddress: client.Wallet.Address().Hex(), + //}) + //if err != nil { + // log.Fatalf("Failed to get allowance: %v\n", err) + //} + // + //allowance := new(big.Int) + //allowance.SetString(allowanceData.Allowance, 10) //cmp := amountToSwap.Cmp(allowance) // @@ -73,6 +75,8 @@ func main() { log.Fatalf("Failed to get allowance: %v\n", err) } + var permit string + cmp2 := amountToSwap.Cmp(perimit2allowance) if cmp2 > 0 { approveDataString, err := client.Wallet.GenerateApproveCallData(UniswapPermit2Polygon, amountToSwap.Uint64()) @@ -102,74 +106,92 @@ func main() { if err != nil { log.Fatalf("Failed to broadcast transaction: %v\n", err) } + + spender, err := client.GetApproveSpender(ctx) + if err != nil { + panic(err) + } + now := time.Now() + twoDaysLater := now.Add(time.Hour * 24 * 2) + + permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(UniswapPermit2Polygon), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) + if err != nil { + log.Fatalf("Failed to get permit data: %v\n", err) + } + + permit, err = client.Wallet.TokenPermit(*permitData) + if err != nil { + log.Fatalf("Failed to sign permit: %v\n", err) + } } - //if cmp > 0 { - // spender, err := client.GetApproveSpender(ctx) - // if err != nil { - // panic(err) - // } - // now := time.Now() - // twoDaysLater := now.Add(time.Hour * 24 * 2) - // - // permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(UniswapPermit2Polygon), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) - // if err != nil { - // log.Fatalf("Failed to get permit data: %v\n", err) - // } - // - // permit, err = client.Wallet.TokenPermit(*permitData) - // if err != nil { - // log.Fatalf("Failed to sign permit: %v\n", err) - // } - //} - // - //swapParams := aggregation.GetSwapParams{ - // Src: PolygonFRAX, - // Dst: PolygonWeth, - // Amount: amountToSwap.String(), - // From: client.Wallet.Address().Hex(), - // Slippage: 1, - //} - //if permit != "" { - // swapParams.Permit = permit - //} - //swapData, err := client.GetSwap(ctx, swapParams) - //if err != nil { - // log.Fatalf("Failed to get swap data: %v\n", err) - //} - // - //builder := client.TxBuilder.New() - // - //tx, err := builder.SetData(swapData.TxNormalized.Data).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) - //if err != nil { - // log.Fatalf("Failed to build transaction: %v\n", err) - //} - //signedTx, err := client.Wallet.Sign(tx) - //if err != nil { - // log.Fatalf("Failed to sign transaction: %v\n", err) - //} - // - //err = client.Wallet.BroadcastTransaction(ctx, signedTx) - //if err != nil { - // log.Fatalf("Failed to broadcast transaction: %v\n", err) - //} - // - //// Waiting for transaction, just an example of it - //fmt.Printf("Transaction has been broadcast. View it on Polygonscan here: %v\n", fmt.Sprintf("https://polygonscan.com/tx/%v", signedTx.Hash().Hex())) - //for { - // receipt, err := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) - // if receipt != nil { - // fmt.Println("Transaction complete!") - // return - // } - // if err != nil { - // fmt.Println("Waiting for transaction to be mined") - // } - // select { - // case <-time.After(1000 * time.Millisecond): // check again after a delay - // case <-ctx.Done(): - // fmt.Println("Context cancelled") - // return - // } - //} + if cmp2 == 0 { + spender, err := client.GetApproveSpender(ctx) + if err != nil { + panic(err) + } + now := time.Now() + twoDaysLater := now.Add(time.Hour * 24 * 2) + + permitData, err := client.Wallet.GetContractDetailsForPermit(ctx, common.HexToAddress(UniswapPermit2Polygon), common.HexToAddress(spender.Address), amountToSwap, twoDaysLater.Unix()) + if err != nil { + log.Fatalf("Failed to get permit data: %v\n", err) + } + + permit, err = client.Wallet.TokenPermit(*permitData) + if err != nil { + log.Fatalf("Failed to sign permit: %v\n", err) + } + } + + swapParams := aggregation.GetSwapParams{ + Src: PolygonFRAX, + Dst: PolygonWeth, + Amount: amountToSwap.String(), + From: client.Wallet.Address().Hex(), + Slippage: 1, + UsePermit2: true, + } + if permit != "" { + swapParams.Permit = permit + } + swapData, err := client.GetSwap(ctx, swapParams) + if err != nil { + log.Fatalf("Failed to get swap data: %v\n", err) + } + + builder := client.TxBuilder.New() + + tx, err := builder.SetData(swapData.TxNormalized.Data).SetTo(&swapData.TxNormalized.To).SetGas(swapData.TxNormalized.Gas).SetValue(swapData.TxNormalized.Value).Build(ctx) + if err != nil { + log.Fatalf("Failed to build transaction: %v\n", err) + } + signedTx, err := client.Wallet.Sign(tx) + if err != nil { + log.Fatalf("Failed to sign transaction: %v\n", err) + } + + err = client.Wallet.BroadcastTransaction(ctx, signedTx) + if err != nil { + log.Fatalf("Failed to broadcast transaction: %v\n", err) + } + + // Waiting for transaction, just an example of it + fmt.Printf("Transaction has been broadcast. View it on Polygonscan here: %v\n", fmt.Sprintf("https://polygonscan.com/tx/%v", signedTx.Hash().Hex())) + for { + receipt, err := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) + if receipt != nil { + fmt.Println("Transaction complete!") + return + } + if err != nil { + fmt.Println("Waiting for transaction to be mined") + } + select { + case <-time.After(1000 * time.Millisecond): // check again after a delay + case <-ctx.Done(): + fmt.Println("Context cancelled") + return + } + } } From 2ed9f25d389ee6892c208eb91412f65bb80f70ca Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:36:14 +0200 Subject: [PATCH 09/18] feat: add signing permit2 --- internal/web3-provider/permit2.go | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/internal/web3-provider/permit2.go b/internal/web3-provider/permit2.go index 4752e0ed..457302f0 100644 --- a/internal/web3-provider/permit2.go +++ b/internal/web3-provider/permit2.go @@ -2,10 +2,12 @@ package web3_provider import ( "bytes" + "fmt" "math/big" "reflect" "strings" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -14,6 +16,22 @@ const ( PERMIT2_DOMAIN_NAME = "Permit2" ) +const ( + // Update this with the ABI for your contract + abiString = ` +[ + { + "type": "function", + "name": "permitTransferFrom", + "inputs": [ + { "name": "data", "type": "PermitTransferFromData" }, + { "name": "witness", "type": "bytes" } + ] + } +] +` +) + var ( MaxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) MaxSignatureTransferAmount = MaxUint256 @@ -203,3 +221,30 @@ func hashStruct(types map[string][]TypedDataField, values PermitTransferFrom) [] return crypto.Keccak256(buffer.Bytes()) } + +func (w Wallet) SignPermit2TransferFrom( + permit PermitTransferFrom, + permit2Address string, + witness *Witness, +) ([]byte, error) { + permitData := getPermitTransferFromData(permit, permit2Address, int(w.chainId.Uint64()), witness) + + encodedData, err := abi.JSON(strings.NewReader(abiString)) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %v", err) + } + _, err = encodedData.Pack("permitTransferFrom", permitData, nil) + if err != nil { + return nil, fmt.Errorf("failed to pack permit data: %v", err) + } + + message := fmt.Sprintf("\x19\x01%s", string(hashPermitTransferFrom(permit, permit2Address, chainId, witness))) + + hash := crypto.Keccak256Hash([]byte(message)) + signature, err := crypto.Sign(hash.Bytes(), w.privateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign permit: %v", err) + } + + return signature, nil +} From 9ba3cbacfddc1e37eac79a0e54016bd2aa0f8c39 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:37:05 +0200 Subject: [PATCH 10/18] fix(var): permit2 --- internal/web3-provider/permit2.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/web3-provider/permit2.go b/internal/web3-provider/permit2.go index 457302f0..cd274b6e 100644 --- a/internal/web3-provider/permit2.go +++ b/internal/web3-provider/permit2.go @@ -227,7 +227,8 @@ func (w Wallet) SignPermit2TransferFrom( permit2Address string, witness *Witness, ) ([]byte, error) { - permitData := getPermitTransferFromData(permit, permit2Address, int(w.chainId.Uint64()), witness) + chainId := int(w.chainId.Uint64()) + permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) encodedData, err := abi.JSON(strings.NewReader(abiString)) if err != nil { From 3ea0460a4350817eaa89d048caaaa7df8397c96b Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:43:00 +0200 Subject: [PATCH 11/18] fix: lint --- go.mod | 1 + internal/transaction-builder/transaction_builder_test.go | 7 +++++++ internal/web3-provider/permit2.go | 4 +++- sdk-clients/aggregation/client_test.go | 7 +++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index fd432f81..66774b05 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/go-querystring v1.1.0 github.com/oapi-codegen/runtime v1.1.1 github.com/stretchr/testify v1.9.0 + golang.org/x/text v0.14.0 ) require ( diff --git a/internal/transaction-builder/transaction_builder_test.go b/internal/transaction-builder/transaction_builder_test.go index 802ec5d7..aa7d8b04 100644 --- a/internal/transaction-builder/transaction_builder_test.go +++ b/internal/transaction-builder/transaction_builder_test.go @@ -229,3 +229,10 @@ func (w *MyWallet) IsEIP1559Applicable() bool { func (w *MyWallet) ChainId() int64 { return w.chainID.Int64() } + +func (w *MyWallet) GenerateApproveCallData(addressTo string, amount uint64) (string, error) { + return "", nil +} +func (w *MyWallet) TokenAllowance(ctx context.Context, tokenAddress string, spenderAddress string) (*big.Int, error) { + return big.NewInt(1), nil +} diff --git a/internal/web3-provider/permit2.go b/internal/web3-provider/permit2.go index cd274b6e..9e864e71 100644 --- a/internal/web3-provider/permit2.go +++ b/internal/web3-provider/permit2.go @@ -10,6 +10,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) const ( @@ -206,7 +208,7 @@ func hashStruct(types map[string][]TypedDataField, values PermitTransferFrom) [] value := reflect.ValueOf(values) for _, field := range types[primaryType] { fieldName := field.Name - fieldValue := value.FieldByName(strings.Title(fieldName)) + fieldValue := value.FieldByName(cases.Title(language.English).String(fieldName)) if field.Type == "address" { buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) diff --git a/sdk-clients/aggregation/client_test.go b/sdk-clients/aggregation/client_test.go index 2ea8bae4..364f33a8 100644 --- a/sdk-clients/aggregation/client_test.go +++ b/sdk-clients/aggregation/client_test.go @@ -177,6 +177,13 @@ func (w *MyWallet) ChainId() int64 { return w.chainID.Int64() } +func (w *MyWallet) GenerateApproveCallData(addressTo string, amount uint64) (string, error) { + return "", nil +} +func (w *MyWallet) TokenAllowance(ctx context.Context, tokenAddress string, spenderAddress string) (*big.Int, error) { + return big.NewInt(1), nil +} + type mockHttpExecutor struct { Called bool ExecuteErr error From 535be5c058f4792319be5c8ca31d51e7685911e3 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Tue, 25 Jun 2024 16:15:48 +0200 Subject: [PATCH 12/18] feat: allowance draft --- internal/web3-provider/permit2.go | 503 +++++++++--------- internal/web3-provider/permit2_test.go | 67 ++- internal/web3-provider/permit2allowance.go | 129 +++++ .../web3-provider/permit2allowance_test.go | 38 ++ 4 files changed, 449 insertions(+), 288 deletions(-) create mode 100644 internal/web3-provider/permit2allowance.go create mode 100644 internal/web3-provider/permit2allowance_test.go diff --git a/internal/web3-provider/permit2.go b/internal/web3-provider/permit2.go index 9e864e71..fef0fdf0 100644 --- a/internal/web3-provider/permit2.go +++ b/internal/web3-provider/permit2.go @@ -1,253 +1,254 @@ package web3_provider -import ( - "bytes" - "fmt" - "math/big" - "reflect" - "strings" - - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -const ( - PERMIT2_DOMAIN_NAME = "Permit2" -) - -const ( - // Update this with the ABI for your contract - abiString = ` -[ - { - "type": "function", - "name": "permitTransferFrom", - "inputs": [ - { "name": "data", "type": "PermitTransferFromData" }, - { "name": "witness", "type": "bytes" } - ] - } -] -` -) - -var ( - MaxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) - MaxSignatureTransferAmount = MaxUint256 - MaxUnorderedNonce = MaxUint256 - MaxSigDeadline = MaxUint256 -) - -type TypedDataDomain struct { - Name string - ChainId int - VerifyingContract string -} - -type TypedDataField struct { - Name string - Type string -} - -type TokenPermissions struct { - Token string - Amount *big.Int -} - -type PermitTransferFrom struct { - Permitted TokenPermissions - Spender string - Nonce *big.Int - Deadline *big.Int -} - -type PermitBatchTransferFrom struct { - Permitted []TokenPermissions - Spender string - Nonce *big.Int - Deadline *big.Int -} - -type Witness struct { - Witness interface{} - WitnessTypeName string - WitnessType map[string][]TypedDataField -} - -type PermitTransferFromData struct { - Domain TypedDataDomain - Types map[string][]TypedDataField - Values PermitTransferFrom -} - -var TOKEN_PERMISSIONS = []TypedDataField{ - {Name: "token", Type: "address"}, - {Name: "amount", Type: "uint256"}, -} - -var PERMIT_TRANSFER_FROM_TYPES = map[string][]TypedDataField{ - "PermitTransferFrom": { - {Name: "permitted", Type: "TokenPermissions"}, - {Name: "spender", Type: "address"}, - {Name: "nonce", Type: "uint256"}, - {Name: "deadline", Type: "uint256"}, - }, - "TokenPermissions": TOKEN_PERMISSIONS, -} - -func permit2Domain(permit2Address string, chainId int) TypedDataDomain { - return TypedDataDomain{ - Name: PERMIT2_DOMAIN_NAME, - ChainId: chainId, - VerifyingContract: permit2Address, - } -} - -func permitTransferFromWithWitnessType(witness Witness) map[string][]TypedDataField { - return map[string][]TypedDataField{ - "PermitWitnessTransferFrom": { - {Name: "permitted", Type: "TokenPermissions"}, - {Name: "spender", Type: "address"}, - {Name: "nonce", Type: "uint256"}, - {Name: "deadline", Type: "uint256"}, - {Name: "witness", Type: witness.WitnessTypeName}, - }, - "TokenPermissions": TOKEN_PERMISSIONS, - witness.WitnessTypeName: witness.WitnessType[witness.WitnessTypeName], - } -} - -func validateTokenPermissions(permissions TokenPermissions) { - if MaxSignatureTransferAmount.Cmp(permissions.Amount) < 0 { - panic("AMOUNT_OUT_OF_RANGE") - } -} - -func getPermitTransferFromData( - permit PermitTransferFrom, - permit2Address string, - chainId int, - witness *Witness, -) PermitTransferFromData { - if MaxSigDeadline.Cmp(permit.Deadline) < 0 { - panic("SIG_DEADLINE_OUT_OF_RANGE") - } - if MaxUnorderedNonce.Cmp(permit.Nonce) < 0 { - panic("NONCE_OUT_OF_RANGE") - } - - validateTokenPermissions(permit.Permitted) - - domain := permit2Domain(permit2Address, chainId) - var types map[string][]TypedDataField - var values interface{} - - if witness != nil { - types = permitTransferFromWithWitnessType(*witness) - values = struct { - PermitTransferFrom - Witness interface{} - }{permit, witness.Witness} - } else { - types = PERMIT_TRANSFER_FROM_TYPES - values = permit - } - - return PermitTransferFromData{ - Domain: domain, - Types: types, - Values: values.(PermitTransferFrom), - } -} - -func hashPermitTransferFrom( - permit PermitTransferFrom, - permit2Address string, - chainId int, - witness *Witness, -) string { - permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) - domainHash := hashDomain(permitData.Domain) - structHash := hashStruct(permitData.Types, permitData.Values) - - finalHash := crypto.Keccak256Hash(append([]byte{0x19, 0x01}, append(domainHash, structHash...)...)).Hex() - return finalHash -} - -func hashDomain(domain TypedDataDomain) []byte { - var buffer bytes.Buffer - - buffer.Write(crypto.Keccak256([]byte("EIP712Domain(string name,uint256 chainId,address verifyingContract)"))) - - nameHash := crypto.Keccak256([]byte(domain.Name)) - buffer.Write(nameHash) - - chainIdBytes := common.LeftPadBytes(new(big.Int).SetInt64(int64(domain.ChainId)).Bytes(), 32) - buffer.Write(chainIdBytes) - - addressBytes := common.HexToAddress(domain.VerifyingContract).Bytes() - buffer.Write(addressBytes) - - return crypto.Keccak256(buffer.Bytes()) -} - -func hashStruct(types map[string][]TypedDataField, values PermitTransferFrom) []byte { - var buffer bytes.Buffer - - primaryType := "" - for t := range types { - primaryType = t - break - } - - buffer.Write(crypto.Keccak256([]byte(primaryType))) - - value := reflect.ValueOf(values) - for _, field := range types[primaryType] { - fieldName := field.Name - fieldValue := value.FieldByName(cases.Title(language.English).String(fieldName)) - - if field.Type == "address" { - buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) - } else if field.Type == "uint256" { - buffer.Write(common.LeftPadBytes(fieldValue.Interface().(*big.Int).Bytes(), 32)) - } else if field.Type == "TokenPermissions" { - tokenPerm := fieldValue.Interface().(TokenPermissions) - buffer.Write(common.HexToAddress(tokenPerm.Token).Bytes()) - buffer.Write(common.LeftPadBytes(tokenPerm.Amount.Bytes(), 32)) - } - } - - return crypto.Keccak256(buffer.Bytes()) -} - -func (w Wallet) SignPermit2TransferFrom( - permit PermitTransferFrom, - permit2Address string, - witness *Witness, -) ([]byte, error) { - chainId := int(w.chainId.Uint64()) - permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) - - encodedData, err := abi.JSON(strings.NewReader(abiString)) - if err != nil { - return nil, fmt.Errorf("failed to parse ABI: %v", err) - } - _, err = encodedData.Pack("permitTransferFrom", permitData, nil) - if err != nil { - return nil, fmt.Errorf("failed to pack permit data: %v", err) - } - - message := fmt.Sprintf("\x19\x01%s", string(hashPermitTransferFrom(permit, permit2Address, chainId, witness))) - - hash := crypto.Keccak256Hash([]byte(message)) - signature, err := crypto.Sign(hash.Bytes(), w.privateKey) - if err != nil { - return nil, fmt.Errorf("failed to sign permit: %v", err) - } - - return signature, nil -} +// +//import ( +// "bytes" +// "fmt" +// "math/big" +// "reflect" +// "strings" +// +// "github.com/ethereum/go-ethereum/accounts/abi" +// "github.com/ethereum/go-ethereum/common" +// "github.com/ethereum/go-ethereum/crypto" +// "golang.org/x/text/cases" +// "golang.org/x/text/language" +//) +// +//const ( +// PERMIT2_DOMAIN_NAME = "Permit2" +//) +// +//const ( +// // Update this with the ABI for your contract +// abiString = ` +//[ +// { +// "type": "function", +// "name": "permitTransferFrom", +// "inputs": [ +// { "name": "data", "type": "PermitTransferFromData" }, +// { "name": "witness", "type": "bytes" } +// ] +// } +//] +//` +//) +// +//var ( +// MaxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) +// MaxSignatureTransferAmount = MaxUint256 +// MaxUnorderedNonce = MaxUint256 +// MaxSigDeadline = MaxUint256 +//) +// +//type TypedDataDomain struct { +// Name string +// ChainId int +// VerifyingContract string +//} +// +//type TypedDataField struct { +// Name string +// Type string +//} +// +//type TokenPermissions struct { +// Token string +// Amount *big.Int +//} +// +//type PermitTransferFrom struct { +// Permitted TokenPermissions +// Spender string +// Nonce *big.Int +// Deadline *big.Int +//} +// +//type PermitBatchTransferFrom struct { +// Permitted []TokenPermissions +// Spender string +// Nonce *big.Int +// Deadline *big.Int +//} +// +//type Witness struct { +// Witness interface{} +// WitnessTypeName string +// WitnessType map[string][]TypedDataField +//} +// +//type PermitTransferFromData struct { +// Domain TypedDataDomain +// Types map[string][]TypedDataField +// Values PermitTransferFrom +//} +// +//var TOKEN_PERMISSIONS = []TypedDataField{ +// {Name: "token", Type: "address"}, +// {Name: "amount", Type: "uint256"}, +//} +// +//var PERMIT_TRANSFER_FROM_TYPES = map[string][]TypedDataField{ +// "PermitTransferFrom": { +// {Name: "permitted", Type: "TokenPermissions"}, +// {Name: "spender", Type: "address"}, +// {Name: "nonce", Type: "uint256"}, +// {Name: "deadline", Type: "uint256"}, +// }, +// "TokenPermissions": TOKEN_PERMISSIONS, +//} +// +//func permit2Domain(permit2Address string, chainId int) TypedDataDomain { +// return TypedDataDomain{ +// Name: PERMIT2_DOMAIN_NAME, +// ChainId: chainId, +// VerifyingContract: permit2Address, +// } +//} +// +//func permitTransferFromWithWitnessType(witness Witness) map[string][]TypedDataField { +// return map[string][]TypedDataField{ +// "PermitWitnessTransferFrom": { +// {Name: "permitted", Type: "TokenPermissions"}, +// {Name: "spender", Type: "address"}, +// {Name: "nonce", Type: "uint256"}, +// {Name: "deadline", Type: "uint256"}, +// {Name: "witness", Type: witness.WitnessTypeName}, +// }, +// "TokenPermissions": TOKEN_PERMISSIONS, +// witness.WitnessTypeName: witness.WitnessType[witness.WitnessTypeName], +// } +//} +// +//func validateTokenPermissions(permissions TokenPermissions) { +// if MaxSignatureTransferAmount.Cmp(permissions.Amount) < 0 { +// panic("AMOUNT_OUT_OF_RANGE") +// } +//} +// +//func getPermitTransferFromData( +// permit PermitTransferFrom, +// permit2Address string, +// chainId int, +// witness *Witness, +//) PermitTransferFromData { +// if MaxSigDeadline.Cmp(permit.Deadline) < 0 { +// panic("SIG_DEADLINE_OUT_OF_RANGE") +// } +// if MaxUnorderedNonce.Cmp(permit.Nonce) < 0 { +// panic("NONCE_OUT_OF_RANGE") +// } +// +// validateTokenPermissions(permit.Permitted) +// +// domain := permit2Domain(permit2Address, chainId) +// var types map[string][]TypedDataField +// var values interface{} +// +// if witness != nil { +// types = permitTransferFromWithWitnessType(*witness) +// values = struct { +// PermitTransferFrom +// Witness interface{} +// }{permit, witness.Witness} +// } else { +// types = PERMIT_TRANSFER_FROM_TYPES +// values = permit +// } +// +// return PermitTransferFromData{ +// Domain: domain, +// Types: types, +// Values: values.(PermitTransferFrom), +// } +//} +// +//func hashPermitTransferFrom( +// permit PermitTransferFrom, +// permit2Address string, +// chainId int, +// witness *Witness, +//) string { +// permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) +// domainHash := hashDomain(permitData.Domain) +// structHash := hashStruct(permitData.Types, permitData.Values) +// +// finalHash := crypto.Keccak256Hash(append([]byte{0x19, 0x01}, append(domainHash, structHash...)...)).Hex() +// return finalHash +//} +// +//func hashDomain(domain TypedDataDomain) []byte { +// var buffer bytes.Buffer +// +// buffer.Write(crypto.Keccak256([]byte("EIP712Domain(string name,uint256 chainId,address verifyingContract)"))) +// +// nameHash := crypto.Keccak256([]byte(domain.Name)) +// buffer.Write(nameHash) +// +// chainIdBytes := common.LeftPadBytes(new(big.Int).SetInt64(int64(domain.ChainId)).Bytes(), 32) +// buffer.Write(chainIdBytes) +// +// addressBytes := common.HexToAddress(domain.VerifyingContract).Bytes() +// buffer.Write(addressBytes) +// +// return crypto.Keccak256(buffer.Bytes()) +//} +// +//func hashStruct(types map[string][]TypedDataField, values PermitTransferFrom) []byte { +// var buffer bytes.Buffer +// +// primaryType := "" +// for t := range types { +// primaryType = t +// break +// } +// +// buffer.Write(crypto.Keccak256([]byte(primaryType))) +// +// value := reflect.ValueOf(values) +// for _, field := range types[primaryType] { +// fieldName := field.Name +// fieldValue := value.FieldByName(cases.Title(language.English).String(fieldName)) +// +// if field.Type == "address" { +// buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) +// } else if field.Type == "uint256" { +// buffer.Write(common.LeftPadBytes(fieldValue.Interface().(*big.Int).Bytes(), 32)) +// } else if field.Type == "TokenPermissions" { +// tokenPerm := fieldValue.Interface().(TokenPermissions) +// buffer.Write(common.HexToAddress(tokenPerm.Token).Bytes()) +// buffer.Write(common.LeftPadBytes(tokenPerm.Amount.Bytes(), 32)) +// } +// } +// +// return crypto.Keccak256(buffer.Bytes()) +//} +// +//func (w Wallet) SignPermit2TransferFrom( +// permit PermitTransferFrom, +// permit2Address string, +// witness *Witness, +//) ([]byte, error) { +// chainId := int(w.chainId.Uint64()) +// permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) +// +// encodedData, err := abi.JSON(strings.NewReader(abiString)) +// if err != nil { +// return nil, fmt.Errorf("failed to parse ABI: %v", err) +// } +// _, err = encodedData.Pack("permitTransferFrom", permitData, nil) +// if err != nil { +// return nil, fmt.Errorf("failed to pack permit data: %v", err) +// } +// +// message := fmt.Sprintf("\x19\x01%s", string(hashPermitTransferFrom(permit, permit2Address, chainId, witness))) +// +// hash := crypto.Keccak256Hash([]byte(message)) +// signature, err := crypto.Sign(hash.Bytes(), w.privateKey) +// if err != nil { +// return nil, fmt.Errorf("failed to sign permit: %v", err) +// } +// +// return signature, nil +//} diff --git a/internal/web3-provider/permit2_test.go b/internal/web3-provider/permit2_test.go index d6a7289b..ff16111d 100644 --- a/internal/web3-provider/permit2_test.go +++ b/internal/web3-provider/permit2_test.go @@ -1,39 +1,32 @@ package web3_provider -import ( - "bytes" - "testing" - - "github.com/ethereum/go-ethereum/common" -) - -func TestHashPermitTransferFrom_MaxValues(t *testing.T) { - permit := PermitTransferFrom{ - Permitted: TokenPermissions{ - Token: "0x0000000000000000000000000000000000000000", - Amount: MaxSignatureTransferAmount, - }, - Spender: "0x0000000000000000000000000000000000000000", - Nonce: MaxUnorderedNonce, - Deadline: MaxSigDeadline, - } - permit2Address := "0x0000000000000000000000000000000000000000" - chainId := 1 - - // Call the hash function - hash := hashPermitTransferFrom(permit, permit2Address, chainId, nil) - - // Define the expected hash value - expectedHash := "0x99e8cd5cd187c1dcb3c9cb41664cb12c1a3a76143d21b16f7880f4839d2b2ad4" - - // Convert expectedHash to bytes for comparison - expectedHashBytes := common.Hex2Bytes(expectedHash) - - // Convert hash to bytes for comparison - hashBytes := common.Hex2Bytes(hash) - - // Compare the hashes - if !bytes.Equal(hashBytes, expectedHashBytes) { - t.Errorf("Expected hash %s, but got %s", expectedHash, hash) - } -} +//func TestHashPermitTransferFrom_MaxValues(t *testing.T) { +// permit := PermitTransferFrom{ +// Permitted: TokenPermissions{ +// Token: "0x0000000000000000000000000000000000000000", +// Amount: MaxSignatureTransferAmount, +// }, +// Spender: "0x0000000000000000000000000000000000000000", +// Nonce: MaxUnorderedNonce, +// Deadline: MaxSigDeadline, +// } +// permit2Address := "0x0000000000000000000000000000000000000000" +// chainId := 1 +// +// // Call the hash function +// hash := hashPermitTransferFrom(permit, permit2Address, chainId, nil) +// +// // Define the expected hash value +// expectedHash := "0x99e8cd5cd187c1dcb3c9cb41664cb12c1a3a76143d21b16f7880f4839d2b2ad4" +// +// // Convert expectedHash to bytes for comparison +// expectedHashBytes := common.Hex2Bytes(expectedHash) +// +// // Convert hash to bytes for comparison +// hashBytes := common.Hex2Bytes(hash) +// +// // Compare the hashes +// if !bytes.Equal(hashBytes, expectedHashBytes) { +// t.Errorf("Expected hash %s, but got %s", expectedHash, hash) +// } +//} diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go new file mode 100644 index 00000000..2c59ec51 --- /dev/null +++ b/internal/web3-provider/permit2allowance.go @@ -0,0 +1,129 @@ +package web3_provider + +import ( + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +type PermitDetails struct { + Token common.Address + Amount *big.Int + Expiration *big.Int + Nonce *big.Int +} + +type PermitSingle struct { + Details PermitDetails + Spender common.Address + SigDeadline *big.Int +} + +type PermitSingleData struct { + Domain apitypes.TypedDataDomain + Types apitypes.Types + Values apitypes.TypedDataMessage +} + +var PERMIT_DETAILS = []apitypes.Type{ + {Name: "token", Type: "address"}, + {Name: "amount", Type: "uint160"}, + {Name: "expiration", Type: "uint48"}, + {Name: "nonce", Type: "uint48"}, +} + +var PERMIT_TYPES = apitypes.Types{ + "PermitSingle": { + {Name: "details", Type: "PermitDetails"}, + {Name: "spender", Type: "address"}, + {Name: "sigDeadline", Type: "uint256"}, + }, + "PermitDetails": PERMIT_DETAILS, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, +} + +func getPermitData(permit PermitSingle, permit2Address common.Address, chainId *big.Int) PermitSingleData { + validatePermitDetails(permit.Details) + + domain := apitypes.TypedDataDomain{ + Name: "Permit2", + Version: "1", + ChainId: math.NewHexOrDecimal256(chainId.Int64()), + VerifyingContract: permit2Address.Hex(), + } + + values := map[string]interface{}{ + "details": map[string]interface{}{ + "token": permit.Details.Token.Hex(), + "amount": permit.Details.Amount, + "expiration": permit.Details.Expiration, + "nonce": permit.Details.Nonce, + }, + "spender": permit.Spender.Hex(), + "sigDeadline": permit.SigDeadline, + } + + return PermitSingleData{ + Domain: domain, + Types: PERMIT_TYPES, + Values: values, + } +} + +func validatePermitDetails(details PermitDetails) { + maxUint48 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + maxUint160 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) + + if details.Amount.Cmp(maxUint160) > 0 { + log.Fatal("AMOUNT_OUT_OF_RANGE") + } + if details.Expiration.Cmp(maxUint48) > 0 { + log.Fatal("EXPIRATION_OUT_OF_RANGE") + } + if details.Nonce.Cmp(maxUint48) > 0 { + log.Fatal("NONCE_OUT_OF_RANGE") + } +} + +func hashPermitSingle(permit PermitSingle, permit2Address common.Address, chainId *big.Int) (string, error) { + permitData := getPermitData(permit, permit2Address, chainId) + + typedData := apitypes.TypedData{ + Types: permitData.Types, + PrimaryType: "PermitSingle", + Domain: permitData.Domain, + Message: permitData.Values, + } + + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return "", err + } + + domainSeparator, err := typedData.HashStruct("EIP712Domain", map[string]interface{}{ + "name": typedData.Domain.Name, + "version": typedData.Domain.Version, + "chainId": typedData.Domain.ChainId, + "verifyingContract": typedData.Domain.VerifyingContract, + }) + if err != nil { + return "", err + } + + digest := crypto.Keccak256Hash( + []byte("\x19\x01"), + domainSeparator, + typedDataHash, + ) + + return digest.Hex(), nil +} diff --git a/internal/web3-provider/permit2allowance_test.go b/internal/web3-provider/permit2allowance_test.go new file mode 100644 index 00000000..61e3580d --- /dev/null +++ b/internal/web3-provider/permit2allowance_test.go @@ -0,0 +1,38 @@ +package web3_provider + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +// Define the maximum values +var ( + MaxAllowanceTransferAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) + MaxAllowanceExpiration = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + MaxOrderedNonce = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + MaxSigDeadline = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) +) + +func TestMaxValuesPass(t *testing.T) { + permitDetails := PermitDetails{ + Token: common.HexToAddress("0x0000000000000000000000000000000000000000"), + Amount: MaxAllowanceTransferAmount, + Expiration: MaxAllowanceExpiration, + Nonce: MaxOrderedNonce, + } + + permit := PermitSingle{ + Details: permitDetails, + Spender: common.HexToAddress("0x0000000000000000000000000000000000000000"), + SigDeadline: MaxSigDeadline, + } + + permit2Address := common.HexToAddress("0x0000000000000000000000000000000000000000") + chainId := big.NewInt(1) // For example, 1 for Ethereum mainnet + + _, err := hashPermitSingle(permit, permit2Address, chainId) + assert.NoError(t, err) +} From a7b8256b3a56db3f0cfb991ec9fbbc4f2715c213 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:45:05 +0200 Subject: [PATCH 13/18] chore: some more changes of allowance logic --- internal/web3-provider/permit2allowance.go | 58 +++++++++---------- .../web3-provider/permit2allowance_test.go | 3 +- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go index 2c59ec51..47a3023e 100644 --- a/internal/web3-provider/permit2allowance.go +++ b/internal/web3-provider/permit2allowance.go @@ -1,12 +1,12 @@ package web3_provider import ( - "log" + "errors" + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) @@ -51,8 +51,11 @@ var PERMIT_TYPES = apitypes.Types{ }, } -func getPermitData(permit PermitSingle, permit2Address common.Address, chainId *big.Int) PermitSingleData { - validatePermitDetails(permit.Details) +func getPermitData(permit PermitSingle, permit2Address common.Address, chainId *big.Int) (PermitSingleData, error) { + err := validatePermitDetails(permit.Details) + if err != nil { + return PermitSingleData{}, err + } domain := apitypes.TypedDataDomain{ Name: "Permit2", @@ -68,35 +71,42 @@ func getPermitData(permit PermitSingle, permit2Address common.Address, chainId * "expiration": permit.Details.Expiration, "nonce": permit.Details.Nonce, }, - "spender": permit.Spender.Hex(), - "sigDeadline": permit.SigDeadline, + "spender": permit.Spender.Hex(), + "sigDeadline": permit.SigDeadline, + "name": domain.Name, + "version": domain.Version, + "chainId": domain.ChainId, + "verifyingContract": domain.VerifyingContract, } return PermitSingleData{ Domain: domain, Types: PERMIT_TYPES, Values: values, - } + }, nil } -func validatePermitDetails(details PermitDetails) { +func validatePermitDetails(details PermitDetails) error { maxUint48 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) maxUint160 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) if details.Amount.Cmp(maxUint160) > 0 { - log.Fatal("AMOUNT_OUT_OF_RANGE") + return errors.New("AMOUNT_OUT_OF_RANGE") } if details.Expiration.Cmp(maxUint48) > 0 { - log.Fatal("EXPIRATION_OUT_OF_RANGE") + return errors.New("EXPIRATION_OUT_OF_RANGE") } if details.Nonce.Cmp(maxUint48) > 0 { - log.Fatal("NONCE_OUT_OF_RANGE") + return errors.New("NONCE_OUT_OF_RANGE") } + return nil } func hashPermitSingle(permit PermitSingle, permit2Address common.Address, chainId *big.Int) (string, error) { - permitData := getPermitData(permit, permit2Address, chainId) - + permitData, err := getPermitData(permit, permit2Address, chainId) + if err != nil { + return "", err + } typedData := apitypes.TypedData{ Types: permitData.Types, PrimaryType: "PermitSingle", @@ -104,26 +114,10 @@ func hashPermitSingle(permit PermitSingle, permit2Address common.Address, chainI Message: permitData.Values, } - typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + challengeHash, _, err := apitypes.TypedDataAndHash(typedData) if err != nil { - return "", err - } - - domainSeparator, err := typedData.HashStruct("EIP712Domain", map[string]interface{}{ - "name": typedData.Domain.Name, - "version": typedData.Domain.Version, - "chainId": typedData.Domain.ChainId, - "verifyingContract": typedData.Domain.VerifyingContract, - }) - if err != nil { - return "", err + return "", fmt.Errorf("error using TypedDataAndHash: %v", err) } - digest := crypto.Keccak256Hash( - []byte("\x19\x01"), - domainSeparator, - typedDataHash, - ) - - return digest.Hex(), nil + return fmt.Sprintf("%x", challengeHash), nil } diff --git a/internal/web3-provider/permit2allowance_test.go b/internal/web3-provider/permit2allowance_test.go index 61e3580d..c008a9c5 100644 --- a/internal/web3-provider/permit2allowance_test.go +++ b/internal/web3-provider/permit2allowance_test.go @@ -33,6 +33,7 @@ func TestMaxValuesPass(t *testing.T) { permit2Address := common.HexToAddress("0x0000000000000000000000000000000000000000") chainId := big.NewInt(1) // For example, 1 for Ethereum mainnet - _, err := hashPermitSingle(permit, permit2Address, chainId) + hash, err := hashPermitSingle(permit, permit2Address, chainId) assert.NoError(t, err) + assert.Equal(t, "0x7c7685afe45d5d39b6279f05214f9bb9aa275f541f950d0a97d0c18aa43158c8", hash) } From f4f6728654da89add89e4b6f2c3ce7db15de4b39 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:46:34 +0200 Subject: [PATCH 14/18] fix: make allowance hash works --- internal/web3-provider/permit2allowance.go | 238 +++++++++++++----- .../web3-provider/permit2allowance_test.go | 37 ++- 2 files changed, 200 insertions(+), 75 deletions(-) diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go index 47a3023e..278c19a6 100644 --- a/internal/web3-provider/permit2allowance.go +++ b/internal/web3-provider/permit2allowance.go @@ -1,109 +1,235 @@ package web3_provider import ( - "errors" + "bytes" "fmt" "math/big" + "reflect" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) -type PermitDetails struct { - Token common.Address - Amount *big.Int - Expiration *big.Int - Nonce *big.Int +type TypedDataField struct { + Name string `json:"name"` + Type string `json:"type"` } -type PermitSingle struct { - Details PermitDetails - Spender common.Address - SigDeadline *big.Int +type TypedDataDomain struct { + Name string `json:"name"` + ChainId int `json:"chainId"` + VerifyingContract string `json:"verifyingContract"` +} + +type PermitDetails struct { + Token string `json:"token"` + Amount string `json:"amount"` + Expiration string `json:"expiration"` + Nonce string `json:"nonce"` } -type PermitSingleData struct { - Domain apitypes.TypedDataDomain - Types apitypes.Types - Values apitypes.TypedDataMessage +type PermitSingle struct { + Details PermitDetails `json:"details"` + Spender string `json:"spender"` + SigDeadline string `json:"sigDeadline"` } -var PERMIT_DETAILS = []apitypes.Type{ - {Name: "token", Type: "address"}, - {Name: "amount", Type: "uint160"}, - {Name: "expiration", Type: "uint48"}, - {Name: "nonce", Type: "uint48"}, +type TypedData struct { + Domain apitypes.TypedDataDomain `json:"domain"` + Types map[string][]apitypes.Type `json:"types"` + Values apitypes.TypedDataMessage `json:"values"` } -var PERMIT_TYPES = apitypes.Types{ +var PERMIT_TYPES = map[string][]apitypes.Type{ + "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": PERMIT_DETAILS, - "EIP712Domain": []apitypes.Type{ - {Name: "name", Type: "string"}, - {Name: "version", Type: "string"}, - {Name: "chainId", Type: "uint256"}, - {Name: "verifyingContract", Type: "address"}, + "PermitDetails": { + {Name: "token", Type: "address"}, + {Name: "amount", Type: "uint160"}, + {Name: "expiration", Type: "uint48"}, + {Name: "nonce", Type: "uint48"}, }, } -func getPermitData(permit PermitSingle, permit2Address common.Address, chainId *big.Int) (PermitSingleData, error) { - err := validatePermitDetails(permit.Details) - if err != nil { - return PermitSingleData{}, err - } +var ( + MaxAllowanceTransferAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) + MaxAllowanceExpiration = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + MaxOrderedNonce = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + MaxSigDeadline = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) +) - domain := apitypes.TypedDataDomain{ - Name: "Permit2", - Version: "1", - ChainId: math.NewHexOrDecimal256(chainId.Int64()), - VerifyingContract: permit2Address.Hex(), +func GetPermitData(permit PermitSingle, permit2Address common.Address, chainId int) (TypedData, error) { + sigDeadline, ok := new(big.Int).SetString(permit.SigDeadline, 10) + if !ok { + return TypedData{}, fmt.Errorf("invalid sigDeadline") + } + if sigDeadline.Cmp(MaxSigDeadline) > 0 { + return TypedData{}, fmt.Errorf("SIG_DEADLINE_OUT_OF_RANGE") } + err := validatePermitDetails(permit.Details) + if err != nil { + return TypedData{}, err + } values := map[string]interface{}{ "details": map[string]interface{}{ - "token": permit.Details.Token.Hex(), + "token": permit.Details.Token, "amount": permit.Details.Amount, "expiration": permit.Details.Expiration, "nonce": permit.Details.Nonce, }, - "spender": permit.Spender.Hex(), - "sigDeadline": permit.SigDeadline, - "name": domain.Name, - "version": domain.Version, - "chainId": domain.ChainId, - "verifyingContract": domain.VerifyingContract, + "spender": permit.Spender, + "sigDeadline": permit.SigDeadline, } - return PermitSingleData{ - Domain: domain, + return TypedData{ + Domain: apitypes.TypedDataDomain{ + Name: "Permit2", + ChainId: math.NewHexOrDecimal256(int64(chainId)), + VerifyingContract: permit2Address.Hex(), + }, Types: PERMIT_TYPES, Values: values, }, nil } func validatePermitDetails(details PermitDetails) error { - maxUint48 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) - maxUint160 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) + nonce, ok := new(big.Int).SetString(details.Nonce, 10) + if !ok { + return fmt.Errorf("invalid nonce") + } + if nonce.Cmp(MaxOrderedNonce) > 0 { + return fmt.Errorf("NONCE_OUT_OF_RANGE") + } - if details.Amount.Cmp(maxUint160) > 0 { - return errors.New("AMOUNT_OUT_OF_RANGE") + amount, ok := new(big.Int).SetString(details.Amount, 10) + if !ok { + return fmt.Errorf("invalid amount") } - if details.Expiration.Cmp(maxUint48) > 0 { - return errors.New("EXPIRATION_OUT_OF_RANGE") + if amount.Cmp(MaxAllowanceTransferAmount) > 0 { + return fmt.Errorf("AMOUNT_OUT_OF_RANGE") } - if details.Nonce.Cmp(maxUint48) > 0 { - return errors.New("NONCE_OUT_OF_RANGE") + + expiration, ok := new(big.Int).SetString(details.Expiration, 10) + if !ok { + return fmt.Errorf("invalid expiration") + } + if expiration.Cmp(MaxAllowanceExpiration) > 0 { + return fmt.Errorf("EXPIRATION_OUT_OF_RANGE") } + return nil } -func hashPermitSingle(permit PermitSingle, permit2Address common.Address, chainId *big.Int) (string, error) { - permitData, err := getPermitData(permit, permit2Address, chainId) +func hashPermitDetails(types map[string][]TypedDataField, details PermitDetails) []byte { + var buffer bytes.Buffer + + buffer.Write(crypto.Keccak256([]byte("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"))) + + value := reflect.ValueOf(details) + for _, field := range types["PermitDetails"] { + fieldName := field.Name + fieldValue := value.FieldByName(cases.Title(language.English).String(fieldName)) + + switch field.Type { + case "address": + buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) + case "uint160": + intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) + if !ok { + panic(fmt.Sprintf("Invalid uint160 value for field %s", fieldName)) + } + buffer.Write(common.LeftPadBytes(intValue.Bytes(), 20)) + case "uint48": + intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) + if !ok { + panic(fmt.Sprintf("Invalid uint48 value for field %s", fieldName)) + } + buffer.Write(common.LeftPadBytes(intValue.Bytes(), 6)) + } + } + + return crypto.Keccak256(buffer.Bytes()) +} + +func hashStruct(types map[string][]TypedDataField, values PermitSingle) []byte { + var buffer bytes.Buffer + + primaryType := "PermitSingle" + buffer.Write(crypto.Keccak256([]byte("PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)"))) + + value := reflect.ValueOf(values) + for _, field := range types[primaryType] { + fieldName := field.Name + // Manually handle the specific case for "sigDeadline" to "SigDeadline" + var fieldValue reflect.Value + if fieldName == "sigDeadline" { + fieldValue = value.FieldByName("SigDeadline") + } else { + fieldValue = value.FieldByName(cases.Title(language.English).String(fieldName)) + } + + switch field.Type { + case "address": + buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) + case "uint160": + intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) + if !ok { + panic(fmt.Sprintf("Invalid uint160 value for field %s", fieldName)) + } + buffer.Write(common.LeftPadBytes(intValue.Bytes(), 20)) + case "uint256": + intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) + if !ok { + panic(fmt.Sprintf("Invalid uint256 value for field %s", fieldName)) + } + buffer.Write(common.LeftPadBytes(intValue.Bytes(), 32)) + case "uint48": + intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) + if !ok { + panic(fmt.Sprintf("Invalid uint48 value for field %s", fieldName)) + } + buffer.Write(common.LeftPadBytes(intValue.Bytes(), 6)) + case "PermitDetails": + buffer.Write(hashPermitDetails(types, fieldValue.Interface().(PermitDetails))) + + } + } + + return crypto.Keccak256(buffer.Bytes()) +} + +func hashDomain(domain TypedDataDomain) []byte { + var buffer bytes.Buffer + + buffer.Write(crypto.Keccak256([]byte("EIP712Domain(string name,uint256 chainId,address verifyingContract)"))) + + nameHash := crypto.Keccak256([]byte(domain.Name)) + buffer.Write(nameHash) + + chainIdBytes := common.LeftPadBytes(new(big.Int).SetInt64(int64(domain.ChainId)).Bytes(), 32) + buffer.Write(chainIdBytes) + + addressBytes := common.HexToAddress(domain.VerifyingContract).Bytes() + buffer.Write(addressBytes) + + return crypto.Keccak256(buffer.Bytes()) +} + +func hashPermitData(permit PermitSingle, permit2Address common.Address, chainId int) (string, error) { + permitData, err := GetPermitData(permit, permit2Address, chainId) if err != nil { return "", err } @@ -119,5 +245,5 @@ func hashPermitSingle(permit PermitSingle, permit2Address common.Address, chainI return "", fmt.Errorf("error using TypedDataAndHash: %v", err) } - return fmt.Sprintf("%x", challengeHash), nil + return "0x" + common.Bytes2Hex(challengeHash), nil } diff --git a/internal/web3-provider/permit2allowance_test.go b/internal/web3-provider/permit2allowance_test.go index c008a9c5..bfe474f0 100644 --- a/internal/web3-provider/permit2allowance_test.go +++ b/internal/web3-provider/permit2allowance_test.go @@ -1,6 +1,7 @@ package web3_provider import ( + "fmt" "math/big" "testing" @@ -8,32 +9,30 @@ import ( "github.com/stretchr/testify/assert" ) -// Define the maximum values -var ( - MaxAllowanceTransferAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) - MaxAllowanceExpiration = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) - MaxOrderedNonce = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) - MaxSigDeadline = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) -) - func TestMaxValuesPass(t *testing.T) { - permitDetails := PermitDetails{ - Token: common.HexToAddress("0x0000000000000000000000000000000000000000"), - Amount: MaxAllowanceTransferAmount, - Expiration: MaxAllowanceExpiration, - Nonce: MaxOrderedNonce, - } permit := PermitSingle{ - Details: permitDetails, - Spender: common.HexToAddress("0x0000000000000000000000000000000000000000"), - SigDeadline: MaxSigDeadline, + Details: PermitDetails{ + Token: "0x0000000000000000000000000000000000000000", + Amount: "1461501637330902918203684832716283019655932542975", + Expiration: "281474976710655", + Nonce: "281474976710655", + }, + Spender: "0x0000000000000000000000000000000000000000", + SigDeadline: "115792089237316195423570985008687907853269984665640564039457584007913129639935", } permit2Address := common.HexToAddress("0x0000000000000000000000000000000000000000") - chainId := big.NewInt(1) // For example, 1 for Ethereum mainnet + chainId := big.NewInt(1) - hash, err := hashPermitSingle(permit, permit2Address, chainId) + _, err := GetPermitData(permit, permit2Address, int(chainId.Int64())) assert.NoError(t, err) + + hash, err := hashPermitData(permit, permit2Address, int(chainId.Int64())) + if err != nil { + fmt.Println("Error:", err) + return + } + assert.Equal(t, "0x7c7685afe45d5d39b6279f05214f9bb9aa275f541f950d0a97d0c18aa43158c8", hash) } From 9aa7863c8bebb3b33fdbc48d03f8c8e12ee75b2e Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:53:51 +0200 Subject: [PATCH 15/18] feat: refactor --- internal/web3-provider/permit2.go | 254 ------------------ internal/web3-provider/permit2_test.go | 32 --- internal/web3-provider/permit2allowance.go | 174 ++---------- .../web3-provider/permit2allowance_test.go | 6 +- 4 files changed, 29 insertions(+), 437 deletions(-) delete mode 100644 internal/web3-provider/permit2.go delete mode 100644 internal/web3-provider/permit2_test.go diff --git a/internal/web3-provider/permit2.go b/internal/web3-provider/permit2.go deleted file mode 100644 index fef0fdf0..00000000 --- a/internal/web3-provider/permit2.go +++ /dev/null @@ -1,254 +0,0 @@ -package web3_provider - -// -//import ( -// "bytes" -// "fmt" -// "math/big" -// "reflect" -// "strings" -// -// "github.com/ethereum/go-ethereum/accounts/abi" -// "github.com/ethereum/go-ethereum/common" -// "github.com/ethereum/go-ethereum/crypto" -// "golang.org/x/text/cases" -// "golang.org/x/text/language" -//) -// -//const ( -// PERMIT2_DOMAIN_NAME = "Permit2" -//) -// -//const ( -// // Update this with the ABI for your contract -// abiString = ` -//[ -// { -// "type": "function", -// "name": "permitTransferFrom", -// "inputs": [ -// { "name": "data", "type": "PermitTransferFromData" }, -// { "name": "witness", "type": "bytes" } -// ] -// } -//] -//` -//) -// -//var ( -// MaxUint256 = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) -// MaxSignatureTransferAmount = MaxUint256 -// MaxUnorderedNonce = MaxUint256 -// MaxSigDeadline = MaxUint256 -//) -// -//type TypedDataDomain struct { -// Name string -// ChainId int -// VerifyingContract string -//} -// -//type TypedDataField struct { -// Name string -// Type string -//} -// -//type TokenPermissions struct { -// Token string -// Amount *big.Int -//} -// -//type PermitTransferFrom struct { -// Permitted TokenPermissions -// Spender string -// Nonce *big.Int -// Deadline *big.Int -//} -// -//type PermitBatchTransferFrom struct { -// Permitted []TokenPermissions -// Spender string -// Nonce *big.Int -// Deadline *big.Int -//} -// -//type Witness struct { -// Witness interface{} -// WitnessTypeName string -// WitnessType map[string][]TypedDataField -//} -// -//type PermitTransferFromData struct { -// Domain TypedDataDomain -// Types map[string][]TypedDataField -// Values PermitTransferFrom -//} -// -//var TOKEN_PERMISSIONS = []TypedDataField{ -// {Name: "token", Type: "address"}, -// {Name: "amount", Type: "uint256"}, -//} -// -//var PERMIT_TRANSFER_FROM_TYPES = map[string][]TypedDataField{ -// "PermitTransferFrom": { -// {Name: "permitted", Type: "TokenPermissions"}, -// {Name: "spender", Type: "address"}, -// {Name: "nonce", Type: "uint256"}, -// {Name: "deadline", Type: "uint256"}, -// }, -// "TokenPermissions": TOKEN_PERMISSIONS, -//} -// -//func permit2Domain(permit2Address string, chainId int) TypedDataDomain { -// return TypedDataDomain{ -// Name: PERMIT2_DOMAIN_NAME, -// ChainId: chainId, -// VerifyingContract: permit2Address, -// } -//} -// -//func permitTransferFromWithWitnessType(witness Witness) map[string][]TypedDataField { -// return map[string][]TypedDataField{ -// "PermitWitnessTransferFrom": { -// {Name: "permitted", Type: "TokenPermissions"}, -// {Name: "spender", Type: "address"}, -// {Name: "nonce", Type: "uint256"}, -// {Name: "deadline", Type: "uint256"}, -// {Name: "witness", Type: witness.WitnessTypeName}, -// }, -// "TokenPermissions": TOKEN_PERMISSIONS, -// witness.WitnessTypeName: witness.WitnessType[witness.WitnessTypeName], -// } -//} -// -//func validateTokenPermissions(permissions TokenPermissions) { -// if MaxSignatureTransferAmount.Cmp(permissions.Amount) < 0 { -// panic("AMOUNT_OUT_OF_RANGE") -// } -//} -// -//func getPermitTransferFromData( -// permit PermitTransferFrom, -// permit2Address string, -// chainId int, -// witness *Witness, -//) PermitTransferFromData { -// if MaxSigDeadline.Cmp(permit.Deadline) < 0 { -// panic("SIG_DEADLINE_OUT_OF_RANGE") -// } -// if MaxUnorderedNonce.Cmp(permit.Nonce) < 0 { -// panic("NONCE_OUT_OF_RANGE") -// } -// -// validateTokenPermissions(permit.Permitted) -// -// domain := permit2Domain(permit2Address, chainId) -// var types map[string][]TypedDataField -// var values interface{} -// -// if witness != nil { -// types = permitTransferFromWithWitnessType(*witness) -// values = struct { -// PermitTransferFrom -// Witness interface{} -// }{permit, witness.Witness} -// } else { -// types = PERMIT_TRANSFER_FROM_TYPES -// values = permit -// } -// -// return PermitTransferFromData{ -// Domain: domain, -// Types: types, -// Values: values.(PermitTransferFrom), -// } -//} -// -//func hashPermitTransferFrom( -// permit PermitTransferFrom, -// permit2Address string, -// chainId int, -// witness *Witness, -//) string { -// permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) -// domainHash := hashDomain(permitData.Domain) -// structHash := hashStruct(permitData.Types, permitData.Values) -// -// finalHash := crypto.Keccak256Hash(append([]byte{0x19, 0x01}, append(domainHash, structHash...)...)).Hex() -// return finalHash -//} -// -//func hashDomain(domain TypedDataDomain) []byte { -// var buffer bytes.Buffer -// -// buffer.Write(crypto.Keccak256([]byte("EIP712Domain(string name,uint256 chainId,address verifyingContract)"))) -// -// nameHash := crypto.Keccak256([]byte(domain.Name)) -// buffer.Write(nameHash) -// -// chainIdBytes := common.LeftPadBytes(new(big.Int).SetInt64(int64(domain.ChainId)).Bytes(), 32) -// buffer.Write(chainIdBytes) -// -// addressBytes := common.HexToAddress(domain.VerifyingContract).Bytes() -// buffer.Write(addressBytes) -// -// return crypto.Keccak256(buffer.Bytes()) -//} -// -//func hashStruct(types map[string][]TypedDataField, values PermitTransferFrom) []byte { -// var buffer bytes.Buffer -// -// primaryType := "" -// for t := range types { -// primaryType = t -// break -// } -// -// buffer.Write(crypto.Keccak256([]byte(primaryType))) -// -// value := reflect.ValueOf(values) -// for _, field := range types[primaryType] { -// fieldName := field.Name -// fieldValue := value.FieldByName(cases.Title(language.English).String(fieldName)) -// -// if field.Type == "address" { -// buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) -// } else if field.Type == "uint256" { -// buffer.Write(common.LeftPadBytes(fieldValue.Interface().(*big.Int).Bytes(), 32)) -// } else if field.Type == "TokenPermissions" { -// tokenPerm := fieldValue.Interface().(TokenPermissions) -// buffer.Write(common.HexToAddress(tokenPerm.Token).Bytes()) -// buffer.Write(common.LeftPadBytes(tokenPerm.Amount.Bytes(), 32)) -// } -// } -// -// return crypto.Keccak256(buffer.Bytes()) -//} -// -//func (w Wallet) SignPermit2TransferFrom( -// permit PermitTransferFrom, -// permit2Address string, -// witness *Witness, -//) ([]byte, error) { -// chainId := int(w.chainId.Uint64()) -// permitData := getPermitTransferFromData(permit, permit2Address, chainId, witness) -// -// encodedData, err := abi.JSON(strings.NewReader(abiString)) -// if err != nil { -// return nil, fmt.Errorf("failed to parse ABI: %v", err) -// } -// _, err = encodedData.Pack("permitTransferFrom", permitData, nil) -// if err != nil { -// return nil, fmt.Errorf("failed to pack permit data: %v", err) -// } -// -// message := fmt.Sprintf("\x19\x01%s", string(hashPermitTransferFrom(permit, permit2Address, chainId, witness))) -// -// hash := crypto.Keccak256Hash([]byte(message)) -// signature, err := crypto.Sign(hash.Bytes(), w.privateKey) -// if err != nil { -// return nil, fmt.Errorf("failed to sign permit: %v", err) -// } -// -// return signature, nil -//} diff --git a/internal/web3-provider/permit2_test.go b/internal/web3-provider/permit2_test.go deleted file mode 100644 index ff16111d..00000000 --- a/internal/web3-provider/permit2_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package web3_provider - -//func TestHashPermitTransferFrom_MaxValues(t *testing.T) { -// permit := PermitTransferFrom{ -// Permitted: TokenPermissions{ -// Token: "0x0000000000000000000000000000000000000000", -// Amount: MaxSignatureTransferAmount, -// }, -// Spender: "0x0000000000000000000000000000000000000000", -// Nonce: MaxUnorderedNonce, -// Deadline: MaxSigDeadline, -// } -// permit2Address := "0x0000000000000000000000000000000000000000" -// chainId := 1 -// -// // Call the hash function -// hash := hashPermitTransferFrom(permit, permit2Address, chainId, nil) -// -// // Define the expected hash value -// expectedHash := "0x99e8cd5cd187c1dcb3c9cb41664cb12c1a3a76143d21b16f7880f4839d2b2ad4" -// -// // Convert expectedHash to bytes for comparison -// expectedHashBytes := common.Hex2Bytes(expectedHash) -// -// // Convert hash to bytes for comparison -// hashBytes := common.Hex2Bytes(hash) -// -// // Compare the hashes -// if !bytes.Equal(hashBytes, expectedHashBytes) { -// t.Errorf("Expected hash %s, but got %s", expectedHash, hash) -// } -//} diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go index 278c19a6..e9b837dc 100644 --- a/internal/web3-provider/permit2allowance.go +++ b/internal/web3-provider/permit2allowance.go @@ -1,47 +1,25 @@ package web3_provider import ( - "bytes" "fmt" "math/big" - "reflect" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) -type TypedDataField struct { - Name string `json:"name"` - Type string `json:"type"` -} - -type TypedDataDomain struct { - Name string `json:"name"` - ChainId int `json:"chainId"` - VerifyingContract string `json:"verifyingContract"` -} - -type PermitDetails struct { +type AllowancePermitDetails struct { Token string `json:"token"` Amount string `json:"amount"` Expiration string `json:"expiration"` Nonce string `json:"nonce"` } -type PermitSingle struct { - Details PermitDetails `json:"details"` - Spender string `json:"spender"` - SigDeadline string `json:"sigDeadline"` -} - -type TypedData struct { - Domain apitypes.TypedDataDomain `json:"domain"` - Types map[string][]apitypes.Type `json:"types"` - Values apitypes.TypedDataMessage `json:"values"` +type AllowancePermitSingle struct { + Details AllowancePermitDetails `json:"details"` + Spender string `json:"spender"` + SigDeadline string `json:"sigDeadline"` } var PERMIT_TYPES = map[string][]apitypes.Type{ @@ -70,21 +48,13 @@ var ( MaxSigDeadline = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) ) -func GetPermitData(permit PermitSingle, permit2Address common.Address, chainId int) (TypedData, error) { - sigDeadline, ok := new(big.Int).SetString(permit.SigDeadline, 10) - if !ok { - return TypedData{}, fmt.Errorf("invalid sigDeadline") - } - if sigDeadline.Cmp(MaxSigDeadline) > 0 { - return TypedData{}, fmt.Errorf("SIG_DEADLINE_OUT_OF_RANGE") - } - - err := validatePermitDetails(permit.Details) +func GetTypedDataAllowancePermitSingle(permit AllowancePermitSingle, permit2Address common.Address, chainId int) (apitypes.TypedData, error) { + err := validatePermit(permit) if err != nil { - return TypedData{}, err + return apitypes.TypedData{}, err } - values := map[string]interface{}{ - "details": map[string]interface{}{ + values := apitypes.TypedDataMessage{ + "details": apitypes.TypedDataMessage{ "token": permit.Details.Token, "amount": permit.Details.Amount, "expiration": permit.Details.Expiration, @@ -94,19 +64,20 @@ func GetPermitData(permit PermitSingle, permit2Address common.Address, chainId i "sigDeadline": permit.SigDeadline, } - return TypedData{ + return apitypes.TypedData{ Domain: apitypes.TypedDataDomain{ Name: "Permit2", ChainId: math.NewHexOrDecimal256(int64(chainId)), VerifyingContract: permit2Address.Hex(), }, - Types: PERMIT_TYPES, - Values: values, + Types: PERMIT_TYPES, + Message: values, + PrimaryType: "PermitSingle", }, nil } -func validatePermitDetails(details PermitDetails) error { - nonce, ok := new(big.Int).SetString(details.Nonce, 10) +func validatePermit(permit AllowancePermitSingle) error { + nonce, ok := new(big.Int).SetString(permit.Details.Nonce, 10) if !ok { return fmt.Errorf("invalid nonce") } @@ -114,7 +85,7 @@ func validatePermitDetails(details PermitDetails) error { return fmt.Errorf("NONCE_OUT_OF_RANGE") } - amount, ok := new(big.Int).SetString(details.Amount, 10) + amount, ok := new(big.Int).SetString(permit.Details.Amount, 10) if !ok { return fmt.Errorf("invalid amount") } @@ -122,7 +93,7 @@ func validatePermitDetails(details PermitDetails) error { return fmt.Errorf("AMOUNT_OUT_OF_RANGE") } - expiration, ok := new(big.Int).SetString(details.Expiration, 10) + expiration, ok := new(big.Int).SetString(permit.Details.Expiration, 10) if !ok { return fmt.Errorf("invalid expiration") } @@ -130,115 +101,22 @@ func validatePermitDetails(details PermitDetails) error { return fmt.Errorf("EXPIRATION_OUT_OF_RANGE") } - return nil -} - -func hashPermitDetails(types map[string][]TypedDataField, details PermitDetails) []byte { - var buffer bytes.Buffer - - buffer.Write(crypto.Keccak256([]byte("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"))) - - value := reflect.ValueOf(details) - for _, field := range types["PermitDetails"] { - fieldName := field.Name - fieldValue := value.FieldByName(cases.Title(language.English).String(fieldName)) - - switch field.Type { - case "address": - buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) - case "uint160": - intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) - if !ok { - panic(fmt.Sprintf("Invalid uint160 value for field %s", fieldName)) - } - buffer.Write(common.LeftPadBytes(intValue.Bytes(), 20)) - case "uint48": - intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) - if !ok { - panic(fmt.Sprintf("Invalid uint48 value for field %s", fieldName)) - } - buffer.Write(common.LeftPadBytes(intValue.Bytes(), 6)) - } + sigDeadline, ok := new(big.Int).SetString(permit.SigDeadline, 10) + if !ok { + return fmt.Errorf("invalid sigDeadline") } - - return crypto.Keccak256(buffer.Bytes()) -} - -func hashStruct(types map[string][]TypedDataField, values PermitSingle) []byte { - var buffer bytes.Buffer - - primaryType := "PermitSingle" - buffer.Write(crypto.Keccak256([]byte("PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)"))) - - value := reflect.ValueOf(values) - for _, field := range types[primaryType] { - fieldName := field.Name - // Manually handle the specific case for "sigDeadline" to "SigDeadline" - var fieldValue reflect.Value - if fieldName == "sigDeadline" { - fieldValue = value.FieldByName("SigDeadline") - } else { - fieldValue = value.FieldByName(cases.Title(language.English).String(fieldName)) - } - - switch field.Type { - case "address": - buffer.Write(common.HexToAddress(fieldValue.String()).Bytes()) - case "uint160": - intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) - if !ok { - panic(fmt.Sprintf("Invalid uint160 value for field %s", fieldName)) - } - buffer.Write(common.LeftPadBytes(intValue.Bytes(), 20)) - case "uint256": - intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) - if !ok { - panic(fmt.Sprintf("Invalid uint256 value for field %s", fieldName)) - } - buffer.Write(common.LeftPadBytes(intValue.Bytes(), 32)) - case "uint48": - intValue, ok := new(big.Int).SetString(fieldValue.String(), 10) - if !ok { - panic(fmt.Sprintf("Invalid uint48 value for field %s", fieldName)) - } - buffer.Write(common.LeftPadBytes(intValue.Bytes(), 6)) - case "PermitDetails": - buffer.Write(hashPermitDetails(types, fieldValue.Interface().(PermitDetails))) - - } + if sigDeadline.Cmp(MaxSigDeadline) > 0 { + return fmt.Errorf("SIG_DEADLINE_OUT_OF_RANGE") } - return crypto.Keccak256(buffer.Bytes()) -} - -func hashDomain(domain TypedDataDomain) []byte { - var buffer bytes.Buffer - - buffer.Write(crypto.Keccak256([]byte("EIP712Domain(string name,uint256 chainId,address verifyingContract)"))) - - nameHash := crypto.Keccak256([]byte(domain.Name)) - buffer.Write(nameHash) - - chainIdBytes := common.LeftPadBytes(new(big.Int).SetInt64(int64(domain.ChainId)).Bytes(), 32) - buffer.Write(chainIdBytes) - - addressBytes := common.HexToAddress(domain.VerifyingContract).Bytes() - buffer.Write(addressBytes) - - return crypto.Keccak256(buffer.Bytes()) + return nil } -func hashPermitData(permit PermitSingle, permit2Address common.Address, chainId int) (string, error) { - permitData, err := GetPermitData(permit, permit2Address, chainId) +func hashPermitData(permit AllowancePermitSingle, permit2Address common.Address, chainId int) (string, error) { + typedData, err := GetTypedDataAllowancePermitSingle(permit, permit2Address, chainId) if err != nil { return "", err } - typedData := apitypes.TypedData{ - Types: permitData.Types, - PrimaryType: "PermitSingle", - Domain: permitData.Domain, - Message: permitData.Values, - } challengeHash, _, err := apitypes.TypedDataAndHash(typedData) if err != nil { diff --git a/internal/web3-provider/permit2allowance_test.go b/internal/web3-provider/permit2allowance_test.go index bfe474f0..7bc58776 100644 --- a/internal/web3-provider/permit2allowance_test.go +++ b/internal/web3-provider/permit2allowance_test.go @@ -11,8 +11,8 @@ import ( func TestMaxValuesPass(t *testing.T) { - permit := PermitSingle{ - Details: PermitDetails{ + permit := AllowancePermitSingle{ + Details: AllowancePermitDetails{ Token: "0x0000000000000000000000000000000000000000", Amount: "1461501637330902918203684832716283019655932542975", Expiration: "281474976710655", @@ -25,7 +25,7 @@ func TestMaxValuesPass(t *testing.T) { permit2Address := common.HexToAddress("0x0000000000000000000000000000000000000000") chainId := big.NewInt(1) - _, err := GetPermitData(permit, permit2Address, int(chainId.Int64())) + _, err := GetTypedDataAllowancePermitSingle(permit, permit2Address, int(chainId.Int64())) assert.NoError(t, err) hash, err := hashPermitData(permit, permit2Address, int(chainId.Int64())) From d8a3661d90a7d2d818ab4ce73311235941084d36 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:05:24 +0200 Subject: [PATCH 16/18] chore: more cleanup --- internal/web3-provider/permit2allowance.go | 61 +++++++++---------- .../web3-provider/permit2allowance_test.go | 2 +- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go index e9b837dc..3b89ac15 100644 --- a/internal/web3-provider/permit2allowance.go +++ b/internal/web3-provider/permit2allowance.go @@ -22,25 +22,6 @@ type AllowancePermitSingle struct { SigDeadline string `json:"sigDeadline"` } -var PERMIT_TYPES = map[string][]apitypes.Type{ - "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"}, - }, -} - var ( MaxAllowanceTransferAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) MaxAllowanceExpiration = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) @@ -53,16 +34,6 @@ func GetTypedDataAllowancePermitSingle(permit AllowancePermitSingle, permit2Addr if err != nil { return apitypes.TypedData{}, err } - values := apitypes.TypedDataMessage{ - "details": apitypes.TypedDataMessage{ - "token": permit.Details.Token, - "amount": permit.Details.Amount, - "expiration": permit.Details.Expiration, - "nonce": permit.Details.Nonce, - }, - "spender": permit.Spender, - "sigDeadline": permit.SigDeadline, - } return apitypes.TypedData{ Domain: apitypes.TypedDataDomain{ @@ -70,8 +41,34 @@ func GetTypedDataAllowancePermitSingle(permit AllowancePermitSingle, permit2Addr ChainId: math.NewHexOrDecimal256(int64(chainId)), VerifyingContract: permit2Address.Hex(), }, - Types: PERMIT_TYPES, - Message: values, + Types: map[string][]apitypes.Type{ + "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"}, + }, + }, + Message: apitypes.TypedDataMessage{ + "details": apitypes.TypedDataMessage{ + "token": permit.Details.Token, + "amount": permit.Details.Amount, + "expiration": permit.Details.Expiration, + "nonce": permit.Details.Nonce, + }, + "spender": permit.Spender, + "sigDeadline": permit.SigDeadline, + }, PrimaryType: "PermitSingle", }, nil } @@ -112,7 +109,7 @@ func validatePermit(permit AllowancePermitSingle) error { return nil } -func hashPermitData(permit AllowancePermitSingle, permit2Address common.Address, chainId int) (string, error) { +func AllowancePermitSingleTypedDataHash(permit AllowancePermitSingle, permit2Address common.Address, chainId int) (string, error) { typedData, err := GetTypedDataAllowancePermitSingle(permit, permit2Address, chainId) if err != nil { return "", err diff --git a/internal/web3-provider/permit2allowance_test.go b/internal/web3-provider/permit2allowance_test.go index 7bc58776..50a8fac6 100644 --- a/internal/web3-provider/permit2allowance_test.go +++ b/internal/web3-provider/permit2allowance_test.go @@ -28,7 +28,7 @@ func TestMaxValuesPass(t *testing.T) { _, err := GetTypedDataAllowancePermitSingle(permit, permit2Address, int(chainId.Int64())) assert.NoError(t, err) - hash, err := hashPermitData(permit, permit2Address, int(chainId.Int64())) + hash, err := AllowancePermitSingleTypedDataHash(permit, permit2Address, int(chainId.Int64())) if err != nil { fmt.Println("Error:", err) return From 1871b658cf28527159a480a0f14fc99751466b4a Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:32:19 +0200 Subject: [PATCH 17/18] feat: add fetch-nonce logic --- internal/web3-provider/permit2allowance.go | 67 ++++++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go index 3b89ac15..c337a7c9 100644 --- a/internal/web3-provider/permit2allowance.go +++ b/internal/web3-provider/permit2allowance.go @@ -1,14 +1,25 @@ package web3_provider import ( + "context" "fmt" "math/big" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) +type AllowancePermitParams struct { + Token string `json:"token"` + Amount string `json:"amount"` + Expiration string `json:"expiration"` + Spender string `json:"spender"` + SigDeadline string `json:"sigDeadline"` + Permit2Address string +} + type AllowancePermitDetails struct { Token string `json:"token"` Amount string `json:"amount"` @@ -22,12 +33,49 @@ type AllowancePermitSingle struct { SigDeadline string `json:"sigDeadline"` } -var ( - MaxAllowanceTransferAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) - MaxAllowanceExpiration = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) - MaxOrderedNonce = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) - MaxSigDeadline = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) -) +func (w Wallet) GetAllowancePermitSingle(ctx context.Context, params AllowancePermitParams) (apitypes.TypedData, error) { + callData, err := w.erc20ABI.Pack("allowance", w.address.Hex(), params.Token, params.Spender) + if err != nil { + return apitypes.TypedData{}, fmt.Errorf("failed to pack allowance call data: %v", err) + } + + permitAddress := common.HexToAddress(params.Permit2Address) + + msg := ethereum.CallMsg{ + To: &permitAddress, + Data: callData, + } + + result, err := w.ethClient.CallContract(ctx, msg, nil) + if err != nil { + return apitypes.TypedData{}, fmt.Errorf("failed to call contract: %v", err) + } + + var nonce *big.Int + err = w.erc20ABI.UnpackIntoInterface(&nonce, "nonce", result) + if err != nil { + return apitypes.TypedData{}, fmt.Errorf("failed to unpack result: %v", err) + } + + // Construct the permit details + d := AllowancePermitSingle{ + Details: AllowancePermitDetails{ + Token: params.Token, + Amount: params.Amount, + Expiration: params.Expiration, + Nonce: nonce.String(), + }, + Spender: params.Spender, + SigDeadline: params.SigDeadline, + } + + permit, err := GetTypedDataAllowancePermitSingle(d, permitAddress, int(w.chainId.Int64())) + if err != nil { + return apitypes.TypedData{}, fmt.Errorf("failed to generate permit: %v", err) + } + + return permit, nil +} func GetTypedDataAllowancePermitSingle(permit AllowancePermitSingle, permit2Address common.Address, chainId int) (apitypes.TypedData, error) { err := validatePermit(permit) @@ -74,6 +122,13 @@ func GetTypedDataAllowancePermitSingle(permit AllowancePermitSingle, permit2Addr } func validatePermit(permit AllowancePermitSingle) error { + var ( + MaxAllowanceTransferAmount = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) + MaxAllowanceExpiration = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + MaxOrderedNonce = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 48), big.NewInt(1)) + MaxSigDeadline = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)) + ) + nonce, ok := new(big.Int).SetString(permit.Details.Nonce, 10) if !ok { return fmt.Errorf("invalid nonce") From 2839e0cc3e2491ae21d1fb74a5532d347ba13718 Mon Sep 17 00:00:00 2001 From: Nick Kozlov <22479658+EnoRage@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:51:36 +0200 Subject: [PATCH 18/18] feat: start working on final permit2 func for sign + call generation --- internal/web3-provider/permit2allowance.go | 28 +++++++++++++++++++++- internal/web3-provider/provider.go | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/internal/web3-provider/permit2allowance.go b/internal/web3-provider/permit2allowance.go index c337a7c9..5a86a287 100644 --- a/internal/web3-provider/permit2allowance.go +++ b/internal/web3-provider/permit2allowance.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" ) @@ -34,7 +35,7 @@ type AllowancePermitSingle struct { } func (w Wallet) GetAllowancePermitSingle(ctx context.Context, params AllowancePermitParams) (apitypes.TypedData, error) { - callData, err := w.erc20ABI.Pack("allowance", w.address.Hex(), params.Token, params.Spender) + callData, err := w.erc20ABI.Pack("nonce", w.address.Hex(), params.Token, params.Spender) if err != nil { return apitypes.TypedData{}, fmt.Errorf("failed to pack allowance call data: %v", err) } @@ -177,3 +178,28 @@ func AllowancePermitSingleTypedDataHash(permit AllowancePermitSingle, permit2Add return "0x" + common.Bytes2Hex(challengeHash), nil } + +func (w Wallet) SignPermit2AllowanceAndPackToContract(permit AllowancePermitSingle) (string, error) { + challengeHash, err := AllowancePermitSingleTypedDataHash(permit, *w.address, int(w.chainId.Int64())) + if err != nil { + return "", err + } + challengeHashWithoutPrefix := challengeHash[2:] + challengeHashWithoutPrefixRaw := common.Hex2Bytes(challengeHashWithoutPrefix) + + signature, err := crypto.Sign(challengeHashWithoutPrefixRaw, w.privateKey) + if err != nil { + return "", fmt.Errorf("error signing challenge hash: %v", err) + } + signature[64] += 27 // Adjust the `v` value + + // Convert createPermitSignature to hex string + signatureHex := fmt.Sprintf("%x", signature) + + // Step 5: Encode the permit data with the signature + permitCall, err := w.permit2ABI.Pack("permit", w.address, permit, convertSignatureToVRSString(signatureHex)) + if err != nil { + return "", fmt.Errorf("failed to encode function data: %v", err) + } + return padStringWithZeroes(common.Bytes2Hex(permitCall)), nil +} diff --git a/internal/web3-provider/provider.go b/internal/web3-provider/provider.go index 745c30a6..fd393f61 100644 --- a/internal/web3-provider/provider.go +++ b/internal/web3-provider/provider.go @@ -23,6 +23,7 @@ type Wallet struct { chainId *big.Int erc20ABI *abi.ABI seriesNonceManagerABI *abi.ABI + permit2ABI *abi.ABI } func DefaultWalletProvider(pk string, nodeURL string, chainId uint64) (*Wallet, error) {