Discovering OpenZeppelin Defender features with an NFT Game
Tags: blockchain, web3, openzeppelin, dapp
Published: 2022-05-10 18:08:55

[OpenZeppelin Defender](https://defender.openzeppelin.com) is a web-based application that allows developers to perform and automate smart contract operations securely. 

This blog post will show how some of the Defender features can be useful for interacting and securing a simple NFT Game.

In this game, users will obtain Hero NFTs by passing a captcha challenge. The NFT can then be used as voting power to propose the creation of a new guild in the game.
The system will attack these guilds from time to time, and their members will be able to repair the damages.

The code used in this project is available in the repository below.

{% github https://github.com/Markkop/guilds-openzeppelin-defender no-readme %}

## 🔮 NFT Contract

We start by creating the NFT contract with minting and voting power. Here, another OpenZeppelin tool becomes really useful. The [Wizard](https://docs.openzeppelin.com/contracts/4.x/wizard) can generate the contract we want with just a few clicks.

![NFT Contract](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vdhbwq1pylx3857vdo3b.png)

However, we will need to change it to add an anti-bot measurement.

## 🤖 Anti-bot NFT Minting

By using two Defender features, Relayer and Autotask, we can protect the NFT minting function behind a captcha challenge.

This idea and its original implementation were made by **tinchoabbate** on the [OpenZeppelin's Forum](https://forum.openzeppelin.com/t/human-first-nft-mints/21921). 

Here we will be replicating his code and changing just a few lines so we can move to other features. 
If you would like to understand the details of this implementation better, make sure to check the link above and the repository below.

{% github https://github.com/tinchoabbate/nft-minter-for-humans no-readme %}

It starts by modifying the NFT contract `safeMint` function and creating a `getCurrentTokenId` method in the Autotask script.

```solidity
...
// 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/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract Hero is ERC721, Ownable, EIP712, ERC721Votes, ReentrancyGuard {
    using Counters for Counters.Counter;

    // Address of OZ Defender's Relayer
    address private immutable _defender;
    Counters.Counter private _tokenIdCounter;

    constructor(address defender) ERC721("Hero", "HERO") EIP712("Hero", "1") {
        require(defender != address(0));
        _defender = defender;
    }

    function safeMint(
        address to,
        bytes32 hash,
        bytes memory signature
    ) 
        nonReentrant
        public 
    {
        uint256 tokenId = _tokenIdCounter.current();
        require(
            hash == keccak256(abi.encode(msg.sender, tokenId, address(this))),
            "Invalid hash"
        );
        require(
            ECDSA.recover(ECDSA.toEthSignedMessageHash(hash), signature) == _defender,
            "Invalid signature"
        );
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function getCurrentTokenId() public view returns (uint256){
        return _tokenIdCounter.current();
    }

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

Now we deploy the contract on Rinkeby, and using the NFT contract's address, we create a Defender Relayer by going to [this page](https://defender.openzeppelin.com/#/relay/new).

![Relayer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zsnuchk0uyoexvzq8p5m.png)

Then we [create an Autotask script](https://defender.openzeppelin.com/#/autotask/new) that will be triggered by a webhook post action.

![Autotask](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8oxy6m54flp3t5r1yc66.png)
 
The script is the same one created by tinchoabbate with a small contract abi change and can be seen [here](https://github.com/Markkop/guilds-openzeppelin-defender/blob/main/autotask/minter.js).

Finally, we build a front end with a captcha feature with [NextJS](https://nextjs.org/) and [hCaptcha](https://hcaptcha.com/).

Here I cloned the [existing app](https://github.com/tinchoabbate/nft-minter-for-humans/tree/main/app) built on the nft-minter-for-humans project to make sure that the whole process was working.

The important is to look there to check how the Autotask webhook URL is being integrated with the captcha verification.

## 🏰 Guilds Contract

Let's now see how the [Admin](https://defender.openzeppelin.com/#/admin) feature can help manage a governor contract.

We start by developing the Guilds contract, responsible for creating and storing guilds data.

```solidity
//SPDX-License-Identifier: Unlicense

import "hardhat/console.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Guilds is Ownable {
    using Counters for Counters.Counter;

    Counters.Counter public _guildIds;
    mapping(uint256 => Guild) public guildIdToGuild;

    struct Guild {
        uint256 guildId;
        bytes32 name;
        address[] members;
    }

    constructor() {
    }

    function createGuild(bytes32 name, address[] memory members) public onlyOwner {
        _guildIds.increment();
        uint256 guildId = _guildIds.current();

        guildIdToGuild[guildId] = Guild(
            guildId,
            name,
            members
        );
    }

    function getGuildMembers(uint256 guildId) public view returns (address[] memory) {
        return guildIdToGuild[guildId].members;
    }
}
```

Note that we're using two OpenZeppelin contracts: [Ownable](https://docs.openzeppelin.com/contracts/4.x/api/access#Ownable) and [Counters](https://docs.openzeppelin.com/contracts/4.x/api/utils#Counters).

Counters will help us to create guild ids, while Ownable is required to transfer ownership to the GuildsGovernor contract we'll be making next.

## 🏛️ GuildsGovernor Contract

With OpenZeppelin Wizard, we can again easily create a governor contract.

![Governor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hfl372gi2d0rxf9ppssk.png) 

With this contract deployed, we can now call `transferOwnership` from the Guilds contract with GuildsGovernor address to make it its new owner.

Now we can use OpenZeppelin Defender to propose a new guild using the `propose` method.

![New proposal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b14a0iseyu2r441e2501.png)
 
The tricky part here is to provide arguments for this function. If you want to learn more about OpenZeppelin Governance Contract, take a look at [this documentation](https://docs.openzeppelin.com/contracts/4.x/governance).

The `target` address is the contract we want the governor's contract to interact with. 
In the `value` field, we can input how much ETH we want to transfer.
Finally, `calldata` is the target encoded function with the parameters we want to send. 

This `calldata` value can be obtained in two ways:

### 🎧 Using Remix

1. Open Remix and create a new file pasting the Guilds code in it.
2. Deploy the contract in any environment.
3. [Convert](https://codebeautify.org/string-hex-converter) a guild name string to hex string.
4. Expand the `createGuild` method, provide the converted guild name (with `0x`), an array with any address and copy the encoded call data.

![Remix Calldata](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/deoewkrlvd6krr9gn0uq.png)

### ☁️ Using EthersJS

1. Create a Guilds contract instance by [deploying](https://docs.ethers.io/v4/api-contract.html#deployment) or [attaching](https://docs.ethers.io/v5/api/contract/contract/#Contract-attach) it.
2. Convert the guild name to a hex string using the code below.
3. Call the guilds contract interface with `encodeFunctionData`, passing the arguments, and copy the result.

```js
const guildName = ethers.utils.hexZeroPad(
  ethers.utils.hexlify(ethers.utils.toUtf8Bytes("MyGuild")),
  32
);
const encodedFunctionCall = guilds.interface.encodeFunctionData(
  "createGuild",
  [guildName, [user.address]]
);
console.log(encodedFunctionCall);
```

Now that we have the `calldata` value add it alongside the other parameters like in the screenshot above and run and approve the admin action with EOA (Externally Owned Account) like Metamask.

Voilá! We just used Defender Admin to interact with a Governor contract and propose an action.

The idea, of course, is allowing any holder to propose a guild creation. Having them set up a Defender's Admin action is not practical. 

We did it only as an example. Ideally, a custom frontend would be built on top of the contract for proposing and voting. Tools like [Tally](https://www.tally.xyz/) can be used as well.

## 🕒 Auto Attack a Guild

We've seen how to use Autotask by webhook triggers. Now let's try running it on a scheduled basis.

First, we have to change the Guild contract to let guilds have life values.

Then we create an `attackGuild` function to reduce a guild's life by one. This function can only be called by our relayer, so we will use [OpenZeppelin's AccessControl](https://docs.openzeppelin.com/contracts/4.x/) contract.

This is what the modified Guilds contract should look like:

```solidity
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract Guilds is Ownable, AccessControl {
    using Counters for Counters.Counter;
    bytes32 public constant AUTO_ATTACKER_ROLE = keccak256("AUTO_ATTACKER_ROLE");

    Counters.Counter public _guildIds;
    mapping(uint256 => Guild) public guildIdToGuild;

    struct Guild {
        uint256 guildId;
        bytes32 name;
        address[] members;
        uint256 maxLife;
        uint256 currentLife;
    }

    constructor(address autoAttackerAddress) {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(AUTO_ATTACKER_ROLE, autoAttackerAddress);
    }

    function createGuild(bytes32 name, address[] memory members) public onlyOwner {
        _guildIds.increment();
        uint256 guildId = _guildIds.current();

        guildIdToGuild[guildId] = Guild(
            guildId,
            name,
            members,
            10,
            10
        );
    }

    function getGuildMembers(uint256 guildId) public view returns (address[] memory) {
        return guildIdToGuild[guildId].members;
    }

    function attackGuild(uint256 guildId) public onlyRole(AUTO_ATTACKER_ROLE) {
        Guild storage guild = guildIdToGuild[guildId];
        require(guild.currentLife > 0, "Guild is dead");
        guild.currentLife -= 1;
    }

    function getGuildLife(uint256 guildId) public view returns (uint256) {
        return guildIdToGuild[guildId].currentLife;
    }
}
```

Note that now when deploying this contract, we need to provide the relayer address. We can use the one we have already created, but here I will be making a new one.

![Relayer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l04qyvkez2n9rne6mgkh.png)

This time we need to provide it funds to perform the `attackGuild` transaction. For this, we can simply transfer ether to its address.

Now we can create a new Autotask and attach it to the relayer. Let's set it up to run every minute just for testing, but we would change it for something like days or weeks.

The script we're using is the following one.

```js
const { ethers } = require("ethers");
const {
  DefenderRelayProvider,
  DefenderRelaySigner
} = require('defender-relay-client/lib/ethers');

const contractAbi = ["function attackGuild(uint256 guildId) public"];

exports.handler = async function(event) {
  const { guildsContractAddress } = event.secrets;
  const provider = new DefenderRelayProvider(event);
  const signer = new DefenderRelaySigner(event, provider, { speed: 'fast' });
  const guildsContract = new ethers.Contract(guildsContractAddress, contractAbi, signer);
  const attackTx = await guildsContract.attackGuild(1);
  console.log(attackTx);
  return attackTx.hash;
}
```

After creating the autotask, go back to the Autotask Dashboard, click on "Secrets" and add the key `guildsContractAddress` with the value of the guilds' contract address.

![Secrets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/smbmi4nl54ycj7mz3d33.png)

Now you will see `attackGuild` transactions appearing in the block explorer.

![Attacks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n11b0xfhnwun7pkok69b.png)

## 🏁 That's it!

At this point, you might have noticed that this is not a game yet. Holders can't heal their guilds, and nothing even happens when guilds reach zero life.

Other implementations still should be finished up, like adding/removing guild members, initializing the guild members with the ones that vote for its creation, and handling vote power for owners with multiple NFTs.

The goal of this blog post was to show some of the OpenZeppelin Defender features in a made-up context, and I think we can wrap things here.

Defender indeed can prove to be valuable or even indispensable for more significant and more relevant projects.

I can't wait to see what else will be added to the application.