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 ...
Para transferir a propriedade de um smart contract para um Gnosis Safe multisig
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.
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.
Transferindo propriedades de um contrato para um gnosis safe
Para isso vamos precisar de algumas coisas:
- Um contrato inteligente que usa a biblioteca própria do OpenZeppelin
- Um cofre Gnose
- 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 {
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>"
}
}
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 };
}
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);
});
Também salvaremos isso em scripts/
no projeto hardhat. Podemos então chamá-lo assim:
npx hardhat run scripts/transferOwnershipToGnosis.ts --network rinkeby
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
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).
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}`);
}
}
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);
});
npx hardhat run scripts/transferOwnershipFromGnosis.ts --network rinkeby
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!
Eeee terminamos!!!!!
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!
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
Latest comments (0)