Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
@cruna/ds-protocol
Advanced tools
A protocol to subordinate ERC721 contracts to a dominant contract so that the subordinate follows the ownership of the dominant, which can be even be an ERC721 contract that does not have any additional features.
In 2021, when we started Everdragons2, we had in mind of using the head of the dragons for a PFP token based on the Everdragons2 that you own. Here an example of a full dragon and just the head.
The question was, Should we allow people to transfer the PFP separately from the primary NFT? It didn't make much sense. At the same time, how to avoid that?
ERC721Subordinate introduces a subordinate token that are owned by whoever owns the dominant token. In consequence of this, the subordinate token cannot be approved or transferred separately from the dominant token. It is transferred when the dominant token is transferred.
// A subordinate contract has no control on its own ownership.
// Whoever owns the main token owns the subordinate token.
// ERC165 interface id is 0x431694c0
interface IERC721Subordinate {
// The function dominantToken() returns the address of the dominant token.
function dominantToken() external view returns (address);
// Most marketplaces do not see tokens that have not emitted an initial
// transfer from address 0. This function allow to fix the issue, but
// it is not mandatory — in same cases, the deployer may want the subordinate
// being not visible on marketplaces.
function emitTransfer(
address from,
address to,
uint256 tokenId
) external;
}
To avoid loops, it is paramount that the subordinate token sets the dominant token during the deployment and is not be able to change it.
Here is the implementation of the interface in this repository (at https://github.com/ndujaLabs/erc721subordinate/blob/main/contracts/ERC721Subordinate.sol).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
// Authors: Francesco Sullo <francesco@sullo.co>
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./interfaces/IERC721Subordinate.sol";
import "./ERC721Badge.sol";
/**
* @dev Implementation of IERC721Subordinate interface.
* Strictly based on OpenZeppelin's implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721]
* in openzeppelin/contracts v4.8.0.
*/
contract ERC721Subordinate is IERC721Subordinate, ERC721Badge {
using Address for address;
using Strings for uint256;
error NotAnNFT();
error TransferAlreadyEmitted();
error OnlyDominant();
// dominant token contract
IERC721 private immutable _dominant;
mapping(uint256 => bool) private _initialTransfers;
modifier onlyDominant() {
if (msg.sender != address(_dominant)) revert OnlyDominant();
_;
}
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection
* plus the contract of the dominant token.
*/
constructor(
string memory name_,
string memory symbol_,
address dominant_
) ERC721Badge(name_, symbol_) {
_dominant = IERC721(dominant_);
if (!_dominant.supportsInterface(type(IERC721).interfaceId)) revert NotAnNFT();
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Badge) returns (bool) {
return
interfaceId == type(IERC721Subordinate).interfaceId ||
interfaceId == type(IERC721).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @dev See {IERC721Subordinate}.
*/
function dominantToken() public view override returns (address) {
return address(_dominant);
}
/**
* @dev See {IERC721-balanceOf}.
*/
function balanceOf(address owner) public view virtual override returns (uint256) {
return _dominant.balanceOf(owner);
}
/**
* @dev See {IERC721-ownerOf}.
*/
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
return _dominant.ownerOf(tokenId);
}
function _allowTransfer(address) internal view virtual returns (bool) {
// return _msgSender() == tokenOwner;
return true;
}
function emitInitialTransfer(uint256 tokenId) external virtual {
if (!_initialTransfers[tokenId]) revert TransferAlreadyEmitted();
// if the token does not exist it will revert("ERC721: invalid token ID")
address tokenOwner = _dominant.ownerOf(tokenId);
_allowTransfer(tokenOwner);
emit Transfer(address(0), tokenOwner, tokenId);
_initialTransfers[tokenId] = true;
}
function emitTransfer(
address from,
address to,
uint256 tokenId
) external virtual override onlyDominant {
if (!_initialTransfers[tokenId]) {
from = address(0);
_initialTransfers[tokenId] = true;
}
emit Transfer(from, to, tokenId);
}
}
The repo includes also an upgradeable version.
// SPDX-License-Identifier: GPL3
pragma solidity ^0.8.17;
// Author: Francesco Sullo <francesco@sullo.co>
// erc165 interfaceId 0xbfdf8f79
interface IERC721DefaultApprovable {
// Must be emitted when the contract is deployed.
event DefaultApprovable(bool approvable);
// Must be emitted any time the status changes.
event Approvable(uint256 indexed tokenId, bool approvable);
// Returns true if the token is approvable.
// It should revert if the token does not exist.
function approvable(uint256 tokenId) external view returns (bool);
// A contract implementing this interface should not allow
// the approval for all. So, any actor validating this interface
// should assume that the tokens are not approvable for all.
// An extension of this interface may include info about the
// approval for all, but it should be considered as a separate
// feature, not as a replacement of this interface.
}
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
// erc165 interfaceId 0xb45a3c0e
interface IERC721DefaultLocked {
// Must be emitted one time, when the contract is deployed,
// defining the default status of any token that will be minted
event DefaultLocked(bool locked);
// Must be emitted any time the status changes
event Locked(uint256 indexed tokenId, bool locked);
// Returns the status of the token.
// It should revert if the token does not exist.
function locked(uint256 tokenId) external view returns (bool);
}
It implements the IERC721DefaultApprovable and the IERC721DefaultLocked interfaces.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC721DefaultApprovable.sol";
import "./IERC721DefaultLocked.sol";
contract ERC721Badge is IERC721DefaultLocked, IERC721DefaultApprovable, ERC721{
constructor(string memory name, string memory symbol) ERC721(name, symbol) {
emit DefaultApprovable(false);
emit DefaultLocked(true);
}
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IERC721DefaultApprovable).interfaceId ||
interfaceId == type(IERC721DefaultLocked).interfaceId ||
super.supportsInterface(interfaceId);
}
function approvable(uint256) external view returns (bool) {
return false;
}
function locked(uint256) external view returns (bool) {
return true;
}
function approve(address, uint256) public virtual override {
revert("approvals not allowed");
}
function getApproved(uint256) public view virtual override returns (address) {
return address(0);
}
function setApprovalForAll(address, bool) public virtual override {
revert("approvals not allowed");
}
function isApprovedForAll(address, address) public view virtual override returns (bool) {
return false;
}
function transferFrom(
address,
address,
uint256
) public virtual override {
revert("transfers not allowed");
}
function safeTransferFrom(
address,
address,
uint256,
bytes memory
) public virtual override {
revert("transfers not allowed");
}
}
Install the dependencies like
npm i @openzeppelin/contracts \
@openzeppelin/contracts-upgradeable \
@cruna/ds-protocol
You initialize the subordinate token passing the address of the main token and the subordinate takes anything from that. Look at some example in mocks and the testing.
What makes the difference is the base token uri. Change that, and everything will work great.
A simple example:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "@ndujalabs/erc721subordinate/contracts/ERC721Subordinate.sol";
contract MySubordinate is ERC721Subordinate {
constructor(address myToken) ERC721Subordinate("MyToken", "MTK", myToken) {}
}
Another example, upgradeable
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "../ERC721SubordinateUpgradeable.sol";
contract MySubordinateUpgradeable is ERC721SubordinateUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}
function initialize(address myTokenEnumerableUpgradeable) public initializer {
__ERC721EnumerableSubordinate_init("SuperToken", "SPT", myTokenEnumerableUpgradeable);
}
function _authorizeUpgrade(address newImplementation) internal virtual override {}
function getInterfaceId() public pure returns (bytes4) {
return type(IERC721SubordinateUpgradeable).interfaceId;
}
}
Notice that there is no reason to make the subordinate enumerable because we can query the dominant token to get all the ID owned by someone and apply that to the subordinate.
There are similar proposal that moves in the same realm.
EIP-6150: Hierarchical NFTs (discussion at https://ethereum-magicians.org/t/eip-6150-hierarchical-nfts-an-extension-to-erc-721/12173) is a proposal for a new standard for non-fungible tokens (NFTs) on the Ethereum blockchain that would allow NFTs to have a hierarchical structure, similar to a filesystem. This would allow for the creation of complex structures, such as NFTs that contain other NFTs, or NFTs that represent collections of other NFTs. The proposal is currently in the discussion phase, and has not yet been implemented on the Ethereum network. ERC721Subordinate focuses instead on a simpler scenario, trying to solve a specific problem in the simplest possible way.
EIP-3652 (https://ethereum-magicians.org/t/eip-3652-hierarchical-nft/6963) is very similar to EIP-6150. Both requires all the node following the standard. ERC721Subordinate is very different because it allows to create subordinates of existing, immutable NFTs, if it is not necessary to show the subordinate on marketplaces.
Feel free to make a PR to add your contracts.
1.1.4
virtual
keywords in some functions1.1.3
__ReentrancyGuard_init()
in __ERC721Dominant_init
1.1.2
1.1.1
1.1.0
_canAddSubordinate
that must be implemented by the contract that extends the dominant, like function _canAddSubordinate() internal override onlyOwner {}
1.0.0
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
emitTransfer
function. Any upgradeable contract implementing the previous version won't by ugradeable. While this is not ideal, it is better for future usages, considering we still are in the proposal stage.0.4.1
0.4.0
0.3.0
0.2.0
0.1.4 — version not published
0.1.3
0.1.2
0.1.1
0.1.0
0.0.4
__ERC721SubordinateUpgradeable_init
>> __ERC721Subordinate_init
0.0.3
0.0.2
0.0.1
(c) 2022, Francesco Sullo francesco@sullo.co
MIT
FAQs
A dominant/subordinate NFTs protocol
We found that @cruna/ds-protocol demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.