Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create BEP-129 for GameFi Non-fungible Token #129

Closed
wants to merge 1 commit into from

Conversation

connectorGamefi
Copy link

@connectorGamefi connectorGamefi commented Jan 25, 2022

BEP-129: Non-fungible Token for GameFi

1. Summary

This BEP describes a proposal for GameFi specific NFT on the Binance Smart Chain.

2. Abstract

Games using non-fungible tokens to stand for their internal objects including roles, assets etc. and there will be some dynamic data asscociated with them, e.g.

  • roles in games can change their clothes color
  • NPCs in games can change their skills
  • properties in games will have different attributes while different roles obtain

The existing ERC721 standard does not compatible with these kind of use-cases, and the new protocol introduce a simple data structure to store attributes.

3. Status

This BEP is already implemented.

4. Motivation

The existing NFT protocol stores Metadata externally and links through URI. This link relationship is fragile, and developers can even modify the URI. In addition, the storage location of Metadata is also uncertain and may be stored on a centralized server. Through this protocol:

  • Metadata will no longer be stored off-chain.
  • Only verified NFT owners can modify Metadata of their NFT.

You can find the specific implementation case here.

5. Specification

5.1 Attribute standard

BEP-129 is an extension of EIP-721. Following attribute-related functions and events are suggested to be added:

pragma solidity ^0.8.0;

interface IBEP129 {
    /**
     * @dev Emitted when an attribute is created.
     */
    event CreateAttribute(uint128 attrID, uint8 decimal);

    /**
     * @dev Emitted when a batch of attributes are created.
     */
    event CreateAttributeBatch(uint128[] attrIDs, uint8[] decimals);

    /**
     * @dev Emitted when an attribute is attached to an NFT.
     */
    event AttributeAttached(uint256 tokenID, uint128 attrID, uint128 value);

    /**
     * @dev Emitted when a batch of attributes are attached to an NFT.
     */
    event AttributeAttachedBatch(uint256 tokenID, uint128[] attrIDs, uint128[] values);
    
    /**
     * @dev Emitted when an attribute value of an NFT is updated.
     *
     * @param attrIndex The index of attribute in attribute array of the NFT.
     */
    event AttributeUpdated(uint256 tokenID, uint256 attrIndex, uint128 value);
    
    /**
     * @dev Emitted when a batch of attribute values of an NFT are updated.
     *
     * @param attrIndexes The indexes of attributes in attribute array of the NFT.
     */
    event AttributeUpdatedBatch(uint256 tokenID, uint256[] attrIndexes, uint128[] values);
    
    /**
     * @dev Emitted when an attribute of an NFT has been removed.
     */
    event AttributeRemoved(uint256 tokenID, uint128 attrID);
    
    /**
     * @dev Emitted when a batch of attributes are removed.
     */
    event AttributeRemoveBatch(uint256 tokenID, uint128[] attrIDs);

    /**
     * @dev Returns the decimals places of the attribute.
     */
    function attributeDecimals(uint128 attrID) external view returns (uint8);

    /**
     * @dev Create new attribute.
     */
    function create(uint128 attrID, uint8 decimals) external;

    /**
     * @dev Create a batch of new attributes.
     */
    function createBatch(uint128[] memory attrIDs, uint8[] memory decimals) external;

    /**
     * @dev Attach the attribute to NFT.
     */
    function attach(uint256 tokenID, uint128 attrID, uint128 value) external;

    /**
     * @dev Attach a batch of attributes to NFT.
     */
    function attachBatch(uint256 tokenID, uint128[] memory attrIDs, uint128[] memory values) external;

    /**
     * @dev Update the attribute to NFT.
     */
    function update(uint256 tokenID, uint256 attrIndex, uint128 value) external;

    /**
     * @dev Update a batch of attributes to NFT.
     */
    function updateBatch(uint256 tokenID, uint256[] memory attrIndexes, uint128[] memory values) external;

