Go и мультисиг: интеграция с Safe-контрактом

в 22:00, , рубрики: golang, multisig, SAFe, интеграция

Введение

Для создания мультисиг кошелька Safe можно воспользоваться видеоинструкцией или использовать API.

В этой статье рассказано, как используя Golang, вызвать метод на смарт-контракте и создать мультисиг кошелек. Дополнительная настройка этого кошелька, отправка с него транзакций и другие полезные функции планируется осветить в других статьях.

Содержание

I. Настройка окружения
II. Подключение к блокчейну
III. Создание транзакции, которая создаст мультисиг кошелек
IV. Запуск на выполнение и аналитика результатов

I. Настройка окружения

1. Развернем проект на Go, для этого выполните команду.

$ go mod init github.com/{тут название вашего аккаунта на github}/multisig

Смотрите тут подробнее.

2. Получение данных для подключения к сети и смарт-контрактам

Для начала определимся с сетью, выберем тестовую сеть Ehtereum. Нам нужные следующие смарт-контракты, находятся тут. Будем использовать:

  • safe_proxy_factory: 0X4E1DCF7AD4E460CFD30791CCC4F9C8A4F820EC67

  • safe: 0X41675C099F32341BF84BFC5382AF534DF5C7461A

Для возможности взаимодействовать с блокчейном нам необходимо использовать подключение через провайдера, в этой статье используем alchemy. Сгенерированный ключ на этой платформе (alchemy) и адреса контрактов поместим в файл .env.

rpc_url=https://eth-sepolia.g.alchemy.com/v2/{тут ваш личный ключ}
safe_proxy_factory=0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67
safe=0x41675C099F32341bf84BFc5382aF534df5C7461a
private_key={тут ваш личный приватный ключ}

Приватный ключ в private_key необходим для подписания транзакций. Получить его можно, когда вы создаете обычный кошелек.

3. Получение ABI смарт-контрактов и генерация кода на GO

Скопируем ABI смарт-контрактов в файлы (ниже указаны пути, где разместить файлы). Нам нужны json, которые находятся в разделе Contract ABI.

[{"anonymous":false,"inputs":[{"indexed":...........

[{"anonymous":false,"inputs":[{"indexed":...........
  1. abi/safe_proxy_factory_abi/safe_proxy_factory.abi - ABI у safe_proxy_factory смарт-контракта смотрите по ссылке:

  2. abi/safe_abi/safe.abi - https://sepolia.etherscan.io/address/0x41675C099F32341bf84BFc5382aF534df5C7461a#code

Сгенерируем safe_proxy_factory_abi.go и safe_abi.go для этого запустим по очереди две команды с командной строки, используя abigen.

$ abigen --abi=abi/safe_proxy_factory_abi/safe_proxy_factory.abi --pkg=safe_proxy_factory_abi --out=abi/safe_proxy_factory_abi/safe_proxy_factory_abi.go

$ abigen --abi=abi/safe_abi/safe.abi --pkg=safe_abi --out=abi/safe_abi/safe_abi.go

На текущий момент у нас такая структура файлов:

Структура файлов

Структура файлов

II. Подключение к блокчейну

Файл, в котором будет основная логика, назовем multisig.go. В нем напишем код, который читает данные из .env:

package main

import (
	"github.com/spf13/viper"
)

func LoadConfig() {
	viper.AutomaticEnv()
	viper.SetConfigFile(".env")
	viper.ReadInConfig() //nolint:errcheck
}

func main() {
	LoadConfig()
}

Добавим зависимости:
$ go get github.com/spf13/viper

Добавим в multisig.go функцию getProvider, которая отвечает за подключение к провайдеру. Используем URL, прописанный у нас в .env rpc_url.

func getProvider() (*ethclient.Client, error) {
	rpcClient, err := rpc.DialOptions(
		context.Background(),
		viper.GetString("rpc_url"),
		rpc.WithHTTPClient(&http.Client{ //nolint:exhaustruct
			Timeout: 15 * time.Second,
		}),
	)
	if err != nil {
		return &ethclient.Client{}, err
	}

	connection := ethclient.NewClient(rpcClient)

	return connection, nil
}

Добавим зависимости:

$ go get github.com/ethereum/go-ethereum/ethclient github.com/ethereum/go-ethereum/rpc github.com/ethereum/go-ethereum/accounts/keystore@v1.15.1

III. Создание транзакции

1. Получение и обработка входящих данных

Добавим в multisig.go функцию sendDeployMultisig, в ней получим из .env данные и ABI смарт-контракта safe. Ниже приведен часть кода, в котором в разделе import внесены нужные зависимости для всего файла multisig.go на данном этапе создания файла.

import (
	"context"
	"net/http"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/ethereum/go-ethereum/rpc"
	"github.com/spf13/viper"
	"github.com/timofvy/multisig/abi/safe_abi"
)

//-------------------------------
func sendDeployMultisig() error {
	priv := viper.GetString("private_key")
	safeProxyFactoryAddress := common.HexToAddress(viper.GetString("safe_proxy_factory"))
	safeAddress := common.HexToAddress(viper.GetString("safe"))

	contractAbi, err := abi.JSON(strings.NewReader(safe_abi.SafeAbiABI))
	if err != nil {
		return err
	}

	return nil
}

Далее нам надо подготовить данные для вызова функции setup на смарт-контракте safe. Для понимания, какие нужно передать параметры обратимся к файлу abi/safe_abi/safe_abi.go. Ниже код из этого файла.

// Setup is a paid mutator transaction binding the contract method 0xb63e800d.
//
// Solidity: function setup(address[] _owners, uint256 _threshold, address to, bytes data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver) returns()
func (_SafeAbi *SafeAbiTransactor) Setup(
  opts *bind.TransactOpts,
  _owners []common.Address,
  _threshold *big.Int,
  to common.Address,
  data []byte,
  fallbackHandler common.Address,
  paymentToken common.Address,
  payment *big.Int,
  paymentReceiver common.Address
) (*types.Transaction, error) {
	return _SafeAbi.contract.Transact(opts,
                                      "setup",
                                      _owners,
                                      _threshold,
                                      to,
                                      data,
                                      fallbackHandler,
                                      paymentToken,
                                      payment,
                                      paymentReceiver)
}

Данные сформируем следующим образом:

data, err := contractAbi.Pack("setup",
	owners,
	big.NewInt(int64(threshold)),
	common.HexToAddress(zeroAddress),
	[]byte{0},
	common.HexToAddress(zeroAddress),
	common.HexToAddress(zeroAddress),
	big.NewInt(0),
)
if err != nil {
	return err
}

Первым и вторым параметром передаются настройки мультисиг кошелька:
owners - массив адресов, которые смогут отдавать свои голоса (подписи)
threshold - количество положительных голосов, т.е. "ЗА", необходимых чтобы предложение (proposal) было принято.

zeroAddress - нулевой адрес, вынесем в константу.

const zeroAddress = "0x0000000000000000000000000000000000000000"

Данные owners и threshold можно прописать сразу при формировании data, на сленге это называется захаркодить. Мы же передадим эти параметры из командной строки, когда будем запускать файл к исполнению. Для этого внесем некоторые правки в уже написанный код.

func main() {
	LoadConfig()

	owners := flag.String("owners", "", "Owners")
	threshold := flag.Int("threshold", 0, "Threshold")
	flag.Parse()

	signerAddrs := strings.Split(*owners, ",")

	var ownersAddrSlice []common.Address
	for _, addr := range signerAddrs {
		trimmedAddr := strings.TrimSpace(addr)
		ownerAddr := common.HexToAddress(trimmedAddr)

		ownersAddrSlice = append(ownersAddrSlice, ownerAddr)
	}

	err := sendDeployMultisig(ownersAddrSlice, *threshold)
	if err != nil {
		return
	}
}

func sendDeployMultisig(
	owners []common.Address,
	threshold int,
) error {
  ...

Пакет flag реализует синтаксический анализ флагов командной строки.
В командной строке мы передадим нужный нам список адресов "подписантов" (адреса приведены для примера):
--owners 0x484D411062f0135585774da4ae7bAEC0560B0CB3,0xAB706bb9D041C6E3A7B1088A14b5fDCA90c7E163
и количество подписей --threshold 2.

При интеграции в вашем проекте кода из этой статьи параметры для мультисиг кошелька наверняка вы получите из другого источника, например из запроса. Функция sendDeployMultisig уже будет готова их принять и обработать.

В функцию main (см. выше) мы добавили алгоритм получения параметров из командной строки, их обработку и вызов функции создания и отправки транзакции sendDeployMultisig. Обработка параметра owners делает разбиение передаваемых данных из строки в массив и перевод формата адресов в common.Address. А также удаление пробельных символов в начале и в конце строки.

Добавим проверку в sendDeployMultisig, что данные в командной строке переданы.

func sendDeployMultisig(
	owners []common.Address,
	threshold int,
) error {
	if threshold == 0 {
		return errors.New("threshold must be greater than 0")
	}

	if len(owners) == 0 {
		return errors.New("owners must be greater than 0")
	}
  ...

2. Подписание и отправка транзакции в блокчейн

Получим провайдера, для этого вызовем функцию getProvider().

provider, err := getProvider()
if err != nil {
	return err
}

Создадим экземпляр contractTransactor — это объект, который позволяет отправлять транзакции в смарт-контракт и получать данные из него. 

contractTransactor, err := safe_proxy_factory_abi.NewSafeProxyFactoryAbiTransactor(
	safeProxyFactoryAddress, provider,
)
if err != nil {
	return err
}

Ниже в коде получаем:

  • chainID, в нашем случае это значение 11155111 для https://sepolia.etherscan.io/

  • privateKey — ключ в формате *ecdsa.PrivateKey из вашего приватного ключа указанного в .env

Примечание: bind и crypto из github.com/ethereum/go-ethereum, автоматически добавятся в import при сохранении файла.

chainID, err := provider.ChainID(context.Background())
if err != nil {
	return err
}

privateKey, err := crypto.HexToECDSA(priv)
if err != nil {
	return err
}

trOpts, err := bind.NewKeyedTransactorWithChainID(
	privateKey,
	chainID,
)
if err != nil {
	return err
}

NewKeyedTransactorWithChainID — это вспомогательный метод для простого создания подписчика транзакции из одного закрытого ключа.

Итоговым шагом отправим транзакцию в блокчейн.

transaction, err := contractTransactor.CreateProxyWithNonce(
	trOpts,
	safeAddress,
	data,
	big.NewInt(0), // saltNonce, любое случайное число,
                   //параметр saltNonce позволяет создавать уникальные адреса прокси-контратов
)
if err != nil {
	return err
}

CreateProxyWithNonce — это метод, который представлен в abi/safe_proxy_factory_abi/safe_proxy_factory_abi.go. Ниже код для понимания какие передавать данные.

// CreateProxyWithNonce is a paid mutator transaction binding the contract method 0x1688f0b9.
//
// Solidity: function createProxyWithNonce(address _singleton, bytes initializer, uint256 saltNonce) returns(address proxy)
func (_SafeProxyFactoryAbi *SafeProxyFactoryAbiTransactor) CreateProxyWithNonce(
  opts *bind.TransactOpts
, _singleton common.Address,
  initializer []byte,
  saltNonce *big.Int
) (*types.Transaction, error) {
	return _SafeProxyFactoryAbi.contract.Transact(opts,
                                                  "createProxyWithNonce",
                                                  _singleton,
                                                  initializer,
                                                  saltNonce)
}

Для наглядного отображения факта, что транзакция успешно попала в блокчейн, выведем в лог hash этой транзакции.

log.Println("Transaction sent: ", transaction.Hash().Hex())

Сверим что у нас получилось в итоге, в файле multisig.go.

package main

import (
	"context"
	"errors"
	"flag"
	"log"
	"math/big"
	"net/http"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/ethereum/go-ethereum/rpc"
	"github.com/spf13/viper"
	"github.com/timofvy/multisig/abi/safe_abi"
	"github.com/timofvy/multisig/abi/safe_proxy_factory_abi"
)

const zeroAddress = "0x0000000000000000000000000000000000000000"

func LoadConfig() {
	viper.AutomaticEnv()
	viper.SetConfigFile(".env")
	viper.ReadInConfig() //nolint:errcheck
}

func getProvider() (*ethclient.Client, error) {
	rpcClient, err := rpc.DialOptions(
		context.Background(),
		viper.GetString("rpc_url"),
		rpc.WithHTTPClient(&http.Client{ //nolint:exhaustruct
			Timeout: 15 * time.Second,
		}),
	)
	if err != nil {
		return &ethclient.Client{}, err
	}

	connection := ethclient.NewClient(rpcClient)

	return connection, nil
}

func main() {
	LoadConfig()

	owners := flag.String("owners", "", "Owners")
	threshold := flag.Int("threshold", 0, "Threshold")
	flag.Parse()

	signerAddrs := strings.Split(*owners, ",")

	var ownersAddrSlice []common.Address
	for _, addr := range signerAddrs {
		trimmedAddr := strings.TrimSpace(addr)
		ownerAddr := common.HexToAddress(trimmedAddr)

		ownersAddrSlice = append(ownersAddrSlice, ownerAddr)
	}

	err := sendDeployMultisig(ownersAddrSlice, *threshold)
	if err != nil {
        log.Println(err) // выведем ошибку в лог, если она случилась
		return
	}
}

func sendDeployMultisig(
	owners []common.Address,
	threshold int,
) error {
	if threshold == 0 {
		return errors.New("threshold must be greater than 0")
	}

	if len(owners) == 0 {
		return errors.New("owners must be greater than 0")
	}

	priv := viper.GetString("private_key")
	safeProxyFactoryAddress := common.HexToAddress(viper.GetString("safe_proxy_factory"))
	safeAddress := common.HexToAddress(viper.GetString("safe"))

	contractAbi, err := abi.JSON(strings.NewReader(safe_abi.SafeAbiABI))
	if err != nil {
		return err
	}

	data, err := contractAbi.Pack("setup",
		owners,
		big.NewInt(int64(threshold)),
		common.HexToAddress(zeroAddress),
		[]byte{0},
		common.HexToAddress(zeroAddress),
		common.HexToAddress(zeroAddress),
		big.NewInt(0),
		common.HexToAddress(zeroAddress),
	)
	if err != nil {
		return err
	}

	provider, err := getProvider()
	if err != nil {
		return err
	}

	contractTransactor, err := safe_proxy_factory_abi.NewSafeProxyFactoryAbiTransactor(
		safeProxyFactoryAddress,
		provider,
	)
	if err != nil {
		return err
	}

	chainID, err := provider.ChainID(context.Background())
	if err != nil {
		return err
	}

	privateKey, err := crypto.HexToECDSA(priv)
	if err != nil {
		return err
	}

	trOpts, err := bind.NewKeyedTransactorWithChainID(
		privateKey,
		chainID,
	)
	if err != nil {
		return err
	}

	transaction, err := contractTransactor.CreateProxyWithNonce(
		trOpts,
		safeAddress,
		data,
		big.NewInt(0),
	)
	if err != nil {
		return err
	}

	log.Println("Transaction sent: ", transaction.Hash().Hex())

	return nil
}

IV. Запуск и аналитика результатов

В командной строке выполним команду:

$ go build

Выполним операцию build ("сбилдим")

Выполним операцию build ("сбилдим")

Перед следующим шагом вам надо убедиться, что на кошельке, приватный ключ, которого вы указали в .env, находятся тестовые монеты Eth. Получить их можно например здесь, либо на другом faucet ("кране").

Если выполнить вызов создания мультисиг кошелька кошельком на котором нет или не хватает средств на комиссию для осуществления транзакции в сети, то получим ошибку insufficient funds for transfer.

Ошибка insufficient funds for transfer, так как нет средств на кошельке, отправляющем транзакцию

Ошибка insufficient funds for transfer, так как нет средств на кошельке, отправляющем транзакцию

В консоле вызовем ту же команду, но после того как пополним кошелек, отправляющий транзакцию:

$ go run multisig.go --owners 0x484D411062f0135585774da4ae7bAEC0560B0CB3,0xAB706bb9D041C6E3A7B1088A14b5fDCA90c7E163 --threshold 2

Транзакция успешно прошла, в логах мы видим hash этой транзакции.

Успешная транзакция

Успешная транзакция

Для анализа перейдем по ссылке https://sepolia.etherscan.io/tx/0x07265d5b35c604974f4a39b055af83cfbb851902d1aa8399fb04a3f0ae36564d
В поле To есть информация об адресе вновь созданного мультисиг кошелька (см. ... Created).

Go и мультисиг: интеграция с Safe-контрактом - 6

Проверим параметры нашего нового мультисиг кошелька, перейдем по ссылке https://app.safe.global/settings/setup?safe=sep:0xBe17c4B2dD3512046a709eF91155c8b61328e400

Параметры нового мультисиг кошелька

Параметры нового мультисиг кошелька

У нас все получилось, на языке Go мы создали новый мультисиг кошелек Safe с нужными нам параметрами, обратившись для этого к смарт-контракту.

Автор: timofvy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js