Skip to content

Commit

Permalink
Permit1 support in Orderbook. Tenderly custom balances and refactor. …
Browse files Browse the repository at this point in the history
…Token versions used in permit. Approval fallback for failed Permits.
  • Loading branch information
Tanz0rz authored Feb 22, 2024
1 parent cb773a2 commit f2e79e5
Show file tree
Hide file tree
Showing 21 changed files with 612 additions and 262 deletions.
7 changes: 1 addition & 6 deletions golang/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ type service struct {
type Config struct {
DevPortalApiKey string
Web3HttpProviders []Web3ProviderConfig
TenderlyKey string
}

type Web3ProviderConfig struct {
Expand Down Expand Up @@ -65,8 +64,6 @@ type Client struct {
// The API key to use for authentication
ApiKey string
// When present, tests will simulate swaps on Tenderly
TenderlyKey string
// Once a transaction has been sent by the SDK, the nonce is tracked internally to avoid RPC desync issues on subsequent transactions
NonceCache map[string]uint64
// A struct that will contain a reference to this client. Used to separate each API into a unique namespace to aid in method discovery
common service
Expand All @@ -91,10 +88,9 @@ func NewClient(config Config) (*Client, error) {
return nil, fmt.Errorf("config validation error: %v", err)
}

var ethClient *ethclient.Client
ethClientMap := make(map[int]*ethclient.Client)
for _, provider := range config.Web3HttpProviders {
ethClient, err = ethclient.Dial(provider.Url)
ethClient, err := ethclient.Dial(provider.Url)
if err != nil {
return nil, fmt.Errorf("failed to create eth client: %v", err)
}
Expand All @@ -112,7 +108,6 @@ func NewClient(config Config) (*Client, error) {
ApiBaseURL: apiBaseUrl,
ApiKey: config.DevPortalApiKey,
NonceCache: make(map[string]uint64),
TenderlyKey: config.TenderlyKey,
}

c.common.client = c
Expand Down
9 changes: 9 additions & 0 deletions golang/client/onchain/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Erc20RevokeConfig struct {
type PermitSignatureConfig struct {
FromToken string
Name string
Version string
PublicAddress string
ChainId int
Key string
Expand All @@ -50,3 +51,11 @@ type PermitParamsConfig struct {
Deadline int64
Signature string
}

type ApprovalType int

const (
PermitIfPossible ApprovalType = iota
PermitAlways
ApprovalAlways
)
48 changes: 39 additions & 9 deletions golang/client/onchain/onchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func GetNonce(ethClient *ethclient.Client, key string, publicAddress common.Addr
return nonce, nil
}

func ExecuteTransaction(txConfig TxConfig, ethClient *ethclient.Client, nonceCache map[string]uint64) error {
func ExecuteTransaction(ctx context.Context, txConfig TxConfig, ethClient *ethclient.Client, nonceCache map[string]uint64) error {

nonceCacheKey := fmt.Sprintf("%s+%d", txConfig.PublicAddress, txConfig.ChainId.Int64())
nonce, err := GetNonce(ethClient, nonceCacheKey, txConfig.PublicAddress, nonceCache)
Expand All @@ -65,7 +65,7 @@ func ExecuteTransaction(txConfig TxConfig, ethClient *ethclient.Client, nonceCac
fmt.Printf("Transaction sent! (%s)\n", txConfig.Description)
helpers.PrintBlockExplorerTxLink(int(txConfig.ChainId.Int64()), swapTxSigned.Hash().String())

_, err = WaitForTransaction(ethClient, swapTxSigned.Hash())
_, err = WaitForTransaction(ctx, ethClient, swapTxSigned.Hash())
if err != nil {
return fmt.Errorf("failed to get transaction receipt: %v", err)
}
Expand Down Expand Up @@ -170,6 +170,36 @@ func ReadContractName(client *ethclient.Client, contractAddress common.Address)
return contractName, nil
}

func ReadContractVersion(client *ethclient.Client, contractAddress common.Address) (string, error) {
method := "version"

parsedABI, err := abi.JSON(strings.NewReader(abis.Erc20)) // Make a generic version of this ABI
if err != nil {
return "", err
}

// Construct the call message
msg := ethereum.CallMsg{
To: &contractAddress,
Data: parsedABI.Methods[method].ID,
}

// Query the blockchain
result, err := client.CallContract(context.Background(), msg, nil)
if err != nil {
return "1", nil // Many contracts don't have a version, so just return 1 if the call fails
}

// Unpack the result
var contractVersion string
err = parsedABI.UnpackIntoInterface(&contractVersion, method, result)
if err != nil {
return "", err
}

return contractVersion, nil
}

// ReadContractSymbol reads the 'symbol' public variable from a contract.
func ReadContractSymbol(client *ethclient.Client, contractAddress common.Address) (string, error) {
parsedABI, err := abi.JSON(strings.NewReader(abis.Erc20)) // Make a generic version of this ABI
Expand Down Expand Up @@ -230,7 +260,7 @@ func ReadContractDecimals(client *ethclient.Client, contractAddress common.Addre

// ReadContractNonce reads the 'nonces' public variable from a contract.
func ReadContractNonce(client *ethclient.Client, publicAddress common.Address, contractAddress common.Address) (int64, error) {
parsedABI, err := abi.JSON(strings.NewReader(abis.Erc20)) // Make a generic version of this ABI
parsedABI, err := abi.JSON(strings.NewReader(abis.Erc20))
if err != nil {
return -1, err
}
Expand Down Expand Up @@ -336,7 +366,7 @@ func GetTypeHash(client *ethclient.Client, addressAsString string) (string, erro
return resultAsString, nil
}

func ApproveTokenForRouter(client *ethclient.Client, nonceCache map[string]uint64, config Erc20ApprovalConfig) error {
func ApproveTokenForRouter(ctx context.Context, client *ethclient.Client, nonceCache map[string]uint64, config Erc20ApprovalConfig) error {
// Parse the USDC contract ABI to get the 'Approve' function signature
parsedABI, err := abi.JSON(strings.NewReader(abis.Erc20))
if err != nil {
Expand All @@ -358,7 +388,7 @@ func ApproveTokenForRouter(client *ethclient.Client, nonceCache map[string]uint6
To: config.Erc20Address.Hex(),
Data: data,
}
err = ExecuteTransaction(txConfig, client, nonceCache)
err = ExecuteTransaction(ctx, txConfig, client, nonceCache)
if err != nil {
return fmt.Errorf("failed to execute transaction: %v", err)
}
Expand Down Expand Up @@ -470,7 +500,7 @@ func GetPredicateCalldata(seriesNonceManager string, getTimestampBelowAndNonceEq
return data, nil
}

func RevokeApprovalForRouter(client *ethclient.Client, nonceCache map[string]uint64, config Erc20RevokeConfig) error {
func RevokeApprovalForRouter(ctx context.Context, client *ethclient.Client, nonceCache map[string]uint64, config Erc20RevokeConfig) error {
// Parse the USDC contract ABI to get the 'Approve' function signature
parsedABI, err := abi.JSON(strings.NewReader(abis.Erc20))
if err != nil {
Expand All @@ -492,14 +522,14 @@ func RevokeApprovalForRouter(client *ethclient.Client, nonceCache map[string]uin
To: config.Erc20Address.Hex(),
Data: data,
}
err = ExecuteTransaction(txConfig, client, nonceCache)
err = ExecuteTransaction(ctx, txConfig, client, nonceCache)
if err != nil {
return fmt.Errorf("failed to execute transaction: %v", err)
}
return nil
}

func WaitForTransaction(client *ethclient.Client, txHash common.Hash) (*types.Receipt, error) {
func WaitForTransaction(ctx context.Context, client *ethclient.Client, txHash common.Hash) (*types.Receipt, error) {
for {
receipt, err := client.TransactionReceipt(context.Background(), txHash)
if receipt != nil {
Expand All @@ -511,7 +541,7 @@ func WaitForTransaction(client *ethclient.Client, txHash common.Hash) (*types.Re
}
select {
case <-time.After(1000 * time.Millisecond): // check again after a delay
case <-context.Background().Done():
case <-ctx.Done():
fmt.Println("Context cancelled")
return nil, context.Background().Err()
}
Expand Down
67 changes: 0 additions & 67 deletions golang/client/onchain/onchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,54 +8,8 @@ import (
"github.com/stretchr/testify/require"

"github.com/1inch/1inch-sdk/golang/helpers/consts/amounts"
"github.com/1inch/1inch-sdk/golang/helpers/consts/chains"
)

func TestCreatePermitSignature(t *testing.T) {
testcases := []struct {
description string
fromToken string
name string
publicAddress string
chainId int
key string
nonce int64
deadline int64
expectedSignature string
}{
{
description: "Create Signature",
fromToken: "0x45c32fA6DF82ead1e2EF74d17b76547EDdFaFF89",
publicAddress: "0x2a250893f86Dc8497E131508f680338ac647B498",
chainId: chains.Polygon,
key: "ad21c0552a3b52e94520da713455cc347e4e89628a334be24d85b8083848434f",
name: "Frax",
nonce: 0,
deadline: 1704250835,
expectedSignature: "0x0d95c0246c1356df4653606e586e97447a516c937b5dd758fa0e56f2f8dd1f952b222c24a337e89dfbe20a8e112a7c6d004a3170598b9d4941aa38126920c9ed1b",
},
}

for _, tc := range testcases {
t.Run(tc.description, func(t *testing.T) {

config := &PermitSignatureConfig{
FromToken: tc.fromToken,
Name: tc.name,
PublicAddress: tc.publicAddress,
ChainId: tc.chainId,
Key: tc.key,
Nonce: tc.nonce,
Deadline: tc.deadline,
}

result, err := CreatePermitSignature(config)
require.NoError(t, err)
require.Equal(t, tc.expectedSignature, result)
})
}
}

func TestCreatePermitParams(t *testing.T) {
testcases := []struct {
description string
Expand Down Expand Up @@ -94,27 +48,6 @@ func TestCreatePermitParams(t *testing.T) {
}
}

func TestConvertSignatureToVRSString(t *testing.T) {
testcases := []struct {
description string
signature string
expectedSignatureString string
}{
{
description: "Create Signature",
signature: "c8dcab9ab2ce2055e61c0718117f8d77a56cd0a8b8370d8f5e16932a60d21a3e0eb0214dcbe4e7c5131cc45fd552e12f5bcef3b9c7fcb47ace9d4f694a496d471b",
expectedSignatureString: "000000000000000000000000000000000000000000000000000000000000001bc8dcab9ab2ce2055e61c0718117f8d77a56cd0a8b8370d8f5e16932a60d21a3e0eb0214dcbe4e7c5131cc45fd552e12f5bcef3b9c7fcb47ace9d4f694a496d47",
},
}

for _, tc := range testcases {
t.Run(tc.description, func(t *testing.T) {
result := ConvertSignatureToVRSString(tc.signature)
require.Equal(t, tc.expectedSignatureString, result)
})
}
}

func TestPadStringWithZeroes(t *testing.T) {
testcases := []struct {
description string
Expand Down
79 changes: 67 additions & 12 deletions golang/client/onchain/permits.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,82 @@ import (
"math/big"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/signer/core/apitypes"

"github.com/1inch/1inch-sdk/golang/helpers/consts/amounts"
"github.com/1inch/1inch-sdk/golang/helpers/consts/chains"
"github.com/1inch/1inch-sdk/golang/helpers/consts/contracts"
"github.com/1inch/1inch-sdk/golang/helpers/consts/typehashes"
)

type CreatePermitConfig struct {
EthClient *ethclient.Client
MakerAsset string
PublicAddress common.Address
ChainId int
PrivateKey string
Deadline int64
}

func CreatePermit(config *CreatePermitConfig) (string, error) {

// TODO due to a bug in the Limit Order API, we must check the version of the contract before attempting permit generation
// If the version of the contract is not 1, we exit early and default to an approval
version, err := ReadContractVersion(config.EthClient, common.HexToAddress(config.MakerAsset))
if err != nil {
return "0x", fmt.Errorf("failed to read contract version: %v", err)
}
if version != "1" {
return "0x", fmt.Errorf("contract version is not 1")
}

name, err := ReadContractName(config.EthClient, common.HexToAddress(config.MakerAsset))
if err != nil {
return "0x", fmt.Errorf("failed to read contract name: %v", err)
}

nonce, err := ReadContractNonce(config.EthClient, config.PublicAddress, common.HexToAddress(config.MakerAsset))
if err != nil {
return "0x", fmt.Errorf("failed to read contract nonce: %v", err)
}

sig, err := CreatePermitSignature(&PermitSignatureConfig{
FromToken: config.MakerAsset,
Name: name,
Version: version,
PublicAddress: config.PublicAddress.Hex(),
ChainId: config.ChainId,
Key: config.PrivateKey,
Nonce: nonce,
Deadline: config.Deadline,
})
if err != nil {
return "0x", fmt.Errorf("failed to create permit signature: %v", err)
}

aggregationRouter, err := contracts.Get1inchRouterFromChainId(config.ChainId)
if err != nil {
return "0x", fmt.Errorf("failed to get 1inch router address: %v", err)
}

return CreatePermitParams(&PermitParamsConfig{
Owner: config.PublicAddress.Hex(),
Spender: aggregationRouter,
Value: amounts.BigMaxUint256,
Deadline: config.Deadline,
Signature: sig,
}), nil
}

func CreatePermitSignature(config *PermitSignatureConfig) (string, error) {

// Domain Data
domainData := apitypes.TypedDataDomain{
Name: config.Name,
Version: "1",
Version: config.Version,
ChainId: math.NewHexOrDecimal256(int64(config.ChainId)),
VerifyingContract: config.FromToken,
}
Expand Down Expand Up @@ -114,16 +174,11 @@ func CreatePermitParams(config *PermitParamsConfig) string {
}

func ShouldUsePermit(ethClient *ethclient.Client, chainId int, srcToken string) bool {
// Currently, Permit1 swaps are only tested on Ethereum and Polygon
isPermitSupportedForCurrentChain := chainId == chains.Ethereum || chainId == chains.Polygon

if isPermitSupportedForCurrentChain {
typehash, err := GetTypeHash(ethClient, srcToken)
if err == nil {
// If a typehash for Permit1 is present, use that instead of Approve
if typehash == typehashes.Permit1 {
return true
}
typehash, err := GetTypeHash(ethClient, srcToken) // TODO this typehash lookup can miss many permit1-enabled tokens
if err == nil {
// If a typehash for Permit1 is present, use that instead of Approve
if typehash == typehashes.Permit1 {
return true
}
}
return false
Expand Down
Loading

0 comments on commit f2e79e5

Please sign in to comment.