Halborn Logo

Moonscape - Seascape


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/26/2024

Date of Engagement by: February 4th, 2022 - March 25th, 2022

Summary

100% of all REPORTED Findings have been addressed

All findings

7

Critical

0

High

0

Medium

2

Low

3

Informational

2


1. INTRODUCTION

Seascape engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-02-04 and ending on 2022-03-25. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn was provided seven weeks for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended

    • Identify potential security issues with the smart contracts

In summary, Halborn identified some security risks that were mostly addressed by the Seascape team.

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the bridge code and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:

    • Research into architecture and purpose

    • Smart contract manual code review and walkthrough

    • Graphing out functionality and contract logic/connectivity/functions (solgraph)

    • Manual assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes

    • Manual testing by custom scripts

    • Static Analysis of security for scoped contract, and imported functions. (Slither)

    • Testnet deployment (Brownie, Remix IDE)

4. SCOPE

The security assessment was scoped to the following Seastarinteractive/moonscape-smartcontracts

    • /contracts/game/MoonscapeGame.sol

    • /contracts/nfts/CityNft.sol

    • /contracts/nfts/RoverNft.sol

Commit ID: 7f01159e97c132b6bbad5d5511768461d2d480c9

    • /contracts/defi/MoonscapeDefi.sol

    • /contracts/defi/Stake.sol

    • /contracts/beta/MoonscapeBeta.sol

Commit ID: ba85acb2bf7b893d338b9150c2273305e5303eb7

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

2

Low

3

Informational

2

Impact x Likelihood

HAL-02

HAL-01

HAL-03

HAL-04

HAL-06

HAL-07

HAL-05

Security analysisRisk levelRemediation Date
UNTRUSTED TOKENS NOT CHECKING BEFORE AND AFTER BALANCEMediumSolved - 04/04/2022
POSSIBLE SIGNATURE RE-USAGEMediumSolved - 03/31/2022
MULTIPLE ACTIVE SESSIONSLowRisk Accepted
MISSING PARAMETERS VALIDATIONLowSolved - 04/04/2022
UNUSED MINTED VARIABLELowSolved - 04/06/2022
STORAGE USAGE ON READ ONLY FLOWInformationalSolved - 03/31/2022
UNUSED PARAMETERSInformationalSolved - 04/04/2022

8. Findings & Tech Details

8.1 UNTRUSTED TOKENS NOT CHECKING BEFORE AND AFTER BALANCE

// Medium

Description

None

Code Location

contracts/defi/MoonscapeDefi.sol

    function _safeTransfer(address _token, address _to, uint256 _amount) internal {
        if (_token != address(0)) {
            IERC20 _rewardToken = IERC20(_token);

            uint256 _balance = _rewardToken.balanceOf(address(this));
            if (_amount > _balance) {
                _rewardToken.transfer(_to, _balance);
            } else {
                _rewardToken.transfer(_to, _amount);
            }
        } else {
            uint256 _balance = address(this).balance;
            if (_amount > _balance) {
                payable(_to).transfer(_balance);
            } else {
                payable(_to).transfer(_amount);
            }
        }
    }

contracts/defi/MoonscapeDefi.sol

    function addTokenStaking(uint _sessionId, address stakeAddress, uint rewardPool, address rewardToken, uint _burn) external {
        bytes32 key = keccak256(abi.encodePacked(_sessionId, stakeAddress, rewardToken));

        require(!addedStakings[key], "DUPLICATE_STAKING");

        addedStakings[key] = true;
        bool burn = true;

        if (_burn == 1) {
            burn = true;
        } else {
            burn = false;
        }

        tokenStakings[++stakeId] = TokenStaking(_sessionId, stakeAddress, rewardPool, rewardToken, burn);

        bytes32 stakeKey = stakeKeyOf(sessionId, stakeId);

        keyToId[stakeKey] = stakeId;

        Session storage session = sessions[_sessionId];

        newStakePeriod(
            stakeKey,
            session.startTime,
            session.endTime,
            rewardPool    
        );

        emit AddStaking(_sessionId, stakeId);
    }