    /**
     * @dev Remove the attribute from NFT.
     */
    function remove(uint256 tokenID, uint256 attrIndex) external;

    /**
     * @dev Remove a batch of attributes from NFT.
     */
    function removeBatch(uint256 tokenID, uint256[] memory attrIndexes) external;
}

5.2 Implementation

5.2.1 The flow of NFT in the protocol

  1. NFTs are initially minted on-chain.
  2. Deposit NFTs into the game: there is a treasure contract as NFT asset storage, transfer the NFT to the treasure contract to complete the deposit, then the corresponding assets will appear in the game.
  3. Withdraw NFTs from the game: NFTs are transferred from the treasure contract, while synchronizing the modified attributes, freezing corresponding assets inside the game.

5.2.2 BEP-129 implementation

The attribute operation permission of NFT is controlled by the treasure contract.

pragma solidity ^0.8.0;

contract BEP129 is ERC721, IBEP129 {
    struct AttributeBaseData {
        uint8 decimal;
        bool exist;
    }

    struct AttributeData {
        uint128 attrID;
        uint128 attrValue;
    }

    address public treasure;

    // attrID => decimal
    mapping(uint128 => AttributeBaseData) internal _attrBaseData;
    // tokenID => attribute data
    mapping(uint256 => AttributeData[]) internal _attrData;

    uint256 internal _cap;

    event CreateAttribute(uint128 attrID, uint8 decimal);
    event CreateAttributeBatch(uint128[] attrIDs, uint8[] decimals);
    event AttributeAttached(uint256 tokenID, uint128 attrID, uint128 value);
    event AttributeAttachedBatch(uint256 tokenID, uint128[] attrIDs, uint128[] values);
    event AttributeUpdated(uint256 tokenID, uint256 attrIndex, uint128 value);
    event AttributeUpdatedBatch(uint256 tokenID, uint256[] attrIndexes, uint128[] values);
    event AttributeRemoved(uint256 tokenID, uint128 attrID);
    event AttributeRemoveBatch(uint256 tokenID, uint128[] attrIDs);

    constructor(
        address treasure_,
        string memory name_, 
        string memory symbol_, 
        uint256 cap_
    ) ERC721(name_, symbol_) {
        treasure = treasure_;
        _cap = cap_;
    }

    function attributeDecimals(uint128 _attrID) public override virtual view returns (uint8) {
        return _attrBaseData[_attrID].decimal;
    }

    function attributes(uint256 _tokenID) public virtual view returns (AttributeData[] memory) {
        return _attrData[_tokenID];
    }

    function create(uint128 _id, uint8 _decimal) public override virtual {
        _create(_id, _decimal);
    }

    function createBatch(uint128[] memory _ids, uint8[] memory _decimals) public override virtual {
        _createBatch(_ids, _decimals);
    }

    function _create(uint128 _attrID, uint8 _decimal) internal virtual {
        _attrBaseData[_attrID] = AttributeBaseData(_decimal, true);
        emit CreateAttribute(_attrID, _decimal);
    }

    function _createBatch(uint128[] memory _attrIDs, uint8[] memory _decimals) internal virtual {
        require(_attrIDs.length == _decimals.length, "GameLoot: param length error");
        for (uint256 i; i < _attrIDs.length; i++) {
            _attrBaseData[_attrIDs[i]] = AttributeBaseData(_decimals[i], true);
        }
        emit CreateAttributeBatch(_attrIDs, _decimals);
    }

    function attach(uint256 tokenID, uint128 attrID, uint128 value) public virtual onlyTreasure {
        require(_attrBaseData[attrID].exist, "GameLoot: attribute is not existed");
        require(_attrData[tokenID].length + 1 <= _cap, "GameLoot: too many attributes");
        _attrData[tokenID].push(AttributeData(attrID, value));
        emit AttributeAttached(tokenID, attrID, value);
    }

    function attachBatch(uint256 tokenID, uint128[] memory attrIDs, uint128[] memory values) public virtual onlyTreasure {
        require(_attrData[tokenID].length + attrIDs.length <= _cap, "GameLoot: too many attributes");
        for (uint256 i; i < attrIDs.length; i++) {
            require(_attrBaseData[attrIDs[i]].exist, "GameLoot: attribute is not existed");
            _attrData[tokenID].push(AttributeData(attrIDs[i], values[i]));
        }
        emit AttributeAttachedBatch(tokenID, attrIDs, values);
    }

    function update(uint256 tokenID, uint256 attrIndex, uint128 value) public virtual onlyTreasure {
        _attrData[tokenID][attrIndex].attrValue = value;
        emit AttributeUpdated(tokenID, attrIndex, value);
    }

    function updateBatch(uint256 tokenID, uint256[] memory attrIndexes, uint128[] memory values) public virtual onlyTreasure {
        for (uint256 i; i < attrIndexes.length; i++) {
            _attrData[tokenID][attrIndexes[i]].attrValue = values[i];
        }
        emit AttributeUpdatedBatch(tokenID, attrIndexes, values);
    }

    function remove(uint256 tokenID, uint256 attrIndex) public virtual onlyTreasure {
        uint128 id = _attrData[tokenID][attrIndex].attrID;
        _attrData[tokenID][attrIndex] = _attrData[tokenID][_attrData[tokenID].length - 1];
        _attrData[tokenID].pop();
        emit AttributeRemoved(tokenID, id);
    }

    function removeBatch(uint256 tokenID, uint256[] memory attrIndexes) public virtual onlyTreasure{
        uint128[] memory ids = new uint128[](attrIndexes.length);
        for (uint256 i; i < attrIndexes.length; i++) {
            ids[i] = _attrData[tokenID][attrIndexes[i]].attrID;
            _attrData[tokenID][attrIndexes[i]] = _attrData[tokenID][_attrData[tokenID].length - 1];
            _attrData[tokenID].pop();
        }
        emit AttributeRemoveBatch(tokenID, ids);
    }

    function getCap() public view returns (uint256){
        return _cap;
    }

    modifier onlyTreasure(){
        require(msg.sender == treasure, "is not treasure");
        _;
    }
}

