paint-brush
Adición de la gobernanza de DAO a los contratos de token existentespor@tally
8,235 lecturas
8,235 lecturas

Adición de la gobernanza de DAO a los contratos de token existentes

por Tally2022/06/07
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Natacha De la Rosa explica cómo convertir tu contrato de token NFT o ERC20 en un DAO. Puede agregar un [Contrato de gobernador] para administrar propuestas y votos para su DAO usando la biblioteca de contratos de [OpenZeppelin]. Su contrato de token necesita funciones **delegate()**, **delegates()** para delegar votos de un usuario a otro. Debe tener una definición clara de cómo calcular el poder de voto de cada titular de token. También necesita emitir registros de eventos para cambios de votos, transferencias de tokens y cambios de delegación.

Company Mentioned

Mention Thumbnail
featured image - Adición de la gobernanza de DAO a los contratos de token existentes
Tally HackerNoon profile picture


En mi publicación anterior, hablé sobre cómo hacer que su contrato NFT esté listo para DAO desde el primer día. Pero, ¿qué sucede si ya implementó su contrato de token NFT o ERC20 sin un DAO futuro? ¿Cómo puede agregar la gobernanza de DAO utilizando ese token existente? Vamos a averiguar.


Puede convertir su contrato de token en un DAO agregando un contrato de gobernador para administrar propuestas y votos para su DAO. Pero antes de que pueda hacer eso, deberá asegurarse de que su contrato de token sea compatible con el gobernador.


El gobernador espera una interfaz particular del contrato de token. Aquí hay un resumen de lo que necesita:


  • Su contrato de token necesita funciones delegate() , delegateBySig() y delegates() para delegar votos de un usuario a otro.


  • Su contrato de token debe tener una definición clara de cómo calcular el poder de voto de cada titular de token. Por lo general, una ficha = un voto, pero también puede personalizar una fórmula basada en el suministro de fichas. Necesita tener estas definiciones de función getVotes() , getPastVotes() y getPastTotalSupply() .


  • Por último, pero no menos importante, su contrato de token debe emitir registros de eventos para cambios de votos, transferencias de tokens y cambios de delegación. Necesita tener estas definiciones de eventos específicas DelegateChanged , DelegateVotesChanged y Transfer .


Para obtener más información sobre cómo se ven estas funciones y firmas de eventos y qué deben devolver, lea la definición de Open Zeppelin IVotes .


Ahora que sabe lo que necesita su contrato de token, puedo explicarle cómo puede lograrlo. Hay dos formas, dependiendo de cómo haya creado su contrato de token en primer lugar.


Con la biblioteca de contratos de OpenZeppelin , puedo agregar ERC20Votes o ERC721Votes según mi tipo de token y anular algunos métodos necesarios incluso si mi contrato no se puede actualizar.


Si su contrato es actualizable, deberá crear una nueva implementación y actualizar el proxy para que implemente las bibliotecas adicionales que necesita su contrato de token.


Si su contrato no es actualizable, creará un contrato adicional con las características que he mencionado anteriormente. Luego, deberá permitir que los usuarios apuesten los tokens que tienen para acuñar los nuevos.

1. Su contrato de token es actualizable

Si este es tu caso, tienes el camino más fácil a seguir.


Es necesario tener permisos de administración del contrato para realizar estos cambios. Si no lo hace, no podrá actualizar la implementación del token del contrato actualizable.


Primero, digamos que tengo el siguiente contrato simbólico que implementé en Rinkeby. No admite delegación ni ninguna otra capacidad de votación, pero se puede actualizar:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract OldToken is Initializable, ERC20Upgradeable, OwnableUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} function initialize() public initializer { __ERC20_init("OldToken", "OTK"); __Ownable_init(); _mint(msg.sender, 20 * 10**decimals()); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }


