Create a blockchain asset tracking system using an ERC-721-inspired smart contract!
Rafael Abuawad
Posted on January 12, 2023
Introduction
Most asset tracking done today is done using something as basic and as insecure as a simple spreadsheet, in some cases a most sophisticated approach is done with some kind of database with custom software as an interface.
Both are extremely insecure and can be altered by an attacker. Let us imagine a company that distributes high-end computer boards, not only sells them but leases them to different types of organizations for testing, marketing, and more. If an attacker wanted to modify this register using the current approach it would be as simple as accessing the database or spreadsheet and changing a simple record.
Using multi-ownable smart contracts would prevent this kind of behavior by default, and asset tracking with NFTs can give better control of where is each asset and with some extra code give a lot more information than what can be stored in a simple spreadsheet.
Solving this problem
This problem can be solved using blockchain technology:
- Create smart contracts that enable the company to track its assets transparently and reliably.
- Create a custom blockchain to solve this issue.
in this article, we are going to see the first approach, but the second one can be as, if not more interesting and reliable.
Smart-contracts
For this approach, we only need one smart contract for tracking the assets.
We are going to use a similar approach to the ERC-721 token standard, but with a lot of parts removed and a lot of modifications.
Since we don't want users to trade tokens like a normal NFT, we also what the ability to remove token access to a given access, and we want to control who can have what.
Interfaces
This interface defines the asset collection itself and is a stripped-down version of an ERC-721.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IAssetCollection {
event Transfer(
address indexed from,
address indexed to,
uint256 indexed tokenId
);
function name() external view returns (string memory);
function balanceOf(address owner) external view returns (uint256);
function tokensOf(address tokenHolder)
external
view
returns (uint256[] memory);
function ownerOf(uint256 tokenId) external view returns (address owner);
function tokenURI(uint256 tokenId) external view returns (string memory);
function mint(address tokenHolder, string memory tokenURI) external;
function burn(address tokenHolder, uint256 tokenId) external;
function transfer(uint256 tokenId) external;
function transferFrom(
address tokenHolder,
address to,
uint256 tokenId
) external;
}
This interface describes is to allow control over the smart contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IOwnable {
event TransferOwnership(address indexed from, address indexed to);
function owner() external view returns (address);
function transferOwnership(address _newOwner) external;
}
Smart-contract
The main smart contract is the Asset Collection smart contract, this contract is responsible for managing all the assets inside a particular collection, we (as the owners of the smart contract) can mint, burn, move and transfer tokens, each token represents an asset.
We don't want users to trade tokens (like NFTs), but we need the basic functionality from an ERC-721.
Due to limitations on smart contract data storage, we are storing all the relevant information outside the blockchain, preferably on a persistent storage platform (like Machina or Arweave), and using the token URI metadata pattern used commonly for NFTs.
Based on our work on the interface defined above, we will overview some functions.
Mint
The "mint" function can only be called ourselves (the owner) and is similar to the mint function found on any other NFT smart contracts. It gives us the ability to create a token.
function mint(address tokenHolder, string memory _tokenURI)
public
onlyOwner
{
uint256 tokenId = _tokenIds;
_tokenIds += 1;
_setTokenURI(tokenId, _tokenURI);
_mint(tokenHolder, tokenId);
}
Burn
The "burn" method allows us to destroy a token permanently.
function burn(address tokenHolder, uint256 tokenId)
public
onlyOwner
{
_mint(tokenHolder, tokenId);
}
Transfer
The "transfer" method returns a token back to us, and can only be called by the token owner itself.
function transfer(uint256 tokenId) public {
_transfer(msg.sender, _owner, tokenId);
}
Transfer from
The "transfer from" method allows us (the owner) to control tokens on behalf of a user. If we want to change the holder or if we need to take it back and assign it to ourselves again.
// Moves a token from one user to another
function transferFrom(
address tokenHolder,
address to,
uint256 tokenId
) public onlyOwner {
_transfer(tokenHolder, to, tokenId);
}
The final smart contract should look something like this.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./interfaces/IAssetCollection.sol";
import "./interfaces/IOwnable.sol";
contract AssetCollection is IAssetCollection, IOwnable {
// Token ID counter
uint256 private _tokenIds;
// Owner of the smart-contract
address private _owner;
// Name of the asset collection
string private _name;
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping owner address to token count
mapping(address => uint256) private _balances;
// Mapping from token ID to token URI
mapping(uint256 => string) private _tokenURIs;
constructor(string memory __name) {
_name = __name;
_owner = msg.sender;
}
modifier onlyOwner() {
require(_owner == msg.sender, "Ownable: caller is not the owner");
_;
}
function _exists(uint256 tokenId) internal view returns (bool) {
return _owners[tokenId] != address(0);
}
function _setTokenURI(uint tokenId, string memory _tokenURI) internal {
_tokenURIs[tokenId] = _tokenURI;
}
function _mint(address to, uint256 tokenId) internal {
require(to != address(0), "ERC721: mint to the zero address");
// Check that tokenId was not minted
require(!_exists(tokenId), "ERC721: token already minted");
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
}
function _transfer(
address from,
address to,
uint256 tokenId
) internal {
require(
ownerOf(tokenId) == from,
"ERC721: transfer from incorrect owner"
);
require(to != address(0), "ERC721: transfer to the zero address");
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
function _burn(uint256 tokenId) internal {
address tokenOwner = ownerOf(tokenId);
_balances[tokenOwner] -= 1;
emit Transfer(tokenOwner, address(0), tokenId);
}
// Creates token
function mint(address tokenHolder, string memory _tokenURI)
public
onlyOwner
{
uint256 tokenId = _tokenIds;
_tokenIds += 1;
_setTokenURI(tokenId, _tokenURI);
_mint(tokenHolder, tokenId);
}
// Destroys token
function burn(address tokenHolder, uint256 tokenId) public onlyOwner {
_mint(tokenHolder, tokenId);
}
// Transfer the token back to the DAO
function transfer(uint256 tokenId) public {
_transfer(msg.sender, _owner, tokenId);
}
// Moves a token from one user to another
function transferFrom(
address tokenHolder,
address to,
uint256 tokenId
) public onlyOwner {
_transfer(tokenHolder, to, tokenId);
}
// Returns how many tokens a user owns
function balanceOf(address tokenOwner) public view returns (uint256) {
require(
tokenOwner != address(0),
"ERC721: address zero is not a valid owner"
);
return _balances[tokenOwner];
}
// Returns how many tokens a user owns
function tokensOf(address tokenOwner)
public
view
returns (uint256[] memory)
{
require(
tokenOwner != address(0),
"ERC721: address zero is not a valid owner"
);
uint256 balance = balanceOf(tokenOwner);
uint256[] memory tokens = new uint[](balance);
uint256 index = 0;
for (uint id = 0; id < _tokenIds; id++) {
if (_owners[id] == tokenOwner) {
tokens[index] = id;
index += 1;
}
}
return tokens;
}
// Returns the token URI of the token
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(_exists(tokenId), "ERC721: token hasn't been minted");
return _tokenURIs[_tokenIds];
}
// Returns the owner of the token
function ownerOf(uint256 tokenId) public view returns (address) {
address tokenOwner = _owners[tokenId];
require(tokenOwner != address(0), "ERC721: invalid token ID");
return tokenOwner;
}
// Transfers the ownership of the smart-contract
function transferOwnership(address _newOwner) public {
require(msg.sender == _owner, "Ownable: Caller is not owner");
require(
_newOwner == address(0),
"Ownable: New owner can not be the zero address"
);
address olOwner = _owner;
_owner = _newOwner;
emit TransferOwnership(olOwner, _newOwner);
}
// Returns name of the collection
function name() public view returns (string memory) {
return _name;
}
// Returns owner of the smart-contract
function owner() public view returns (address) {
return _owner;
}
}
This token allows us to track an asset securely and transparently.
This is only contract is just for demonstration purposes and is not optimized nor audited.
Asset Collection smart contract deployed on the Avalanche Testnet
Posted on January 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 30, 2024