Score
Impact: 5
Likelihood: 2
Recommendation

SOLVED: The code does check the post balance and compares it to the sum of the pre-balance and the requested amount.

8.2 POSSIBLE SIGNATURE RE-USAGE

// Medium

Description

None

Code Location

contracts/defi/MoonscapeDefi.sol

    function stakeToken(uint _stakeId, uint _cityId, uint _buildingId, uint _amount, uint8 v, bytes32[2] calldata sig) external {
        TokenStaking storage tokenStaking = tokenStakings[_stakeId];

        // todo
        // validate the session id
        bytes32 stakeKey = stakeKeyOf(tokenStaking.sessionId, _stakeId);

        require(isActive(stakeKey), "session not active");

        //validate stake id
        require(_stakeId <= stakeId,"do not have this stakeId");

        {
        bytes memory prefix     = "\x19Ethereum Signed Message:\n32";
        bytes32 message         = keccak256(abi.encodePacked(_stakeId, tokenStaking.sessionId, _cityId, _buildingId));
        bytes32 hash            = keccak256(abi.encodePacked(prefix, message));
        address recover         = ecrecover(hash, v, sig[0], sig[1]);

        require(recover == owner(), "Verification failed about stakeToken");
        }

        deposit(stakeKey, msg.sender, _amount);

        IERC20 token = IERC20(tokenStaking.stakeToken);

        require(token.balanceOf(msg.sender) >= _amount, "Not enough token to stake");
        // uint preBalance = token.balanceOf(address(this));

        token.safeTransferFrom(msg.sender, address(this), _amount);
        // _amount = token.balanceOf(address(this)) - preBalance;
        emit StakeToken(msg.sender, tokenStaking.sessionId, _stakeId, _cityId, _buildingId, _amount);
    }

contracts/defi/MoonscapeDefi.sol

    function stakeNft(uint _stakeId, uint _cityId, uint _buildingId, uint _scapeNftId, uint _power, uint8 _v, bytes32[2] calldata sig) external {
        TokenStaking storage tokenStaking = tokenStakings[_stakeId];

        // validate the session id
        bytes32 stakeKey = stakeKeyOf(tokenStaking.sessionId, _stakeId);

        Player storage player = playerParams[stakeKey][msg.sender];

        require(player.nftId <= 0 && player.power <= 0, "already stake nft");

        require(isActive(stakeKey), "session not active");

        //validate stake id
        require(_stakeId <= stakeId,"do not have this stakeId");

        IERC721 nft = IERC721(tokenStaking.stakeToken);
        require(nft.ownerOf(_scapeNftId) == msg.sender, "not owned");

        {
        bytes memory prefix     = "\x19Ethereum Signed Message:\n32";
        bytes32 message         = keccak256(abi.encodePacked(tokenStaking.sessionId, _stakeId, _cityId, _buildingId, _scapeNftId, _power));
        bytes32 hash            = keccak256(abi.encodePacked(prefix, message));
        address recover         = ecrecover(hash, _v, sig[0], sig[1]);

        require(recover == owner(), "Verification failed about stakeNft");
        }

        nft.safeTransferFrom(msg.sender, address(this), _scapeNftId);

        deposit(stakeKey, msg.sender, _power);

        player.nftId = _scapeNftId;
        player.power = _power;

        emit StakeNft(msg.sender, tokenStaking.sessionId, _stakeId, _cityId, _buildingId, _scapeNftId, _power);
    }

