将DAO治理添加到现有的代币合约中

475 阅读20分钟

Natacha De la Rosa解释了如何将你的NFT或ERC20代币合约变成一个DAO。你可以使用[OpenZeppelin]合约库添加一个[治理者合约]来管理你的DAO的提案和投票。你的代币合约需要一个**delegate()**、**delegates()**函数来将投票从一个用户委托给另一个用户。它需要对如何计算每个代币持有人的投票权有一个明确的定义。它还需要对投票的变化、代币的转移和授权的变化发出事件日志。

Tally

Tally是一个DAO操作平台

在我的上一篇文章我谈到了如何让你的NFT合约从第一天起就能准备好DAO。但如果你已经部署了你的NFT或ERC20代币合约而没有未来的DAO怎么办?你怎么能使用那个现有的代币添加DAO治理呢?让我们来了解一下。

你可以通过添加一个治理者合约来管理你的DAO的提案和投票,从而将你的代币合约变成一个DAO。但在这之前,你需要确保你的代币合约与治理者兼容。

治理者期望从代币合约中获得一个特定的接口。以下是它的需求摘要。

  • 你的代币合约需要一个delegate(),delegateBySig(), 和delegates() 函数来将投票从一个用户委托给另一个用户。

  • 你的代币合约需要对如何计算每个代币持有人的投票权有一个明确的定义。通常情况下,一个代币=一票,但你也可以根据代币供应情况定制一个公式。它需要有这些函数定义getVotes(),getPastVotes()getPastTotalSupply()

  • 最后但并非最不重要的是,你的代币合约需要为投票变化、代币转移和授权变化发出事件日志。它需要有这些特定的事件定义DelegateChanged,DelegateVotesChanged, 和Transfer

要获得更多关于这些函数和事件签名的信息,以及它们应该返回什么,请阅读Open Zeppelin IVotes定义

现在你知道了你的代币合约需要什么,我可以解释你如何实现这一点。有两种方法,这取决于你首先是如何创建你的代币合约的。

通过OpenZeppelin合约库,我可以根据我的代币类型添加ERC20VotesERC721Votes,并覆盖一些必要的方法,即使我的合约不可升级。

如果你的合约是可升级的,你需要创建一个新的实现,并更新代理,使其实现你的代币合约所需的额外库。

如果你的合约不可升级,你将创建一个具有我上面提到的功能的额外合约。然后你需要允许用户他们持有的代币抵押给新的代币。

如果这是你的情况,你有最简单的前进道路。

你需要有合同的管理权限来执行这些变化。如果你没有,你将无法更新可升级合约的代币实现。

首先,让我们假设我有以下的代币合约,我已经在Rinkeby上部署。它不支持委托或任何其他投票功能,但它是可升级的。

// 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);
    }
}

现在,我需要更新我的代币,使其有一个新的实现,像这样支持委托和其他投票能力。

// 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);
    }
}

之后,我可以运行更新任务,我的合同将有我需要的新实现。

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 = 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,
  });
});

就这样,现在我的代币合约已经拥有了与我的治理者合约一起使用所需的一切

如果这是你的情况,不要担心。一切都有解决办法。你还需要几个步骤,但没有什么太复杂的。

我将首先创建一个新的合同;你可以使它可升级或不可升级,但在这个例子中,我将使它不可升级以显示双方。如果你想把它创建为可升级的,你可以参考我之前创建的那个。

我将在这个新的代币合约中实现我需要的功能,使我的代币与治理者合约兼容。然后,我将创建两个额外的功能,允许我的代币用户将他们的代币入股,并从这个合约中提取

你还需要创建一个简单的用户界面,让你的用户做这些动作。这就是你需要做的所有事情。让我们直接跳入它。

假设我有这个已经部署在Rinkeby的ERC721代币,但它没有任何可升级的能力。

// 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);
    }
}

在这种情况下,我需要创建一个新的ERC721代币,它具有我需要的DAO功能。这个新合约将允许当前的代币持有人存入他们拥有的代币。然后,他们可以收到新的投票代币并参与到我的DAO中。代币持有者也将可以选择撤回他们抵押的代币。

让我们首先像这样创建新的ERC721代币。

// 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);
    }
}

我的新ERC721代币合约应该有这些功能。

  • 可造币。我将使用这个功能为我的代币持有者铸造一个独特的代币,每次他们他们持有的OldNFTToken的代币押入 NewNFTToken

  • 可烧毁。当持有者提取NewNFTToken时,我将使用这个功能来烧掉已铸造的NewNFTToken

