XLR8: A Blockchain-Powered Car Racing Game

I teamed up with a few friends at Duke to build a blockchain-powered car racing game called XLR8. In this game, users acquire individual car component NFTs and "fuse" them to create their own unique car NFTs. I wrote the custom ERC721 and ERC1155 smart contracts, deployed a custom Chainlink node on AWS EC2 with Docker, and used AWS Lambda for a secure on-to-off-chain data link to MongoDB. XLR8 remains as one of the most technically ambitious NFT projects to date.

Check out the repo here

XLR8 Cybertruck
Above: XLR8 Cybertruck

Technicals

The XLR8 smart contracts are written in Solidity and deployed on the Ethereum mainnet. The most challenging part of the project was restraining people from finding all of the premade cars before people "found" them by fusing the appropriate components together (IPFS folders are public, so if you put them all together, anyone can scan the whole folder). To do this, I uploaded all of the premade cars to IPFS in separate folders, stored all of their IPFS CIDs in MongoDB, and then built a custom Chainlink node to pull the CID from MongoDB when a user fused their cars. This system required launching a custom Chainlink node on AWS EC2 with Docker, and then building a Chainlink External Adapter to trigger the query from on-chain securely, which I deployed on AWS Lambda. The Chainlink node code is open source on their website, and my External Adapter code is open source and can be found here.

XLR8 Jeep
Above: XLR8 Jeep

Tools

I built XLR8 with this stack:

  1. Solidity
  2. MongoDB
  3. AWS Lambda
  4. AWS EC2
  5. Docker
  6. Chainlink
  7. IPFS
XLR8 Optimus Prime
Above: XLR8 Optimus Prime
Below: full car smart contract for XLR8 cars

// SPDX-License-Identifier: MIT
// File: Car.sol -- Full Car contract for XLR8
pragma solidity ^0.8.10;

import '@openzeppelin/contracts/token/ERC721/ERC721.sol'
import '@openzeppelin/contracts/access/Ownable.sol'
import '@openzeppelin/contracts/utils/Counters.sol'
import '@openzeppelin/contracts/utils/Strings.sol'

// Deploy first, before the xlr8 minter contract address
contract ComponentNFT is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private \_tokenIds;
using Strings for uint256;

    address public XLR8Minter;
    address public Car;
    uint256 public maxSupply;

    string public baseURI;
    uint256 public offset = 0;

    string public constant PROVENANCE = "8sfeew8_3r283LJFSDF8nfsf3n"; // Hardcode this at launch

    event NFTCreated (
        uint256 indexed tokenId,
        address indexed creatorAddress
    );

    constructor(uint256 _maxSupply) ERC721("XLR8 FULL CAR", "XLR8") {
        maxSupply = _maxSupply;
        baseURI = "Pre-reveal mystery URI here"; // Pre-reveal mystery URI
    }

    function setCarContractAddress(address _address) public onlyOwner {
        Car = _address;
        setApprovalForAll(Car, true); // Allows car fusing function to call transferFrom
    }

    function setXLR8MinterAddress(address _address) public onlyOwner {
        XLR8Minter = _address;
    }

    modifier onlyMinter() {
        require(msg.sender == XLR8Minter, "Only the XLR8 Minter contract can mint components");
        _;
    }

    function mintFromMinter(address _msgSender) public onlyMinter returns (bool) {
        _tokenIds.increment();
        uint _tokenId = _tokenIds.current();
        require(_tokenId < maxSupply, "Max supply already reached");

        _safeMint(_msgSender, _tokenId);
        emit NFTCreated(_tokenId, _msgSender);

        return true;
    }

    // Credit to Vox Collectibles team for this offset method
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        if (offset == 0) {
            return bytes(baseURI).length > 0 ? baseURI : "";
        } else {
            uint256 newId = (tokenId + offset) % maxSupply;
            return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, newId.toString())) : "";
        }
    }

    function setBaseURI(string calldata _baseURI) internal onlyOwner {
        baseURI = _baseURI;
    }

    function setOffset(uint256 _offset) public onlyMinter {
        offset = _offset;
    }

}