contracts/defi/MoonscapeDefi.sol

    function getBonus(uint _stakeId, uint _cityId, uint _buildingId, uint _bonusPercent, uint8 _v, bytes32[2] calldata sig) external {
        TokenStaking storage tokenStaking = tokenStakings[_stakeId];
        Session storage session = sessions[tokenStaking.sessionId];
        bytes32 stakeKey = stakeKeyOf(tokenStaking.sessionId, _stakeId);

        require(block.timestamp > session.endTime, "it has to be after the session");

        Player storage player = playerParams[stakeKey][msg.sender];

        require(player.receiveBonus == true, "already rewarded");

        {
        bytes memory prefix     = "\x19Ethereum Signed Message:\n32";
        bytes32 message         = keccak256(abi.encodePacked(_stakeId, tokenStaking.sessionId, _cityId, _buildingId, _bonusPercent));
        bytes32 hash            = keccak256(abi.encodePacked(prefix, message));
        address recover         = ecrecover(hash, _v, sig[0], sig[1]);

        require(recover == owner(), "Verification failed about getBonus");
        }

        uint256 _totalreward = claimable(stakeKey, msg.sender) + player.claimedAmound;
        uint256 _totalBonus  = _totalreward.mul(scaler).mul(_bonusPercent).div(100).div(scaler);

        require(_totalBonus > 0, "totalBonus must > 0");
        _safeTransfer(tokenStaking.rewardToken, msg.sender, _totalBonus);

        player.receiveBonus = true;
    }
Score
Impact: 5
Likelihood: 1
Recommendation

SOLVED: Code is not checking for msg.sender in signatures.

8.3 MULTIPLE ACTIVE SESSIONS

// Low

Description

None

Code Location

contracts/defi/MoonscapeDefi.sol

    function startSession(uint _startTime, uint _endTime) external {
        require(validSessionTime(_startTime, _endTime), "INVALID_SESSION_TIME");

        sessionId++;

        sessions[sessionId] = Session(_startTime, _endTime, true);

        emit StartSession(sessionId, _startTime, _endTime);
    }

contracts/defi/MoonscapeDefi.sol

    function validSessionTime(uint _startTime, uint _endTime) public view returns(bool) {
        Session storage session = sessions[sessionId];

        if (_startTime > session.endTime && _startTime >= block.timestamp && _startTime < _endTime) {
            return true;
        }

        return false;
    }
Score
Impact: 3
Likelihood: 2
Recommendation

RISK ACCEPTED: The code is protected with the onlyOwner modifier, so only the owner can add new sessions. Plus, the client states that: "This is done on purpose because if we by any chance make a mistake, we would need to redeploy everything. But with multiple active sessions, we can just open another session and change the active session in the client"

8.4 MISSING PARAMETERS VALIDATION

// Low

Description

None

Code Location

game/MoonscapeGame.sol

    constructor(
        address _mscpToken,
        address _cityNft,
        address _roverNft,
        address _scapeNft,
        address _verifier,
        address _feeTo
    ) public {
        MSCP = _mscpToken;
        cityNft = _cityNft;
        roverNft = _roverNft;
        scapeNft = _scapeNft;
        verifier = _verifier;
        feeTo = _feeTo;
    }
Score
Impact: 2
Likelihood: 3
Recommendation

SOLVED: The code is now checking if the parameters are not zero.

8.5 UNUSED MINTED VARIABLE

// Low

Description

None

Code Location

nfts/CityNft.sol

    function mint(uint _tokenId, uint8 _category, address _to) external returns(bool) {
        if (!minters[msg.sender] || minted[_tokenId] || _to == address(0) || _category > 8) {
            return false;
        }

          categoryOf[_tokenId] = _category;

        _safeMint(_to, _tokenId);

          emit Minted(_to, _tokenId, _category, block.timestamp);
          return true;
    }

nfts/RoverNft.sol

    function mint(uint _tokenId, uint8 _type, address _to) external returns(bool) {
        if (!minters[msg.sender] || minted[_tokenId] || _to == address(0) || _type > 8) {
            return false;
        }

          typeOf[_tokenId] = _type;

        _safeMint(_to, _tokenId);

          emit Minted(_to, _tokenId, _type, block.timestamp);
          return true;
    }
Score
Impact: 1
Likelihood: 3
Recommendation

SOLVED: Variables were removed from the parameters and the contract storage.