5.2.3 Treasure contract implementation

There are two permissions that need to be controlled:

  1. How to ensure that NFTs are not transferred from the treasure contract by an attacker.
  2. How to ensure that fake attribute values ​​are not uploaded on the chain.

Solutions:

  1. Only stakers can withdraw the corresponding NFT from treasure contract.
  2. By signing the attribute data, verify whether it is a valid signature, and then allow the attribute data to be synchronized to the chain.

Refer to this treasure contract example:

pragma solidity ^0.8.0;

contract Treasure is Ownable, Pausable, IERC721Receiver {
    mapping(address => bool) public signers;
    mapping(uint256 => bool) public usedNonce;
    mapping(uint256 => address) public lastOwner;

    cconstructor(address[] memory _signers){
        for (uint256 i; i < _signers.length; i++)
            signers[_signers[i]] = true;
    }

    event UpChain(address token, uint256 tokenID, uint256 nonce);
    event TopUp(address token, uint256 tokenID, uint256 nonce);

    receive() external payable {}

    /// @notice In-game asset set on chain
    /// @dev Need to sign
    function upChain(
        address _token,
        uint256 _tokenID,
        uint256 _nonce,
        uint128[] memory _attrIDs,
        uint128[] memory _attrValues,
        uint256[] memory _attrIndexesUpdate,
        uint128[] memory _attrValuesUpdate,
        uint256[] memory _attrIndexesRM,
        bytes memory _signature
    ) public whenNotPaused nonceNotUsed(_nonce) {
        require(msg.sender == lastOwner[_tokenID], "only person who topped up it");
        require(verify(msg.sender, address(this), _token, _tokenID, _nonce, _attrIDs, _attrValues, _attrIndexesUpdate, _attrValuesUpdate, _attrIndexesRM, _signature), "sign is not correct");
        usedNonce[_nonce] = true;

        if (_attrIDs.length != 0)
            IGameLoot(_token).attachBatch(_tokenID, _attrIDs, _attrValues);

        if (_attrIndexesUpdate.length != 0)
            IGameLoot(_token).updateBatch(_tokenID, _attrIndexesUpdate, _attrValuesUpdate);

        if (_attrIndexesRM.length != 0)
            IGameLoot(_token).removeBatch(_tokenID, _attrIndexesRM);

        lastOwner[_tokenID] = address(0);
        IERC721(_token).transferFrom(address(this), msg.sender, _tokenID);
        emit UpChain(_token, _tokenID, _nonce);
    }

    /// @notice Top up
    /// @dev Need to sign
    function topUp(
        address _token,
        uint256 _tokenID,
        uint256 _nonce,
        bytes memory _signature
    ) public whenNotPaused nonceNotUsed(_nonce) {
        require(verify(msg.sender, address(this), _token, _tokenID, _nonce, _signature), "sign is not correct");
        usedNonce[_nonce] = true;

        lastOwner[_tokenID] = msg.sender;
        IERC721(_token).transferFrom(msg.sender, address(this), _tokenID);
        emit TopUp(_token, _tokenID, _nonce);
    }

    function verify(
        address _wallet,
        address _this,
        address _token,
        uint256 _tokenID,
        uint256 _nonce,
        uint128[] memory _attrIDs,
        uint128[] memory _attrValues,
        uint256[] memory _attrIndexesUpdate,
        uint128[] memory _attrValuesUpdate,
        uint256[] memory _attrIndexesRMs,
        bytes memory _signature
    ) internal view returns (bool){
        return signers[signatureWallet(_wallet, _this, _token, _tokenID, _nonce, _attrIDs, _attrValues, _attrIndexesUpdate, _attrValuesUpdate, _attrIndexesRMs, _signature)];
    }

    function signatureWallet(
        address _wallet,
        address _this,
        address _token,
        uint256 _tokenID,
        uint256 _nonce,
        uint128[] memory _attrIDs,
        uint128[] memory _attrValues,
        uint256[] memory _attrIndexesUpdate,
        uint128[] memory _attrValuesUpdate,
        uint256[] memory _attrIndexesRMs,
        bytes memory _signature
    ) internal pure returns (address){
        bytes32 hash = keccak256(
            abi.encode(_wallet, _this, _token, _tokenID, _nonce, _attrIDs, _attrValues, _attrIndexesUpdate, _attrValuesUpdate, _attrIndexesRMs)
        );
        return ECDSA.recover(ECDSA.toEthSignedMessageHash(hash), _signature);
    }

    function verify(
        address _wallet,
        address _this,
        address _token,
        uint256 _tokenID,
        uint256 _nonce,
        bytes memory _signature
    ) internal view returns (bool){
        return signers[signatureWallet(_wallet, _this, _token, _tokenID, _nonce, _signature)];
    }

    function signatureWallet(
        address _wallet,
        address _this,
        address _token,
        uint256 _tokenID,
        uint256 _nonce,
        bytes memory _signature
    ) internal pure returns (address){
        bytes32 hash = keccak256(
            abi.encode(_wallet, _this, _token, _tokenID, _nonce)
        );
        return ECDSA.recover(ECDSA.toEthSignedMessageHash(hash), _signature);
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unPause() public onlyOwner {
        _unpause();
    }

    function addSigner(address _signer) public onlyOwner {
        signers[_signer] = true;
    }

    function removeSigner(address _signer) public onlyOwner {
        signers[_signer] = false;
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) public override virtual returns (bytes4) {
        return this.onERC721Received.selector;
    }

    modifier nonceNotUsed(uint256 _nonce){
        require(!usedNonce[_nonce], "nonce already used");
        _;
    }
}

6. License

The content is licensed under CC0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants