WEB3DEV

Cover image for Protegendo um contrato inteligente com uma segurança Multisig Gnosis
AIengineer13
AIengineer13

Posted on

Protegendo um contrato inteligente com uma segurança Multisig Gnosis

Este tutorial demonstra como proteger um contrato inteligente transferindo a propriedade dele para um Multisig Gnosis Safe e, em seguida, interagindo com esse contract usando Hardhat e Ethers.js.

Por que isso importa? “Multisig” significa literalmente que várias assinaturas são necessárias antes que certas ações possam ocorrer. Por exemplo, podemos usar isso para proteger funções específicas de um smart contract, para que uma carteira não tenha controle total sobre o ETH que detém.

Neste tutorial, vamos escrever código ...

  1. Para transferir a propriedade de um smart contract para um Gnosis Safe multisig

  2. Para chamar funções do contract uma vez que é propriedade do Cofre

Esse processo é um elemento-chave da base de código web3 segura da Spectra.art, que foi testada e refinada em muitos lançamentos de NFT bem-sucedidos.

Image description

Este artigo é a Parte 2 de uma série introdutória sobre como criar um smart contract para vender NFTs!
A parte 1 está aqui., percorrendo o desenvolvimento, teste e implantação do contract e criando uma coleção OpenSea
A Parte 3, que cobrirá um aplicativo React básico que cunha NFTs, ainda está sendo escrito
Todo o código para essa série pode ser encontrado neste repositório: [https://github.com/Barefoot-Dev/solidity-nft-boilerplate](https://github.com/Barefoot-Dev/solidity-nft-boilerplate Assumindo que você criou um projeto hardhat e implantou um contrato antes de continuar com as próximas etapas.

Image description

Transferindo propriedades de um contrato para um gnosis safe

Para isso vamos precisar de algumas coisas:

  1. Um contrato inteligente que usa a biblioteca própria do OpenZeppelin
  2. Um cofre Gnose
  3. Algum JavaScript

Se você seguiu a Parte 1 desta série, já herdou Ownable em seu contrato inteligente.

Deve ser algo parecido com isso :


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract CryptoArt is ERC721Enumerable, Ownable {




Enter fullscreen mode Exit fullscreen mode

A seguir, crie um novo Gnosis Safe no the Rinkeby test network: https://gnosis-safe.io/app/open.
Não há problema em adicionar apenas um proprietário por enquanto, se você quiser conferir o código, apenas certifique-se de que seja a mesma carteira com a qual seu projeto de c hardhat está configurado. Quaisquer outros proprietários que você adicionar poderão fornecer assinaturas interagindo com a interface do Cofre, mas falaremos sobre isso mais tarde!
Uma vez criado, pegue o endereço de implantação do Safe e coloque-o em um arquivo config.json assim (indicando em qual rede você o criou):


{  
    "gnosisSafeAddress": {  
        "rinkeby": "<DEPLOYED_SAFE_ADDRESS>"  
    }  
}

Enter fullscreen mode Exit fullscreen mode

Armazene este arquivo no nível superior do seu projeto hardhat.

Agora é hora de um Javascript confiável!

Primeiro, escreveremos uma função auxiliar para obter uma instância do nosso contrato implantado e armazená-la em scripts/utils.ts.


const deployment = require("../deployments.json");

const hre = require("hardhat");

import { config as dotenvConfig } from "dotenv";

import { resolve } from "path";

dotenvConfig({ path: resolve(__dirname, "../.env") });

const alchemyKey: string | undefined = process.env.ALCHEMY_KEY;

if (!alchemyKey) {

throw new Error("Please set your ALCHEMY_KEY in a .env file");

}

export async function getContract(chainId: string) {

//  encontre o endereço do contrato criado na última execução do hardhat deploy

const deploymentInfo = deployment[chainId];

if (!deploymentInfo)

//  você executou o hardhat deploy com o sinalizador export-all?

throw `Error: no network found in deployments.json for chainId ${chainId}`;

const networkName = Object.keys(deploymentInfo)[0]; // get the first key

const address = deploymentInfo[networkName].contracts.CryptoArt.address;

//  carrega  o contrato via ethers.js



const Contract = await hre.ethers.getContractFactory("CryptoArt");

if (!Contract) {

throw new Error("Error: could not load contract factory"); // check the name ^

}

const contract = await Contract.attach(address);

console.log("got deployed contract", contract.address);

//  obter um fornecedor para estimar o gás

const provider = new hre.ethers.providers.AlchemyProvider(

networkName,

alchemyKey,

);

return { contract, provider, networkName };

}



Enter fullscreen mode Exit fullscreen mode

Agora que temos o contrato, podemos escrever um script para chamar a função transferOwnership do contrato. Vamos chamar esse script de transferOwnershipToGnosis.ts (já que faremos o oposto, transferOwnership*From*Gnosis, na próxima seção).

Agora que temos o contrato, podemos escrever um* script* para chamar a função transferOwnership do contract. Vamos chamar esse script de transferOwnershipToGnosis.ts (já que faremos o oposto, transferOwnershipFromGnosis, na próxima seção).



const config = require("../config.json");

import { getContract } from "./utils";

const hre = require("hardhat");

async function main() {

console.log("transfering contract ownership")

// pegue o contrato

const chainId = await hre.getChainId();

let { contract, provider, networkName } = await getContract(chainId);

//  verifique o proprietário inicial



const initOwner = await contract.owner();

console.log("from", initOwner);

const gnosisSafeAddress = config.gnosisSafeAddress[networkName];

console.log("to", gnosisSafeAddress);

//  estime o gás necessário



const methodSignature = await contract.interface.encodeFunctionData(

"transferOwnership",

[gnosisSafeAddress]

);

const tx = {

to: contract.address,

value: 0,

data: methodSignature,

from: initOwner,

};

const gasEstimate = await provider.estimateGas(tx);

//  enviar a transação para transferir a propriedade



const txnReceipt = await contract.transferOwnership(gnosisSafeAddress, {

from: initOwner,

value: 0,

gasLimit: gasEstimate,

});

console.log("txn hash", txnReceipt["hash"]);

}

main()

.then(() => process.exit(0))

.catch((error) => {

console.error(error);

process.exit(1);

});



Enter fullscreen mode Exit fullscreen mode

Também salvaremos isso em scripts/ no projeto hardhat. Podemos então chamá-lo assim:

npx hardhat run scripts/transferOwnershipToGnosis.ts --network rinkeby
Enter fullscreen mode Exit fullscreen mode

Depois de executar isso, podemos executar a função owner do contrato no etherscan para ver se o novo proprietário é de fato nosso Gnosis Safe recém-criado. Observe que isso só funcionará se você tiver verificado o contrato.

https://rinkeby.etherscan.io/address/#readContract

Image description

Image description

Interagindo com seu contrato como Proprietário Seguro

Desenvolvedores de contratos inteligentes normalmente restringem quem pode chamar funções específicas de contratos inteligentes. Por exemplo, não gostaríamos que ninguém pudesse chamar withdrawEth depois de lançar nossa coleção NFT!

A biblioteca ownable.sol do OpenZeppelin nos ajuda aqui fornecendo uma modificação onlyOwner, que podemos usar para garantir que uma função específica só possa ser chamada pelo proprietário do contrato!

Agora que o proprietário é um Gnosis Safe, precisamos de um novo script que chame nosso contratot em nome do Safe_ para continuar executando tais funções (por exemplo, withdrawEth diretamente para o Safe).

Image description

No script a seguir, estamos focados na função proposeTransaction, que instrui o gnosis safe a chamar uma função de smart contract específica. Observe que isso deve ser chamado por um dos proprietários do cofre (este código assume que a carteira para a qual o Hardhat está configurado e da qual implantamos o contrato originalmente, se tornou proprietária do cofre).

Ao propor uma transação, você (um dos proprietários) fornecerá uma assinatura que aprova o cofre para chamar o contrato. Se o seu cofre tiver vários proprietários, o script irá dormir e verificar periodicamente se os outros proprietários forneceram suas assinaturas antes de enviar a transação para a rede. (Outros proprietários precisarão fazer login na interface do usuário do cofre e assinar manualmente).


import EthersAdapter from "@gnosis.pm/safe-ethers-lib";

import Safe from "@gnosis.pm/safe-core-sdk";

import { ethers } from "ethers";

import {

SafeTransactionData,

SafeTransaction,

} from "@gnosis.pm/safe-core-sdk-types";

import SafeServiceClient from "@gnosis.pm/safe-service-client";

import {

SafeMultisigTransactionEstimate,

SafeMultisigTransactionEstimateResponse,

} from "@gnosis.pm/safe-service-client";

import { resolve } from "path";

import { config as dotenvConfig } from "dotenv";

// API

// https://safe-transaction.rinkeby.gnosis.io/

dotenvConfig({ path: resolve(__dirname, "./.env") });

const sk: string | undefined = process.env.PRIVATE_KEY;

if (!sk) {

throw "Please set your PRIVATE_KEY in a .env file";

}

const infuraApiKey: string | undefined = process.env.INFURA_API_KEY;

if (!infuraApiKey) {

throw new Error("Please set your INFURA_API_KEY in a .env file");

}

// como por examplo https://github.com/scaffold-eth/scaffold-eth/blob/gnosis-starter-kit/packages/react-app/src/views/EthSignSignature.jsx

// precisamos criar  de um SafeSignature para executar manualmente o método addSignature

// para obter a assinatura de origem do código da UI na transação antes de executeTransaction


const Signature = class CustomSignature {

signer: string;

data: string;

constructor(signer: string, data: string) {

this.signer = signer;

this.data = data;

}

staticPart() {

return this.data;

}

dynamicPart() {

return "";

}

};

export async function getProvider(chainId: string) {

let safeService;

let provider;

if (chainId === "1") {

safeService = new SafeServiceClient("https://safe-transaction.gnosis.io/");

provider = new ethers.providers.InfuraProvider(1, infuraApiKey);

} else if (chainId === "4") {

safeService = new SafeServiceClient(

"https://safe-transaction.rinkeby.gnosis.io/"

);

provider = new ethers.providers.InfuraProvider(4, infuraApiKey);

} else {

throw "Unsupported chainId";

}

return { safeService, provider };

}

export async function proposeTransaction(

chainId: string, // e.g. '4' for rinkeby

gnosisSafeAddress: string, // endereço

contractAddress: string, // endereço

methodSignature: string // hex

) {

console.log("getting provider");

const { safeService, provider } = await getProvider(chainId);

console.log("getting signer");

// @ts-ignore

const signer = new ethers.Wallet(sk, provider);

const ethAdapterOwner = new EthersAdapter({

ethers,

// @ts-ignore

signer: signer,

});

console.log("getting safe sdk", ethAdapterOwner, gnosisSafeAddress);

const safeSdk: Safe = await Safe.create({

ethAdapter: ethAdapterOwner,

safeAddress: gnosisSafeAddress,

});

//obter variáveis de requires  como entradas para a transação    

// nonce

const nonce = await safeSdk.getNonce();

console.log(`Nonce ${nonce}`);

// estimativa de gás para chamar este método

const safeTransactionEstimate: SafeMultisigTransactionEstimate = {

to: contractAddress,

value: "0", // in Wei

operation: 0, // 0 = CALL

data: methodSignature, // "0x" por nada 

};

console.log("Estimating gas", safeTransactionEstimate);

const gasEstimate: SafeMultisigTransactionEstimateResponse =

await safeService.estimateSafeTransaction(

gnosisSafeAddress,

safeTransactionEstimate

);

console.log("Got gas estimate", gasEstimate);

const safeTxGas: number = parseInt(gasEstimate.safeTxGas);

// docs https://docs.gnosis.io/safe/docs/contracts_tx_execution/

const safeTransactionData: SafeTransactionData = {

to: contractAddress,

value: "0", // in Wei

data: methodSignature, // "0x" por nada 

safeTxGas: safeTxGas,

operation: 0, // 0 = CALL

gasToken: ethers.constants.AddressZero, // ether

gasPrice: 0,  // Preço do gás usado para o cálculo do reembolso

baseGas: 21000,

refundReceiver: gnosisSafeAddress,

nonce: nonce, //  nono do cofre

};

// prepara e assina a transição 

const safeTransaction: SafeTransaction = await safeSdk.createTransaction(

safeTransactionData

);

const txHash = await safeSdk.getTransactionHash(safeTransaction);

console.log(`Safe tx hash = ${txHash}`);

const signature = await safeSdk.signTransactionHash(txHash);

safeTransaction.addSignature(signature);

// envia a transação para a UI  para coletar as outras assinaturas fora da cadeia

console.log(`Proposing transaction`);

await safeService.proposeTransaction({

safeAddress: gnosisSafeAddress,

senderAddress: signer.address,

safeTransaction: safeTransaction,

safeTxHash: txHash,

});

const threshold = await safeSdk.getThreshold();

console.log("Safe threshold:", threshold);

console.log(`Awaiting ${threshold} confirmation(s)`);

let confirmed = false;

while (!confirmed) {

let confirmations = await safeService.getTransactionConfirmations(txHash);

console.log(`Current num confirmations ${confirmations.count}`);

if (confirmations.count === threshold) {

console.log("Received all required confirmations!");

console.log(confirmations);

confirmed = true;

// A 0ª confirmação é a já fornecida acima no código

// O primeiro é a confirmação recém-adquirida da  UI

 // pega isso e adiciona no objeto de transação 

//(a assinatura do primeiro signatário (o sdk) é adicionada dentro de executeTransaction)


for (let i = 1; i < confirmations.results.length; i++) {

const signature = confirmations.results[i];

await safeTransaction.addSignature(

new Signature(signature.owner, signature.signature)

);

}

// const confirmation = confirmations.results[1];

// console.log("adding signature2 from UI confirmation", confirmation);

// const signature2 = new Signature(

// confirmation.owner,

// confirmation.signature

// );

// await safeTransaction.addSignature(signature2);

} else {

await new Promise((resolve) => setTimeout(resolve, 10000));

}

}

const approvers = await safeSdk.getOwnersWhoApprovedTx(txHash);

console.log("approvers", approvers);

console.log("signatures", safeTransaction.signatures);

// usa  ethers.js para adivinhar o preço do gás para usar

const gasPriceBN = await provider.getGasPrice();

let gasPrice = gasPriceBN.toNumber();

console.log("got gasPrice", gasPrice); 

// opção para aumentar o preço do gás através da configuração

console.log(`Executing with gasPrice = ${gasPrice}`);

// garante que o preço do gás foi retornado e não é nan 

if (gasPrice && !isNaN(gasPrice)) {

const executeTxResponse = await safeSdk.executeTransaction(

safeTransaction,

{

gasPrice: gasPrice,

}

);

let executed = false;

while (!executed) {

console.log("awaiting tx execution");

const tx = await safeService.getTransaction(txHash);

if (tx.isExecuted) {

console.log(`Tx executed!`);

// hash é nulo até ser executado

console.log(`Hash: ${tx.transactionHash}`);

console.log(tx);

executed = true;

} else {

await new Promise((resolve) => setTimeout(resolve, 10000));

}

}

await executeTxResponse.transactionResponse?.wait();

console.log(

`Executed tx hash: ${executeTxResponse.transactionResponse?.hash}`

);

} else {

console.log(`Could not execute transaction with gasPrice ${gasPrice}`);

}

}

Enter fullscreen mode Exit fullscreen mode

Agora a peça final!

Precisaremos de uma versão modificada de transferOwnershipToGnosis.ts, que obtém a assinatura do método para transferir a propriedade docofre e de volta para a carteira do implantador, e chama nossa nova função proposeTransaction de cima.

Para manter as coisas simples, vamos chamá-lo de transferOwnershipFromGnosis.ts.


import { proposeTransaction } from "./gnosis";

import { getContract } from "./utils";

const hre = require("hardhat");

async function main() {

// obtem  o contracto 

const chainId = await hre.getChainId();

const { contract, networkName } = await getContract(chainId);

// obter o endereço do implantador original

const { getNamedAccounts } = hre;

const { deployer } = await getNamedAccounts();

// alterar o proprietário e armazenar o recibo da transação

//obter o código hex do txn desejado
const methodSignature = await contract.interface.encodeFunctionData(

"transferOwnership",

[deployer]

);

// em vez de enviar diretamente

//  agora propomos o txn para o Gnosis Safe

const result = await proposeTransaction(

chainId,

deployer,

contract.address,

methodSignature

);

console.log("got result", result);

}

main()

.then(() => process.exit(0))

.catch((error) => {

console.error(error);

process.exit(1);

});

Enter fullscreen mode Exit fullscreen mode
npx hardhat run scripts/transferOwnershipFromGnosis.ts --network rinkeby
Enter fullscreen mode Exit fullscreen mode

Isso deve registrar várias coisas no seu console enquanto ele é executado. Ele aguardará todas as confirmações necessárias (ou seja, assinaturas fornecidas por outros proprietários de cofres, se houver mais de 1), então enviará a transação para a rede, aguardará sua execução e, finalmente, registrará o hash da transação.

É claro que você pode verificar se a propriedade foi transferida de volta para a carteira de implantação original:

https://rinkeby.etherscan.io/address/#readContract
Se você quiser adicionar mais proprietários, basta fazê-lo nas configurações do Cofre. A qualquer momento, você também pode modificar o número de assinaturas necessárias para aprovar uma transação, mas não se esqueça de que todas as assinaturas atuais são necessárias para isso!

Image description

Eeee terminamos!!!!!

Image description

Você garantiu com sucesso um smart contract dando a propriedade dele a um Gnosis Safe multisig e, em seguida, executou as funções do contract solicitando assinaturas dos proprietários do cofre!

Image description

Antes de usar um Safe em produção, leia sobre Gnosis e mantenha-se sempre atualizado com as práticas recomendadas de segurança. Não se esqueça de que, se o seu cofre tiver 3 proprietários e exigir 3 assinaturas para fazer qualquer coisa, perder uma dessas carteiras de proprietário significa que seu contract (e qualquer ETH nele!).

este artigo foi criado por BarefootDev e traduzido por aiengineer13 siga este link para ver o artigo original

Top comments (0)