现在我将在我的新合约中添加两个新的函数来管理押注提取过程。

  1. 首先,创建一个状态变量来接收当前的NFT合约地址。
contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes {
    // ...
    IERC721 public oldNFTToken;
    // ...
  }
  1. 现在,我将更新构造函数以接收我的OldNFTToken的地址**。**
contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes {
    // ...

    constructor(address _oldNFTTokenAddress) ERC721("NewNFTToken", "NNTK") EIP712("NewNFTToken", "1") {
        oldNFTToken = IERC721(_oldNFTTokenAddress);
    }
    
    // ...
}
  1. 我还将更新safeMint函数,使其看起来像这样。
contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes {
  // ...
  function safeMint(address _to, uint256 _tokenId) private {
       _safeMint(_to, _tokenId);
   }
  // ...
}
  1. 然后,我将加入入股提款的函数
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. 最后但并非最不重要的是,我将把ERC721Receiver实现添加到我的合约中,以便它能支持安全转移。
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
    }
    
    // ...
}

现在,新的ERC721代币应该看起来像这样。

// 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);
    }
}

就这样了。现在你可以在你的治理者中使用NewNFTToken了。

现在我们已经看到了如何更新两种类型的代币以支持投票和委托,以便它们与治理者合同一起工作,我将创建我需要的合同来部署我的治理者。首先,我将创建一个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. 现在,我将创建我的治理者合同。它应该看起来像这样。
// 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);
    }
}

现在我们可以编译生成我们的合同的类型了。

yarn compile & yarn typechain
  1. 最后,我将创建一个硬帽任务来部署我们的合同。它应该是这样的。
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 = await tokenFactory.deploy(oldTokenAddress);
  await token.deployed();

  // deploy timelock
  const timelockFactory: Timelock__factory = await ethers.getContractFactory("Timelock");
  const 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 = 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],
  });
});

现在,为了部署我们的新治理者,我们可以运行。

yarn hardhat deploy:Governance

我们已经完成了这个小教程,我们已经学会了如何更新我们的代币合约,使之成为DAO-ready,并为它们部署一个治理者合约。你可以创建一个DAO,并用你的新治理者和代币在Tally上列出它

你可以在这里找到本教程的示例代码。

也发表在这里


参加区块链写作大赛

在我的上一篇文章我谈到了如何让你的NFT合约从第一天起就能成为DAO。但如果你已经部署了你的NFT或ERC20代币合约而没有未来的DAO怎么办?你怎么能使用那个现有的代币添加DAO治理呢?让我们来了解一下。

你可以通过添加一个治理者合约来管理你的DAO的提案和投票,从而将你的代币合约变成一个DAO。但在这之前,你需要确保你的代币合约与治理者兼容。

治理者期望从代币合约中获得一个特定的接口。以下是它的需求摘要。

  • 你的代币合约需要一个delegate(),delegateBySig(), 和delegates() 函数来将投票从一个用户委托给另一个用户。

  • 你的代币合约需要对如何计算每个代币持有人的投票权有一个明确的定义。通常情况下,一个代币=一票,但你也可以根据代币供应情况定制一个公式。它需要有这些函数定义getVotes(),getPastVotes()getPastTotalSupply()

  • 最后但并非最不重要的是,你的代币合约需要为投票变化、代币转移和授权变化发出事件日志。它需要有这些特定的事件定义DelegateChanged,DelegateVotesChanged, 和Transfer

要获得更多关于这些函数和事件签名的信息,以及它们应该返回什么,请阅读Open Zeppelin IVotes定义

现在你知道了你的代币合约需要什么,我可以解释你如何实现这一点。有两种方法,这取决于你首先是如何创建你的代币合约的。

通过OpenZeppelin合约库,我可以根据我的代币类型添加ERC20VotesERC721Votes,并覆盖一些必要的方法,即使我的合约不可升级。

如果你的合约是可升级的,你需要创建一个新的实现,并更新代理,使其实现你的代币合约所需的额外库。

如果你的合约不可升级,你将创建一个具有我上面提到的功能的额外合约。然后你需要允许用户他们持有的代币抵押给新的代币。

如果这是你的情况,你有最简单的前进道路。