Ahora, necesito actualizar mi token para tener una nueva implementación que admita la delegación y otras capacidades de votación como esta:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20VotesUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract NewToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, ERC20VotesUpgradeable { /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} function initialize() public initializer { __ERC20_init("NewToken", "NTK"); __Ownable_init(); __ERC20Permit_init("NewToken"); _mint(msg.sender, 20 * 10**decimals()); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } // The following functions are overrides required by Solidity. function _afterTokenTransfer( address from, address to, uint256 amount ) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { super._afterTokenTransfer(from, to, amount); } function _mint(address to, uint256 amount) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { super._mint(to, amount); } function _burn(address account, uint256 amount) internal override(ERC20Upgradeable, ERC20VotesUpgradeable) { super._burn(account, amount); } }


Después de eso, puedo ejecutar la tarea de actualización y mi contrato tendrá la nueva implementación que necesito:


 import { task } from "hardhat/config"; import type { TaskArguments } from "hardhat/types"; import { NewToken } from "../../src/types/contracts"; import { NewToken__factory } from "../../src/types/factories/contracts"; import { getTokenInfo, saveJSON } from "../../test/utils"; task("deploy:updateToken").setAction(async function (_: TaskArguments, { ethers, upgrades, run }) { // get current proxy address const oldToken = getTokenInfo("./oldTokenAddress.json"); // token upgrade const UpgradedToken: NewToken__factory = await ethers.getContractFactory("NewToken"); const upgraded: NewToken = <NewToken>await upgrades.upgradeProxy(oldToken.proxy, UpgradedToken); await upgraded.deployed(); const tokenImplementationAddress = await upgrades.erc1967.getImplementationAddress(upgraded.address); // write to local const data = { token: { proxy: upgraded.address, implementation: tokenImplementationAddress }, }; saveJSON(data, "./newTokenAddress.json"); // etherscan verification await run("verify:verify", { address: tokenImplementationAddress, }); });


¡Eso es todo, y ahora mi contrato simbólico tiene todo lo que necesita para usarse con mi contrato de gobernador!

2. Su contrato de token no es actualizable

Si este es tu caso, no te preocupes. Todo tiene solución. Necesitarás algunos pasos más, pero nada demasiado complicado.


Primero crearé un nuevo contrato; puede hacer que sea actualizable o no, pero en este ejemplo, haré que no sea actualizable para mostrar ambos lados. Si desea crearlo para que sea actualizable, puede consultar el anterior que creé.


Implementaré las funciones que necesito para que mi token sea compatible con un contrato de gobernador en este nuevo contrato de token. Luego, crearé dos funciones adicionales para permitir que los usuarios de mis tokens apuesten sus tokens y los retiren de este contrato.


También deberá crear una interfaz de usuario simple para permitir que sus usuarios realicen estas acciones. Eso es todo lo que tendrás que hacer. Saltemos directamente a eso.


Digamos que tengo este token ERC721 que ya está implementado en Rinkeby, pero no tiene capacidades actualizables:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract OldNFTToken is ERC721, Ownable { using Counters for Counters.Counter; Counters.Counter private _tokenIdCounter; constructor() ERC721("OldNFTToken", "ONTK") {} function safeMint(address to) public onlyOwner { uint256 tokenId = _tokenIdCounter.current(); _tokenIdCounter.increment(); _safeMint(to, tokenId); } }


En este caso, necesitaré crear un nuevo token ERC721 que tenga las capacidades de DAO que necesito. Este nuevo contrato permitirá a los titulares de tokens actuales depositar sus propios tokens. Luego, pueden recibir el nuevo token de votación y participar en mi DAO. Los poseedores de tokens también tendrán la opción de retirar sus tokens apostados.


Comencemos creando el nuevo token ERC721 así:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { constructor() ERC721("NewNFTToken", "NNTK") EIP712("NewNFTToken", "1") {} function safeMint(address to, uint256 tokenId) public onlyOwner { _safeMint(to, tokenId); } // The following functions are overrides required by Solidity. function _afterTokenTransfer( address from, address to, uint256 tokenId ) internal override(ERC721, ERC721Votes) { super._afterTokenTransfer(from, to, tokenId); } }


Mi nuevo contrato de token ERC721 debería tener estas funcionalidades:


  • Mintable : usaré esta función para crear un token único para mis poseedores de tokens cada vez que depositen un token que poseen de OldNFTToken en NewNFTToken .


  • Quemable: Usaré esta función para quemar un NewNFTToken acuñado cuando el titular retire NewNFTToken .


Ahora agregaré dos nuevas funciones a mi nuevo contrato para administrar los procesos de staking y retiro :


  1. Primero, cree una variable de estado para recibir la dirección del contrato NFT actual.


 contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { // ... IERC721 public oldNFTToken; // ... }


  1. Ahora, actualizaré el constructor para recibir la dirección de mi OldNFTToken.


 contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { // ... constructor(address _oldNFTTokenAddress) ERC721("NewNFTToken", "NNTK") EIP712("NewNFTToken", "1") { oldNFTToken = IERC721(_oldNFTTokenAddress); } // ... }


  1. También actualizaré la función safeMint para que se vea así:


 contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { // ... function safeMint(address _to, uint256 _tokenId) private { _safeMint(_to, _tokenId); } // ... }


  1. Luego, agregaré las funciones para apostar y retirar .


 contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { // ... // holder needs to approve this contract address before calling this method function stake(uint256 _tokenId) public { oldNFTToken.safeTransferFrom(msg.sender, address(this), _tokenId, "0x00"); // transfer token to this contract - stake safeMint(msg.sender, _tokenId); // mint a new vote token for staker } function withdraw(uint256 _tokenId) public { oldNFTToken.safeTransferFrom(address(this), msg.sender, _tokenId, "0x00"); _burn(_tokenId); // burn voteToken after withdraw } // ... }


  1. Por último, pero no menos importante, agregaré la implementación de ERC721Receiver a mi contrato para que pueda admitir transferencias seguras:


 contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { // ... // holder needs to approve this contract address before calling this method function stake(uint256 _tokenId) public { oldNFTToken.safeTransferFrom(msg.sender, address(this), _tokenId, "0x00"); // transfer token to this contract - stake safeMint(msg.sender, _tokenId); // mint a new vote token for staker } function withdraw(uint256 _tokenId) public { oldNFTToken.safeTransferFrom(address(this), msg.sender, _tokenId, "0x00"); _burn(_tokenId); // burn voteToken after withdraw } // ... }


Ahora el nuevo token ERC721 debería verse así:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes { IERC721 public oldNFTToken; constructor(address _oldNFTTokenAddress) ERC721("NewNFTToken", "NNTK") EIP712("NewNFTToken", "1") { oldNFTToken = IERC721(_oldNFTTokenAddress); } // holder needs to approve this contract address before calling this method function stake(uint256 _tokenId) public { oldNFTToken.safeTransferFrom(msg.sender, address(this), _tokenId, "0x00"); // transfer token to this contract - stake safeMint(msg.sender, _tokenId); // mint a new vote token for staker } function withdraw(uint256 _tokenId) public { oldNFTToken.safeTransferFrom(address(this), msg.sender, _tokenId, "0x00"); _burn(_tokenId); // burn voteToken after withdraw } function safeMint(address _to, uint256 _tokenId) private { _safeMint(_to, _tokenId); } function onERC721Received( address operator, address from, uint256 id, uint256 value, bytes calldata data ) external returns (bytes4) { return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); } // The following functions are overrides required by Solidity. function _afterTokenTransfer( address from, address to, uint256 tokenId ) internal override(ERC721, ERC721Votes) { super._afterTokenTransfer(from, to, tokenId); } }


Eso es todo. Ahora puede usar el NewNFTToken en su gobernador.

3. Agregar un Gobernador

Ahora que hemos visto cómo actualizar ambos tipos de tokens para admitir la votación y la delegación para que funcionen con el contrato del gobernador, crearé los contratos que necesito para implementar mi gobernador. Primero, crearé un contrato Timelock:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts/governance/TimelockController.sol"; contract Timelock is TimelockController { constructor( uint256 _minDelay, address[] memory _proposers, address[] memory _executors ) TimelockController(_minDelay, _proposers, _executors) {} }


  1. Ahora, crearé mi contrato de gobernador. Debería verse así:


 // SPDX-License-Identifier: MIT pragma solidity 0.8.4; import "@openzeppelin/contracts/governance/Governor.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; contract MyGovernor is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { constructor(IVotes _token, TimelockController _timelock) Governor("MyGovernor") GovernorSettings( 1, /* 1 block */ 45818, /* 1 week */ 0 ) GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} // The following functions are overrides required by Solidity. function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) { return super.votingDelay(); } function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) { return super.votingPeriod(); } function quorum(uint256 blockNumber) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { return super.quorum(blockNumber); } function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { return super.state(proposalId); } function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) public override(Governor, IGovernor) returns (uint256) { return super.propose(targets, values, calldatas, description); } function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { return super.proposalThreshold(); } function _execute( uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override(Governor, GovernorTimelockControl) { super._execute(proposalId, targets, values, calldatas, descriptionHash); } function _cancel( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal override(Governor, GovernorTimelockControl) returns (uint256) { return super._cancel(targets, values, calldatas, descriptionHash); } function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { return super._executor(); } function supportsInterface(bytes4 interfaceId) public view override(Governor, GovernorTimelockControl) returns (bool) { return super.supportsInterface(interfaceId); } }


Ahora podemos compilar y generar las tipificaciones de nuestros contratos:


 yarn compile & yarn typechain


  1. Finalmente, crearé una tarea de casco para implementar nuestros contratos. Debería verse así:


 import { task } from "hardhat/config"; import { NewNFTToken, Timelock } from "../../src/types/contracts"; import { MyGovernor } from "../../src/types/contracts/Governor.sol"; import { NewNFTToken__factory, Timelock__factory } from "../../src/types/factories/contracts"; import { MyGovernor__factory } from "../../src/types/factories/contracts/Governor.sol"; task("deploy:Governance").setAction(async function (_, { ethers, run }) { const timelockDelay = 2; const tokenFactory: NewNFTToken__factory = await ethers.getContractFactory("NewNFTToken"); // replace with your existing token address const oldTokenAddress = ethers.constants.AddressZero; // old NFT token address const token: NewNFTToken = <NewNFTToken>await tokenFactory.deploy(oldTokenAddress); await token.deployed(); // deploy timelock const timelockFactory: Timelock__factory = await ethers.getContractFactory("Timelock"); const timelock: Timelock = <Timelock>( await timelockFactory.deploy(timelockDelay, [ethers.constants.AddressZero], [ethers.constants.AddressZero]) ); await timelock.deployed(); // deploy governor const governorFactory: MyGovernor__factory = await ethers.getContractFactory("MyGovernor"); const governor: MyGovernor = <MyGovernor>await governorFactory.deploy(token.address, timelock.address); await governor.deployed(); // get timelock roles const timelockExecuterRole = await timelock.EXECUTOR_ROLE(); const timelockProposerRole = await timelock.PROPOSER_ROLE(); const timelockCancellerRole = await timelock.CANCELLER_ROLE(); // grant timelock roles to governor contract await timelock.grantRole(timelockExecuterRole, governor.address); await timelock.grantRole(timelockProposerRole, governor.address); await timelock.grantRole(timelockCancellerRole, governor.address); console.log("Dao deployed to: ", { governor: governor.address, timelock: timelock.address, token: token.address, }); // etherscan verification await run("verify:verify", { address: token.address, constructorArguments: [oldTokenAddress], }); await run("verify:verify", { address: timelock.address, constructorArguments: [timelockDelay, [ethers.constants.AddressZero], [ethers.constants.AddressZero]], contract: "@openzeppelin/contracts/governance/TimelockController.sol:TimelockController", }); await run("verify:verify", { address: governor.address, constructorArguments: [token.address, timelock.address], }); });


Ahora, para implementar nuestro nuevo gobernador, podemos ejecutar:


 yarn hardhat deploy:Governance


Hemos completado este pequeño tutorial y hemos aprendido cómo actualizar nuestro contrato de token para que esté listo para DAO e implementar un contrato de gobernador para ellos. Puede crear un DAO e incluirlo en Tally con su nuevo gobernador y token.


Puede encontrar el código de muestra para este tutorial aquí .


También publicadoaquí .