Dig

2024-06-10


The original Warpcast frame is redacted, but if you searching for "I've got the Dig" on Warpcast, you might be able to find a frame of the game that somebody shared.



View Dig NFT on the Ham blockchain

The Frame

Dig is a frame game on Farcaster. The objective of the game is to hold the Dig NFT at the right time, because every Monday at a random time, the holder will receive 1k of an ERC 20 from the Dig contract on Base. The ERC 20 contract is here. It can receive any ERC 20. For the automated payouts, the token addesses must be whitelisted by me. This is to prevent the diggers who play the game from receiving spammy tokens. The current whitelisted tokens are up to date on the Dig website. The Dig NFT is a 1 of 1 and cannot be transferred. So the only way to get the NFT is to use the frame. The game launched today and about 400 people have dug the NFT. Since the NFT is on the Ham blockchain, an L3, total gas cost has been less than $1. Search for I've got the Dig on Warpcast to see the game in action.

View the Solidity code for the Dig NFT below. You can see it's a standard ERC 721 contract with a few tweaks to disable transfers except by the owner. The only transfers that occured were done programmically by my frames backend.

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.25;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Dig is Ownable, ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    uint256 public maxSupply; 
    mapping(uint256 => bool) public _isTokenMinted;

    constructor(address initialOwner) ERC721("Dig", "DIG") Ownable(initialOwner) {
        transferOwnership(initialOwner);
        maxSupply = 1;
    }

    function setMaxSupply(uint256 newMaxSupply) public onlyOwner {
        maxSupply = newMaxSupply;
    }

    function mintNFT(address recipient, string memory tokenURI) public onlyOwner {
        require(_tokenIds.current() < maxSupply, "Max supply reached");
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);
        _isTokenMinted[newItemId] = true;
    }

    function getRemainingNFTs() public view returns (uint256) {
        return maxSupply - _tokenIds.current();
    }

    function totalSupply() public view returns (uint256) {
        return maxSupply;
    }

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

    function transferGrail(address from, address to, uint256 tokenId) public onlyOwner {
        require(_isTokenMinted[tokenId], "Token does not exist");
        _transfer(from, to, tokenId);
    }

    modifier onlyOwnerTransfer() {
        require(msg.sender == owner(), "Transfers are disabled");
        _;
    }

    function transferFrom(address from, address to, uint256 tokenId) public override(ERC721, IERC721) onlyOwnerTransfer {
        super.transferFrom(from, to, tokenId);
    }

    function approve(address /*to*/, uint256 /*tokenId*/) public override(ERC721, IERC721) {
        revert("Approvals are disabled");
    }

    function setApprovalForAll(address /*operator*/, bool /*approved*/) public override(ERC721, IERC721) {
        revert("Set approval for all is disabled");
    }
}


The tokens contract that stored the ERC 20s is below as well. You can see that it was not able to see ETH directly, and there's a few functions for the owner to manage whitelisted token addresses. There's also a transfer function which was called programmically by the frame backend to send the winner their tokens for the week.
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.25;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DigTokens is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;
    
    mapping(address => bool) public whitelistedTokens;

    event TokenWhitelisted(address indexed tokenAddress);
    event TokenRemovedFromWhitelist(address indexed tokenAddress);
    event TokensTransferred(address indexed tokenAddress, address indexed to, uint256 amount);

    constructor(address initialOwner) Ownable(initialOwner) {
        transferOwnership(initialOwner);
    }

    function addTokenToWhitelist(address tokenAddress) public onlyOwner {
        whitelistedTokens[tokenAddress] = true;
        emit TokenWhitelisted(tokenAddress);
    }

    function removeTokenFromWhitelist(address tokenAddress) public onlyOwner {
        whitelistedTokens[tokenAddress] = false;
        emit TokenRemovedFromWhitelist(tokenAddress);
    }

    receive() external payable {
        revert("Contract does not accept Ether directly");
    }

    function getERC20Balance(address tokenAddress) public view returns (uint256) {
        IERC20 token = IERC20(tokenAddress);
        return token.balanceOf(address(this));
    }

    function transferTokens(address tokenAddress, address to, uint256 amount) public onlyOwner nonReentrant {
        require(whitelistedTokens[tokenAddress], "Token address not whitelisted");
        
        IERC20 token = IERC20(tokenAddress);

        uint256 contractBalance = token.balanceOf(address(this));
        require(contractBalance >= amount, "Not enough tokens in contract");

        token.safeTransfer(to, amount);
        emit TokensTransferred(tokenAddress, to, amount);
    }
}

FCTwitterZora
© 2024