8.6 STORAGE USAGE ON READ ONLY FLOW

// Informational

Description

None

Code Location

contracts/defi/MoonscapeDefi.sol

function addTokenStaking(uint _sessionId, address stakeAddress, uint rewardPool, address rewardToken, uint _burn) external {
        bytes32 key = keccak256(abi.encodePacked(_sessionId, stakeAddress, rewardToken));

        require(!addedStakings[key], "DUPLICATE_STAKING");

        addedStakings[key] = true;
        bool burn = true;

        if (_burn == 1) {
            burn = true;
        } else {
            burn = false;
        }

        tokenStakings[++stakeId] = TokenStaking(_sessionId, stakeAddress, rewardPool, rewardToken, burn);

        bytes32 stakeKey = stakeKeyOf(sessionId, stakeId);

        keyToId[stakeKey] = stakeId;

        Session storage session = sessions[_sessionId];

        newStakePeriod(
            stakeKey,
            session.startTime,
            session.endTime,
            rewardPool    
        );

        emit AddStaking(_sessionId, stakeId);
    }

contracts/defi/MoonscapeDefi.sol

function validSessionTime(uint _startTime, uint _endTime) public view returns(bool) {
        Session storage session = sessions[sessionId];

        if (_startTime > session.endTime && _startTime >= block.timestamp && _startTime < _endTime) {
            return true;
        }

        return false;
    }
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The memory keyword is now used instead of storage.

8.7 UNUSED PARAMETERS

// Informational

Description

None

Code Location

game/MoonscapeGame.sol

    function mintRover(uint _id, uint8 _type, uint  _amount, uint8 _v, bytes32 _r, bytes32 _s) external {
       {   // avoid stack too deep
       // investor, project verification
    bytes memory prefix     = "\x19Ethereum Signed Message:\n32";
    bytes32 message         = keccak256(abi.encodePacked(msg.sender, address(this), roverNft, _id, _amount, _type));
    bytes32 hash            = keccak256(abi.encodePacked(prefix, message));
    address recover         = ecrecover(hash, _v, _r, _s);

    require(recover == verifier, "sig");
       }

       CityNft nft = CityNft(roverNft);
       require(nft.mint(_id, _type, msg.sender), "Failed to mint rover");

       emit MintRover(msg.sender, _amount, _id, _type);
   }

game/MoonscapeGame.sol

    function mintCity(uint _id, uint8 _category, uint _amount, uint8 _v, bytes32 _r, bytes32 _s) external {
       {   // avoid stack too deep
       // investor, project verification
    bytes memory prefix     = "\x19Ethereum Signed Message:\n32";
    bytes32 message         = keccak256(abi.encodePacked(msg.sender, address(this), cityNft, _id, _amount, _category));
    bytes32 hash            = keccak256(abi.encodePacked(prefix, message));
    address recover         = ecrecover(hash, _v, _r, _s);

    require(recover == verifier, "sig");
       }

       CityNft nft = CityNft(cityNft);
       require(nft.mint(_id, _category, msg.sender), "Failed to mint city");

       emit MintCity(msg.sender, _amount, _id, _category);
   }
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: Removed _amount variable from function arguments.

9. Review Notes

Call Graph