你需要有合同的管理权限来执行这些变化。如果你没有,你将无法更新可升级合约的代币实现。

首先,让我们假设我有以下的代币合约,我已经在Rinkeby上部署。它不支持委托或任何其他投票功能,但它是可升级的。

// 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);
    }
}

现在,我需要更新我的代币,使其有一个新的实现,像这样支持委托和其他投票能力。

// 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);
    }
}

之后,我可以运行更新任务,我的合同将有我需要的新实现。

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 = 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,
  });
});

就这样,现在我的代币合约已经拥有了与我的治理者合约一起使用所需的一切

如果这是你的情况,不要担心。一切都有解决办法。你还需要几个步骤,但没有什么太复杂的。

我将首先创建一个新的合同;你可以使它可升级或不可升级,但在这个例子中,我将使它不可升级以显示双方。如果你想把它创建为可升级的,你可以参考我之前创建的那个。

我将在这个新的代币合约中实现我需要的功能,使我的代币与治理者合约兼容。然后,我将创建两个额外的功能,允许我的代币用户将他们的代币入股,并从这个合约中提取

你还需要创建一个简单的用户界面,让你的用户进行这些操作。这就是你需要做的所有事情。让我们直接跳入它。

假设我有这个已经部署在Rinkeby的ERC721代币,但它没有任何可升级的能力。

// 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);
    }
}

在这种情况下,我需要创建一个新的ERC721代币,它具有我需要的DAO功能。这个新合约将允许当前的代币持有人存入他们拥有的代币。然后,他们可以收到新的投票代币并参与到我的DAO中。代币持有者也将可以选择撤回他们抵押的代币。

让我们首先像这样创建新的ERC721代币。

// 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);
    }
}

我的新ERC721代币合约应该有这些功能。

  • 可造币。我将使用这个功能为我的代币持有者铸造一个独特的代币,每次他们他们持有的OldNFTToken的代币押入 NewNFTToken

  • 可烧毁。当持有者提取NewNFTToken时,我将使用这个功能来烧掉已铸造的NewNFTToken

现在我将在我的新合约中添加两个新的函数来管理押注提取过程。

  1. 首先,创建一个状态变量来接收当前的NFT合约地址。
contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes {
    // ...
    IERC721 public oldNFTToken;
    // ...
  }
  1. 现在,我将更新构造函数以接收我的OldNFTToken的地址**。**
contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes {
    // ...

    constructor(address _oldNFTTokenAddress) ERC721("NewNFTToken", "NNTK") EIP712("NewNFTToken", "1") {
        oldNFTToken = IERC721(_oldNFTTokenAddress);
    }
    
    // ...
}
  1. 我还将更新safeMint函数,使其看起来像这样。
contract NewNFTToken is ERC721, Ownable, EIP712, ERC721Votes {
  // ...
  function safeMint(address _to, uint256 _tokenId) private {
       _safeMint(_to, _tokenId);
   }
  // ...
}
  1. 然后,我将加入入股提款的函数
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. 最后但并非最不重要的是,我将在我的合约中添加ERC721Receiver的实现,以便它能够支持安全转移。
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
    }
    
    // ...
}

现在,新的ERC721代币应该看起来像这样。

// 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);
    }
}

就这样了。现在你可以在你的治理者中使用NewNFTToken了。

现在我们已经看到了如何更新两种类型的代币以支持投票和委托,以便它们与治理者合同一起工作,我将创建我需要的合同来部署我的治理者。首先,我将创建一个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. 现在,我将创建我的治理者合同。它应该看起来像这样。
// 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);
    }
}

现在我们可以编译生成我们的合同的类型了。

yarn compile & yarn typechain
  1. 最后,我将创建一个硬帽任务来部署我们的合同。它应该是这样的。
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 = await tokenFactory.deploy(oldTokenAddress);
  await token.deployed();

  // deploy timelock
  const timelockFactory: Timelock__factory = await ethers.getContractFactory("Timelock");
  const 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 = 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],
  });
});

现在,为了部署我们的新治理者,我们可以运行。

yarn hardhat deploy:Governance

我们已经完成了这个小教程,我们已经学会了如何更新我们的代币合约,使其成为DAO-ready并为其部署一个治理者合约。你可以创建一个DAO,并用你的新治理者和代币在Tally上列出它

你可以在这里找到本教程的示例代码。

也发表在这里

标签

相关故事