digraph G {
  graph [ ratio = "auto", page = "100", compound =true, bgcolor = "#2e3e56" ];
  node [ style = "filled", fillcolor = "#edad56", color = "#edad56", penwidth =3 ];
  edge [ color = "#fcfcfc", penwidth =2, fontname = "helvetica Neue Ultra Light" ];
subgraph "clusterMoonscapeGame" {
  graph [ label = "MoonscapeGame", color = "#445773", fontcolor = "#f0f0f0", style = "rounded", bgcolor = "#445773" ];
  "MoonscapeGame.<Constructor>" [ label = "<Constructor>", color = "#FF9797", fillcolor = "#FF9797" ];
  "MoonscapeGame.purchaseMoondust" [ label = "purchaseMoondust", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.stakeForMoondust" [ label = "stakeForMoondust", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.unstakeForMoondust" [ label = "unstakeForMoondust", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.importCity" [ label = "importCity", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.exportCity" [ label = "exportCity", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.mintCity" [ label = "mintCity", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.burnScapeForBuilding" [ label = "burnScapeForBuilding", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.burnScapeForConnection" [ label = "burnScapeForConnection", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.importRover" [ label = "importRover", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.exportRover" [ label = "exportRover", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.mintRover" [ label = "mintRover", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.onERC721Received" [ label = "onERC721Received", color = "#ffbdb9", fillcolor = "#ffbdb9" ];
  "MoonscapeGame.IERC20" [ label = "IERC20" ];
  "MoonscapeGame.CityNft" [ label = "CityNft" ];
}

subgraph "clusterIERC20" {
  graph [ label = "IERC20", color = "#e8726d", fontcolor = "#f0f0f0", style = "rounded,dashed", bgcolor = "#3b4b63" ];
  "IERC20.balanceOf" [ label = "balanceOf" ];
  "IERC20.transferFrom" [ label = "transferFrom" ];
  "IERC20.transfer" [ label = "transfer" ];
}

subgraph "cluster_amount" {
  graph [ label = "_amount", color = "#e8726d", fontcolor = "#f0f0f0", style = "rounded,dashed", bgcolor = "#3b4b63" ];
  "_amount.add" [ label = "add" ];
}

subgraph "clusterCityNft" {
  graph [ label = "CityNft", color = "#e8726d", fontcolor = "#f0f0f0", style = "rounded,dashed", bgcolor = "#3b4b63" ];
  "CityNft.ownerOf" [ label = "ownerOf" ];
  "CityNft.safeTransferFrom" [ label = "safeTransferFrom" ];
  "CityNft.mint" [ label = "mint" ];
}

  "MoonscapeGame.purchaseMoondust";
  "MoonscapeGame.IERC20";
  "IERC20.balanceOf";
  "IERC20.transferFrom";
  "_amount.add";
  "MoonscapeGame.stakeForMoondust";
  "MoonscapeGame.unstakeForMoondust";
  "IERC20.transfer";
  "MoonscapeGame.importCity";
  "MoonscapeGame.CityNft";
  "CityNft.ownerOf";
  "CityNft.safeTransferFrom";
  "MoonscapeGame.exportCity";
  "MoonscapeGame.mintCity";
  "CityNft.mint";
  "MoonscapeGame.burnScapeForBuilding";
  "MoonscapeGame.burnScapeForConnection";
  "MoonscapeGame.importRover";
  "MoonscapeGame.exportRover";
  "MoonscapeGame.mintRover";
  "MoonscapeGame.purchaseMoondust" -> "MoonscapeGame.IERC20" [ color = "#1bc6a6" ];
  "MoonscapeGame.purchaseMoondust" -> "IERC20.balanceOf" [ color = "white" ];
  "MoonscapeGame.purchaseMoondust" -> "IERC20.transferFrom" [ color = "white" ];
  "MoonscapeGame.purchaseMoondust" -> "_amount.add" [ color = "white" ];
  "MoonscapeGame.stakeForMoondust" -> "MoonscapeGame.IERC20" [ color = "#1bc6a6" ];
  "MoonscapeGame.stakeForMoondust" -> "IERC20.balanceOf" [ color = "white" ];
  "MoonscapeGame.stakeForMoondust" -> "IERC20.transferFrom" [ color = "white" ];
  "MoonscapeGame.stakeForMoondust" -> "_amount.add" [ color = "white" ];
  "MoonscapeGame.unstakeForMoondust" -> "MoonscapeGame.IERC20" [ color = "#1bc6a6" ];
  "MoonscapeGame.unstakeForMoondust" -> "IERC20.balanceOf" [ color = "white" ];
  "MoonscapeGame.unstakeForMoondust" -> "IERC20.transfer" [ color = "white" ];
  "MoonscapeGame.importCity" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.importCity" -> "CityNft.ownerOf" [ color = "white" ];
  "MoonscapeGame.importCity" -> "CityNft.safeTransferFrom" [ color = "white" ];
  "MoonscapeGame.exportCity" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.exportCity" -> "CityNft.safeTransferFrom" [ color = "white" ];
  "MoonscapeGame.mintCity" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.mintCity" -> "CityNft.mint" [ color = "white" ];
  "MoonscapeGame.burnScapeForBuilding" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.burnScapeForBuilding" -> "CityNft.ownerOf" [ color = "white" ];
  "MoonscapeGame.burnScapeForBuilding" -> "CityNft.safeTransferFrom" [ color = "white" ];
  "MoonscapeGame.burnScapeForConnection" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.burnScapeForConnection" -> "CityNft.safeTransferFrom" [ color = "white" ];
  "MoonscapeGame.importRover" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.importRover" -> "CityNft.ownerOf" [ color = "white" ];
  "MoonscapeGame.importRover" -> "CityNft.safeTransferFrom" [ color = "white" ];
  "MoonscapeGame.exportRover" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.exportRover" -> "CityNft.safeTransferFrom" [ color = "white" ];
  "MoonscapeGame.mintRover" -> "MoonscapeGame.CityNft" [ color = "#1bc6a6" ];
  "MoonscapeGame.mintRover" -> "CityNft.mint" [ color = "white" ];


rankdir=LR
node [shape=plaintext]
subgraph cluster_01 { 
label = "Legend";
key [label=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
  <tr><td align="right" port="i1">Internal Call</td></tr>
  <tr><td align="right" port="i2">External Call</td></tr>
  <tr><td align="right" port="i3">Defined Contract</td></tr>
  <tr><td align="right" port="i4">Undefined Contract</td></tr>
  </table>>]
key2 [label=<<table border="0" cellpadding="2" cellspacing="0" cellborder="0">
  <tr><td port="i1">&nbsp;&nbsp;&nbsp;</td></tr>
  <tr><td port="i2">&nbsp;&nbsp;&nbsp;</td></tr>
  <tr><td port="i3" bgcolor="#445773">&nbsp;&nbsp;&nbsp;</td></tr>
  <tr><td port="i4">
    <table border="1" cellborder="0" cellspacing="0" cellpadding="7" color="#e8726d">
      <tr>
       <td></td>
      </tr>
     </table>
  </td></tr>
  </table>>]
key:i1:e -> key2:i1:w [color="#1bc6a6"]
key:i2:e -> key2:i2:w [color="white"]
}
}

Inheritance

digraph G {
  graph [ ratio = "auto", page = "40" ];
  "CityNft";
  "ERC721";
  "ERC721Burnable";
  "Ownable";
  "MoonscapeGame";
  "IERC721Receiver";
  "IERC20";
  "SafeERC20";
  "SafeMath";
  "Address";
  "Context";
  "ERC165";
  "IERC721";
  "IERC721Metadata";
  "IERC721Enumerable";
  "IERC165";
  "EnumerableSet";
  "EnumerableMap";
  "Strings";
  "ERC20";
  "CityNft" -> "ERC721";
  "CityNft" -> "ERC721Burnable";
  "CityNft" -> "Ownable";
  "MoonscapeGame" -> "Ownable";
  "MoonscapeGame" -> "IERC721Receiver";
  "Ownable" -> "Context";
  "ERC721" -> "Context";
  "ERC721" -> "ERC165";
  "ERC721" -> "IERC721";
  "ERC721" -> "IERC721Metadata";
  "ERC721" -> "IERC721Enumerable";
  "IERC721" -> "IERC165";
  "IERC721Metadata" -> "IERC721";
  "IERC721Enumerable" -> "IERC721";
  "ERC165" -> "IERC165";
  "ERC721Burnable" -> "Context";
  "ERC721Burnable" -> "ERC721";
  "ERC20" -> "Context";
  "ERC20" -> "IERC20";
}

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.