Intro
Nowadays it is quite popular to build a decentralized application (dApp). If you are familiar with the blockchain smart contracts development and you’ve already developed something cool, probably there is no need for me to tell you that in this field the demand for good developers is quite high.
But for you to be a good smart contracts developer, you have to research, learn the best coding practices, educate yourself and practice coding constantly.
In this tutorial, I’m going to show you step by step my implementation on how to create a decentralized application which functions as a simplified DNS (domain name system). It was created a while back, but recently got a lot of attention, so I decided to make a short tutorial explaining the steps in the making.
There are quite a lot of blockchain platforms that give us the opportunity to develop smart contracts and decentralized applications on top of their networks and infrastructure. The main platforms used for dApp development are Aeternity, Ethereum, NEO, EOS, QTUM, Cardano, Stratis and for sure we will see a lot more in the nearest future. But for the purpose of this tutorial, we’ll be using Solidity as our development language and Ethereum’s infrastructure, as Ethereum is the most widely used for dApps development for now.
If you do not know what a domain name system is, and how it works, now is the time for you to go to Wikipedia and check DNS out to get a basic understanding of it.
At first building a decentralized DNS on top of Ethereum might seem like a trivial task, but we will make it a bit more complex … right now
Let us set some requirements for our project — simplified decentralized domain name system (DDNS).
Requirements
- We should be able to register a domain by providing the domain name and an IP address it should point to. There are also some other conditions:
- A registered domain cannot be bought and is owned by the caller of the method.
- The domain registration should cost 1 ETH and the domain should be registered for 1 year.
- After 1 year, anyone is allowed to buy the domain again.
- The domain registration can be extended by 1 year if the domain owner calls the register method and pays 1 ETH.
- The domain can be any string with length more than 5 symbols.
2. We need a public method to edit a domain. In our simplified decentralized DNS system, the editing of a domain will be changing the IP address it points to. The operation should be free and only the owner of the domain should be able to edit the domain.
3. Public method to transfer the domain ownership to another user. Again this operation is free and only the domain owner can transfer his domains ownership to somebody else.
4. Public method to get the IP based on a given domain.
5. A Public method that returns a list of all receipts by a certain account. A receipt is a domain purchase/extension and contains the price, timestamp of purchase and expiration date of the domain.
And since this is a domain name system, we want to make it as similar to the current ones as possible, so we are going to add some more additional features like:
- Dynamic pricing — the base price can increase if a short domain name is bought.
- Public method to withdraw the funds from the contract. This should be called only from the contract owner (the address which initially created the contract).
- Use contract events to signify that an activity has taken place in the contract. Events can be for domain registration/transfer (DDNS) etc.
And lastly, we need to have unit tests for everything we’ve done so far.
Implementation
Step 1: Architecture design
First, we need to think about our architecture and design it. In our case it is a very simple project so the architecture could be something like this:

Step 2: Architecture preparation
There are already smart contracts for the safe math operations, ownership logic and destruction logic provided from OpenZeppelin, that have passed multiple security audits, so we don’t need to reinvent the wheel.
For the math operations we are going to use SafeMath library, which looks like this:
pragma solidity >=0.4.22 <0.6.0; | |
library SafeMath { | |
function mul(uint256 a, uint256 b) internal pure returns (uint256) { | |
if (a == 0) { | |
return 0; | |
} | |
uint256 c = a * b; | |
assert(c / a == b); | |
return c; | |
} | |
function div(uint256 a, uint256 b) internal pure returns (uint256) { | |
//there is no case where this function can overflow/underflow | |
uint256 c = a / b; | |
return c; | |
} | |
function sub(uint256 a, uint256 b) internal pure returns (uint256) { | |
assert(b <= a); | |
return a - b; | |
} | |
function add(uint256 a, uint256 b) internal pure returns (uint256) { | |
uint256 c = a + b; | |
assert(c >= a); | |
return c; | |
} | |
} |
For the ownership and destruction logic we will again use security audited and proven to work contracts from the openzeppelin-solidity Github repository and then proceed with the implementation of our own DDNS.
pragma solidity >=0.4.22 <0.6.0; | |
/** | |
* @title Ownable | |
* @dev The Ownable contract has an owner address, and provides basic authorization control | |
* functions, this simplifies the implementation of "user permissions". | |
*/ | |
contract Ownable { | |
address private _owner; | |
event OwnershipRenounced(address indexed previousOwner); | |
event OwnershipTransferred( | |
address indexed previousOwner, | |
address indexed newOwner | |
); | |
/** | |
* @dev The Ownable constructor sets the original `owner` of the contract to the sender | |
* account. | |
*/ | |
constructor() public { | |
_owner = msg.sender; | |
} | |
/** | |
* @return the address of the owner. | |
*/ | |
function owner() public view returns(address) { | |
return _owner; | |
} | |
/** | |
* @dev Throws if called by any account other than the owner. | |
*/ | |
modifier onlyOwner() { | |
require(isOwner()); | |
_; | |
} | |
/** | |
* @return true if `msg.sender` is the owner of the contract. | |
*/ | |
function isOwner() public view returns(bool) { | |
return msg.sender == _owner; | |
} | |
/** | |
* @dev Allows the current owner to relinquish control of the contract. | |
* @notice Renouncing to ownership will leave the contract without an owner. | |
* It will not be possible to call the functions with the `onlyOwner` | |
* modifier anymore. | |
*/ | |
function renounceOwnership() public onlyOwner { | |
emit OwnershipRenounced(_owner); | |
_owner = address(0); | |
} | |
/** | |
* @dev Allows the current owner to transfer control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function transferOwnership(address newOwner) public onlyOwner { | |
_transferOwnership(newOwner); | |
} | |
/** | |
* @dev Transfers control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function _transferOwnership(address newOwner) internal { | |
require(newOwner != address(0)); | |
emit OwnershipTransferred(_owner, newOwner); | |
_owner = newOwner; | |
} | |
} |
pragma solidity >=0.4.22 <0.6.0; | |
import "./Ownable.sol"; | |
/** | |
* @title Destructible | |
* @dev Base contract that can be destroyed by owner. All funds in contract will be sent to the owner. | |
*/ | |
contract Destructible is Ownable { | |
/** | |
* @dev Transfers the current balance to the owner and terminates the contract. | |
*/ | |
function destroy() public onlyOwner { | |
selfdestruct(owner); | |
} | |
function destroyAndSend(address _recipient) public onlyOwner { | |
selfdestruct(_recipient); | |
} | |
} |
Lastly our main contract where we will put our logic for the decentralized domain name system (DDNS):
It inherits the Destructible.sol contract which itself inherits the ownership logic from the Ownable.sol contract and we are good to go.
pragma solidity ^0.5.0; | |
import "./common/Ownable.sol"; | |
import "./common/Destructible.sol"; | |
import "./libs/SafeMath.sol"; | |
contract DDNSService is Destructible { | |
/** USINGS */ | |
using SafeMath for uint256; | |
/** | |
* @dev - Constructor of the contract | |
*/ | |
constructor() public { | |
} | |
} |
The directory tree of our project could be looking like this at this point:

Step 3: Defining struct types, function modifiers, and state variables
Starting with the structures — structs are custom defined types that can group several variables. In our case, we will make some things easier by creating structures for them.
So, as we all know every problem has lots of different approaches to be solved, and I challenge you to try solving this one yourself and not trust mine as the “one and only solution”.
Structs
We are defining the DomainDetails structure which has the following properties:
- bytes name — the domain name stored as bytes
- bytes12 topLevel — the TLD of the domain
- address owner — address of the owner
- bytes15 ip — IP that is related to the domain name
- uint expires — expiring date of the domain.
pragma solidity >=0.4.22 <0.6.0; | |
/** | |
* @title Ownable | |
* @dev The Ownable contract has an owner address, and provides basic authorization control | |
* functions, this simplifies the implementation of "user permissions". | |
*/ | |
contract Ownable { | |
address private _owner; | |
event OwnershipRenounced(address indexed previousOwner); | |
event OwnershipTransferred( | |
address indexed previousOwner, | |
address indexed newOwner | |
); | |
/** | |
* @dev The Ownable constructor sets the original `owner` of the contract to the sender | |
* account. | |
*/ | |
constructor() public { | |
_owner = msg.sender; | |
} | |
/** | |
* @return the address of the owner. | |
*/ | |
function owner() public view returns(address) { | |
return _owner; | |
} | |
/** | |
* @dev Throws if called by any account other than the owner. | |
*/ | |
modifier onlyOwner() { | |
require(isOwner()); | |
_; | |
} | |
/** | |
* @return true if `msg.sender` is the owner of the contract. | |
*/ | |
function isOwner() public view returns(bool) { | |
return msg.sender == _owner; | |
} | |
/** | |
* @dev Allows the current owner to relinquish control of the contract. | |
* @notice Renouncing to ownership will leave the contract without an owner. | |
* It will not be possible to call the functions with the `onlyOwner` | |
* modifier anymore. | |
*/ | |
function renounceOwnership() public onlyOwner { | |
emit OwnershipRenounced(_owner); | |
_owner = address(0); | |
} | |
/** | |
* @dev Allows the current owner to transfer control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function transferOwnership(address newOwner) public onlyOwner { | |
_transferOwnership(newOwner); | |
} | |
/** | |
* @dev Transfers control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function _transferOwnership(address newOwner) internal { | |
require(newOwner != address(0)); | |
emit OwnershipTransferred(_owner, newOwner); | |
_owner = newOwner; | |
} | |
} |
Second we are defining the Receipt structure — which is something we should provide the user according to the project requirements we’ve set in the beginning. It has the following properties :
- uint amountPaidWei — the price that was paid in this transaction, stored as the amount of wei (the smallest part of ether)
- uint timestamp — the time when this receipt was issued
- uint expires — expiring time
struct Receipt { | |
uint amountPaidWei; | |
uint timestamp; | |
uint expires; | |
} |
Modifiers
Modifiers can be used to easily change the behavior of functions. For example, they can automatically check a condition prior to executing the function. Modifiers are inheritable properties of contracts and may be overridden by derived contracts.
First modifier that we are going to implement is an isAvailable modifier, which we will be using to check whether a certain domain name is available to be bought.
modifier isAvailable(bytes memory domain, bytes12 topLevel) { | |
// @dev - get the domain hash by the domain name and the TLD | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// @dev - check whether the domain name is available by checking if is expired | |
// if it was not registered at all the `expires` property will be default: 0x00 | |
require( | |
domainNames[domainHash].expires < block.timestamp, | |
"Domain name is not available." | |
); | |
// continue with execution | |
_; | |
} |
The next thing the collectDomainNamePayments modifier that we are going to use as the way for faster check if the user provided the right amount of money for the payment.
modifier collectDomainNamePayment(bytes memory domain) { | |
// @dev - get the price for the provided domain | |
uint domainPrice = getPrice(domain); | |
// @dev - require the payment sent to be enough for | |
// the current domain cost | |
require( | |
msg.value >= domainPrice, | |
"Insufficient amount." | |
); | |
// continue execution | |
_; | |
} |
We need a modifier which we will use for checking whether the transaction initiator (msg.sender) is the owner of the certain domain — isDomainOwner.
modifier isDomainOwner(bytes memory domain, bytes12 topLevel) { | |
// @dev - get the hash of the domain with the provided TLD. | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// @dev - check whether the msg.sender is the owner of the domain name | |
require( | |
domainNames[domainHash].owner == msg.sender, | |
"You are not the owner of this domain." | |
); | |
// continue with execution | |
_; | |
} |
And lastly, we need another two modifiers isDomainNameLengthAllowed and isTopLevelLengthAllowed for checking if the length of the provided domain name or TLD is allowed respectively.
modifier isDomainNameLengthAllowed(bytes memory domain) { | |
// @dev - check if the provided domain is with allowed length | |
require( | |
domain.length >= DOMAIN_NAME_MIN_LENGTH, | |
"Domain name is too short." | |
); | |
// continue with execution | |
_; | |
} |
modifier isTopLevelLengthAllowed(bytes12 topLevel) { | |
// @dev - require the TLD lenght to be equal or greater | |
// than `TOP_LEVEL_DOMAIN_MIN_LENGTH` constant | |
require( | |
topLevel.length >= TOP_LEVEL_DOMAIN_MIN_LENGTH, | |
"The provided TLD is too short." | |
); | |
// continue with execution | |
_; | |
} |
State variables and constants
We are going to add some constants which names speak for what they are actually used for:
/** CONSTANTS */ | |
uint constant public DOMAIN_NAME_COST = 1 ether; | |
uint constant public DOMAIN_NAME_COST_SHORT_ADDITION = 1 ether; | |
uint constant public DOMAIN_EXPIRATION_DATE = 365 days; | |
uint8 constant public DOMAIN_NAME_MIN_LENGTH = 5; | |
uint8 constant public DOMAIN_NAME_EXPENSIVE_LENGTH = 8; | |
uint8 constant public TOP_LEVEL_DOMAIN_MIN_LENGTH = 1; | |
bytes1 constant public BYTES_DEFAULT_VALUE = bytes1(0x00); |
and the state variables where we are going to store the domain names, payment receipts:
/** STATE VARIABLES */ | |
// @dev - storing the DomainHash (bytes32) to its details | |
mapping (bytes32 => DomainDetails) public domainNames; | |
// @dev - all the receipt hashes/keys/ids for certain address | |
mapping(address => bytes32[]) public paymentReceipts; | |
// @dev - the details for a receipt by its hash/key/id | |
mapping(bytes32 => Receipt) public receiptDetails; |
Events
Events in Solidity give an abstraction on top of EVM’s (Ethereum Virtual Machine) logging functionality. An application (e.g. our UI) can subscribe to an event and listen for this events.
Events are defined with a number of parameters and their type. When an event is fired, its arguments are stored in the transaction’s log.
Notice the indexed attribute — in Solidity, you can add up to 3 indexed parameters for an event, which will add them to a special structure called “topic”.
We will get to this point in part 2 when we create the UI for the dApp.
For now, let us just add an event for every action that we are taking on our smart contract. That would be logging that a certain domain name was registered, also log if a domain was renewed. We will add an event for logging domain edits and transfers of a domain ownership. Lastly, we log the money operations and receipt issuing.
/** | |
* EVENTS | |
*/ | |
event LogDomainNameRegistered( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel | |
); | |
event LogDomainNameRenewed( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel, | |
address indexed owner | |
); | |
event LogDomainNameEdited( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel, | |
bytes15 newIp | |
); | |
event LogDomainNameTransferred( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel, | |
address indexed owner, | |
address newOwner | |
); | |
event LogPurchaseChangeReturned( | |
uint indexed timestamp, | |
address indexed _owner, | |
uint amount | |
); | |
event LogReceipt( | |
uint indexed timestamp, | |
bytes domainName, | |
uint amountInWei, | |
uint expires | |
); |
Step 4: Implementing functions
And now let’s get to the interesting part …
First, we have to implement some functions that are going to help us with our logic, since the logic we’ve been following so far is:
- As we know on the internet we could have domain names that are the same and only the TLD could be different (e.g.
hack.bg
andhack.com
) - We need to have a unique
id
for each domain name, combined with its TLD. - Solidity is bad with string manipulation
The key concept regarding the type
string
is that this is an array of UTF-8 characters, and can be seamlessly converted tobytes
. This is the only way of manipulating the string at all. But it is important to note that UTF-8 characters do not exactly match bytes. The conversion in either direction will be accurate, but there is not an immediate relation between each byte index and the corresponding string index.
For most things, there may be an advantage in representing the string directly as the typebytes
(avoiding conversions) …
Solution: we create a pure function for calculating the unique hash for provided the domain and topLevel called getDomainHash. It will basically calculate the hash (keccak256) of the provided domain name + its TLD.
/* | |
* @dev - Get (domain name + top level) hash used for unique identifier | |
* @param domain | |
* @param topLevel | |
* @return domainHash | |
*/ | |
function getDomainHash(bytes memory domain, bytes12 topLevel) public pure returns(bytes32) { | |
// @dev - tightly pack parameters in struct for keccak256 | |
return keccak256(abi.encodePacked(domain, topLevel)); | |
} |
By using this function we will be able to get the unique identifier for every domain name.
Following the same logic — we need a unique id
or serialNumber
for the payment receipts that we issue — function getReceiptKey:
/* | |
* @dev - Get recepit key hash - unique identifier | |
* @param domain | |
* @param topLevel | |
* @return receiptKey | |
*/ | |
function getReceiptKey(bytes memory domain, bytes12 topLevel) public view returns(bytes32) { | |
// @dev - tightly pack parameters in struct for keccak256 | |
return keccak256(abi.encodePacked(domain, topLevel, msg.sender, block.timestamp)); | |
} |
* Probably you’ve noticed that these two functions we’ve already referred in the modifiers definitions.
And a price checker for the provided domain name, since we are going to use this functionality very frequently:
/* | |
* @dev - Get price of domain | |
* @param domain | |
*/ | |
function getPrice( | |
bytes memory domain | |
) | |
public | |
pure | |
returns (uint) | |
{ | |
// check if the domain name fits in the expensive or cheap categroy | |
if (domain.length < DOMAIN_NAME_EXPENSIVE_LENGTH) { | |
// if the domain is too short - its more expensive | |
return DOMAIN_NAME_COST + DOMAIN_NAME_COST_SHORT_ADDITION; | |
} | |
// otherwise return the regular price | |
return DOMAIN_NAME_COST; | |
} |
… the real deal …
Register a domain name on the decentralized DNS
We continue with the register domain function — which is probably the most complex of all functions due to our requirements.
A short explanation of the function below — when a user wants to register a domain name, he passes the domain, TLD and IP, and provides a payment for the domain.
According to our requirements, some conditions should be met and that is why we’ve created some modifiers earlier at the beginning of this tutorial. They are going to help us to reuse the conditions checks when needed.
The first thing you notice is that the function is payable —this is required if we want to collect payment in this function, and then we are using
- isDomainNameLengthAllowed — checks whether the provided domain’s name length is allowed.
- isTopLevelLengthAllowed — checks whether the provided TLD length is allowed.
- isAvailable — checks if the domain name is available to be registered (e.g. is it not bought already or if it is expired).
- collectDomainNamePayment — checks what is the required amount of ETH to be sent to buy this domain name, according to the conditions for the length of the name. And collects the payment if the user has provided the needed value.
modifiers.
If some of the conditions in the modifiers are not met, the contract will stop executing and fire an error message, provided in the require() methods in our modifiers.
So if everything is OK, we are continuing with the execution of the function (line 19–67):
- What is the logic here?
We calculate the domain hash, create a domain object, that we store in the storage, then we issue and store a payment receipt and fire two events receipt issuing and domain registration respectively.
/* | |
* @dev - function to register domain name | |
* @param domain - domain name to be registered | |
* @param topLevel - domain top level (TLD) | |
* @param ip - the ip of the host | |
*/ | |
function register( | |
bytes memory domain, | |
bytes12 topLevel, | |
bytes15 ip | |
) | |
public | |
payable | |
isDomainNameLengthAllowed(domain) | |
isTopLevelLengthAllowed(topLevel) | |
isAvailable(domain, topLevel) | |
collectDomainNamePayment(domain) | |
{ | |
// calculate the domain hash | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// create a new domain entry with the provided fn parameters | |
DomainDetails memory newDomain = DomainDetails( | |
{ | |
name: domain, | |
topLevel: topLevel, | |
owner: msg.sender, | |
ip: ip, | |
expires: block.timestamp + DOMAIN_EXPIRATION_DATE | |
} | |
); | |
// save the domain to the storage | |
domainNames[domainHash] = newDomain; | |
// create an receipt entry for this domain purchase | |
Receipt memory newReceipt = Receipt( | |
{ | |
amountPaidWei: DOMAIN_NAME_COST, | |
timestamp: block.timestamp, | |
expires: block.timestamp + DOMAIN_EXPIRATION_DATE | |
} | |
); | |
// calculate the receipt hash/key | |
bytes32 receiptKey = getReceiptKey(domain, topLevel); | |
// save the receipt key for this `msg.sender` in storage | |
paymentReceipts[msg.sender].push(receiptKey); | |
// save the receipt entry/details in storage | |
receiptDetails[receiptKey] = newReceipt; | |
// log receipt issuance | |
emit LogReceipt( | |
block.timestamp, | |
domain, | |
DOMAIN_NAME_COST, | |
block.timestamp + DOMAIN_EXPIRATION_DATE | |
); | |
// log domain name registered | |
emit LogDomainNameRegistered( | |
block.timestamp, | |
domain, | |
topLevel | |
); | |
} |
Renew domain name
This function is similar to the register domain name function — it does the same thing basically, with the only difference of updating the expires property of the requested domain name with 1 year (365 days).
/* | |
* @dev - function to extend domain expiration date | |
* @param domain - domain name to be registered | |
* @param topLevel - top level | |
*/ | |
function renewDomainName( | |
bytes memory domain, | |
bytes12 topLevel | |
) | |
public | |
payable | |
isDomainOwner(domain, topLevel) | |
collectDomainNamePayment(domain) | |
{ | |
// calculate the domain hash | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// add 365 days (1 year) to the domain expiration date | |
domainNames[domainHash].expires += 365 days; | |
// create a receipt entity | |
Receipt memory newReceipt = Receipt( | |
{ | |
amountPaidWei: DOMAIN_NAME_COST, | |
timestamp: block.timestamp, | |
expires: block.timestamp + DOMAIN_EXPIRATION_DATE | |
} | |
); | |
// calculate the receipt key for this domain | |
bytes32 receiptKey = getReceiptKey(domain, topLevel); | |
// save the receipt id for this msg.sender | |
paymentReceipts[msg.sender].push(receiptKey); | |
// store the receipt details in storage | |
receiptDetails[receiptKey] = newReceipt; | |
// log domain name Renewed | |
emit LogDomainNameRenewed( | |
block.timestamp, | |
domain, | |
topLevel, | |
msg.sender | |
); | |
// log receipt issuance | |
emit LogReceipt( | |
block.timestamp, | |
domain, | |
DOMAIN_NAME_COST, | |
block.timestamp + DOMAIN_EXPIRATION_DATE | |
); | |
} |
Edit domain name
For the edit domain name, according to the requirements, we should have the ability to update the IP address that the domain is pointing to and that should be a free operation (excluding gas costs).
/* | |
* @dev - function to edit domain name | |
* @param domain - the domain name to be editted | |
* @param topLevel - tld of the domain | |
* @param newIp - the new ip for the domain | |
*/ | |
function edit( | |
bytes memory domain, | |
bytes12 topLevel, | |
bytes15 newIp | |
) | |
public | |
isDomainOwner(domain, topLevel) | |
{ | |
// calculate the domain hash - unique id | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// update the new ip | |
domainNames[domainHash].ip = newIp; | |
// log change | |
emit LogDomainNameEdited(block.timestamp, domain, topLevel, newIp); | |
} |
Transfer domain
Transferring the domain name ownership is a trivial task as you’ve probably guessed. There are only a few things that are worth mentioning here:
- first, we check with the isDomainOwner modifier if the transaction initiator (msg.sender) is the owner of the domain
- second, important thing we do is to check whether the new owner’s address is not set to 0x0 address — to prevent domain ownership loss
- the rest is easy — get the domain unique id > update the ownership property with new owner’s address > log the change.
/* | |
* @dev - Transfer domain ownership | |
* @param domain - name of the domain | |
* @param topLevel - tld of the domain | |
* @param newOwner - address of the new owner | |
*/ | |
function transferDomain( | |
bytes memory domain, | |
bytes12 topLevel, | |
address newOwner | |
) | |
public | |
isDomainOwner(domain, topLevel) | |
{ | |
// prevent assigning domain ownership to the 0x0 address | |
require(newOwner != address(0)); | |
// calculate the hash of the current domain | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// assign the new owner of the domain | |
domainNames[domainHash].owner = newOwner; | |
// log the transfer of ownership | |
emit LogDomainNameTransferred( | |
block.timestamp, | |
domain, topLevel, | |
msg.sender, | |
newOwner | |
); | |
} |
Getters
We need to have few getters for accessing the data stored on the blockchain and also for the UI later.
We will implement functions for getting the current IP of a domain is pointed to — getIp, a function to get the list or receipt
/* | |
* @dev - Get ip of domain | |
* @param domain | |
* @param topLevel | |
*/ | |
function getIP( | |
bytes memory domain, | |
bytes12 topLevel | |
) | |
public | |
view | |
returns (bytes15) | |
{ | |
// calculate the hash of the domain | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// return the ip property of the domain from storage | |
return domainNames[domainHash].ip; | |
} |
/** | |
* @dev - Get receipt list for the msg.sender | |
*/ | |
function getReceiptList() public view returns (bytes32[] memory) { | |
return paymentReceipts[msg.sender]; | |
} |
/* | |
* @dev - Get single receipt | |
* @param receiptKey | |
*/ | |
function getReceipt(bytes32 receiptKey) public view returns (uint, uint, uint) { | |
return (receiptDetails[receiptKey].amountPaidWei, | |
receiptDetails[receiptKey].timestamp, | |
receiptDetails[receiptKey].expires); | |
} |
Withdraw
Finally, we want to have a function which is allowing the owner of the decentralized domain name system to be able to withdraw the ETH (funds) collected from domain registration/renewal payments.
/** | |
* @dev - Withdraw function | |
*/ | |
function withdraw() public onlyOwner { | |
msg.sender.transfer(address(this).balance); | |
} |
Finally — here is our smart contract:
And here’s the whole smart contract for the DDNS:
pragma solidity >=0.4.22 <0.6.0; | |
import "./common/Ownable.sol"; | |
import "./common/Destructible.sol"; | |
import "./libs/SafeMath.sol"; | |
contract DDNSService is Destructible { | |
/** USINGS */ | |
using SafeMath for uint256; | |
/** STRUCTS */ | |
struct DomainDetails { | |
bytes name; | |
bytes12 topLevel; | |
address owner; | |
bytes15 ip; | |
uint expires; | |
} | |
struct Receipt { | |
uint amountPaidWei; | |
uint timestamp; | |
uint expires; | |
} | |
/** CONSTANTS */ | |
uint constant public DOMAIN_NAME_COST = 1 ether; | |
uint constant public DOMAIN_NAME_COST_SHORT_ADDITION = 1 ether; | |
uint constant public DOMAIN_EXPIRATION_DATE = 365 days; | |
uint8 constant public DOMAIN_NAME_MIN_LENGTH = 5; | |
uint8 constant public DOMAIN_NAME_EXPENSIVE_LENGTH = 8; | |
uint8 constant public TOP_LEVEL_DOMAIN_MIN_LENGTH = 1; | |
bytes1 constant public BYTES_DEFAULT_VALUE = bytes1(0x00); | |
/** STATE VARIABLES */ | |
mapping (bytes32 => DomainDetails) public domainNames; | |
mapping(address => bytes32[]) public paymentReceipts; | |
mapping(bytes32 => Receipt) public receiptDetails; | |
/** | |
* MODIFIERS | |
*/ | |
modifier isAvailable(bytes memory domain, bytes12 topLevel) { | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
require( | |
domainNames[domainHash].expires < block.timestamp, | |
"Domain name is not available." | |
); | |
_; | |
} | |
modifier collectDomainNamePayment(bytes memory domain) { | |
uint domainPrice = getPrice(domain); | |
require( | |
msg.value >= domainPrice, | |
"Insufficient amount." | |
); | |
_; | |
} | |
modifier isDomainOwner(bytes memory domain, bytes12 topLevel) { | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
require( | |
domainNames[domainHash].owner == msg.sender, | |
"You are not the owner of this domain." | |
); | |
_; | |
} | |
modifier isDomainNameLengthAllowed(bytes memory domain) { | |
require( | |
domain.length >= DOMAIN_NAME_MIN_LENGTH, | |
"Domain name is too short." | |
); | |
_; | |
} | |
modifier isTopLevelLengthAllowed(bytes12 topLevel) { | |
require( | |
topLevel.length >= TOP_LEVEL_DOMAIN_MIN_LENGTH, | |
"The provided TLD is too short." | |
); | |
_; | |
} | |
/** | |
* EVENTS | |
*/ | |
event LogDomainNameRegistered( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel | |
); | |
event LogDomainNameRenewed( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel, | |
address indexed owner | |
); | |
event LogDomainNameEdited( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel, | |
bytes15 newIp | |
); | |
event LogDomainNameTransferred( | |
uint indexed timestamp, | |
bytes domainName, | |
bytes12 topLevel, | |
address indexed owner, | |
address newOwner | |
); | |
event LogPurchaseChangeReturned( | |
uint indexed timestamp, | |
address indexed _owner, | |
uint amount | |
); | |
event LogReceipt( | |
uint indexed timestamp, | |
bytes domainName, | |
uint amountInWei, | |
uint expires | |
); | |
/** | |
* @dev - Constructor of the contract | |
*/ | |
constructor() public { | |
} | |
/* | |
* @dev - function to register domain name | |
* @param domain - domain name to be registered | |
* @param topLevel - domain top level (TLD) | |
* @param ip - the ip of the host | |
*/ | |
function register( | |
bytes memory domain, | |
bytes12 topLevel, | |
bytes15 ip | |
) | |
public | |
payable | |
isDomainNameLengthAllowed(domain) | |
isTopLevelLengthAllowed(topLevel) | |
isAvailable(domain, topLevel) | |
collectDomainNamePayment(domain) | |
{ | |
// calculate the domain hash | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// create a new domain entry with the provided fn parameters | |
DomainDetails memory newDomain = DomainDetails( | |
{ | |
name: domain, | |
topLevel: topLevel, | |
owner: msg.sender, | |
ip: ip, | |
expires: block.timestamp + DOMAIN_EXPIRATION_DATE | |
} | |
); | |
// save the domain to the storage | |
domainNames[domainHash] = newDomain; | |
// create an receipt entry for this domain purchase | |
Receipt memory newReceipt = Receipt( | |
{ | |
amountPaidWei: DOMAIN_NAME_COST, | |
timestamp: block.timestamp, | |
expires: block.timestamp + DOMAIN_EXPIRATION_DATE | |
} | |
); | |
// calculate the receipt hash/key | |
bytes32 receiptKey = getReceiptKey(domain, topLevel); | |
// save the receipt key for this `msg.sender` in storage | |
paymentReceipts[msg.sender].push(receiptKey); | |
// save the receipt entry/details in storage | |
receiptDetails[receiptKey] = newReceipt; | |
// log receipt issuance | |
emit LogReceipt( | |
block.timestamp, | |
domain, | |
DOMAIN_NAME_COST, | |
block.timestamp + DOMAIN_EXPIRATION_DATE | |
); | |
// log domain name registered | |
emit LogDomainNameRegistered( | |
block.timestamp, | |
domain, | |
topLevel | |
); | |
} | |
/* | |
* @dev - function to extend domain expiration date | |
* @param domain - domain name to be registered | |
* @param topLevel - top level | |
*/ | |
function renewDomainName( | |
bytes memory domain, | |
bytes12 topLevel | |
) | |
public | |
payable | |
isDomainOwner(domain, topLevel) | |
collectDomainNamePayment(domain) | |
{ | |
// calculate the domain hash | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// add 365 days (1 year) to the domain expiration date | |
domainNames[domainHash].expires += 365 days; | |
// create a receipt entity | |
Receipt memory newReceipt = Receipt( | |
{ | |
amountPaidWei: DOMAIN_NAME_COST, | |
timestamp: block.timestamp, | |
expires: block.timestamp + DOMAIN_EXPIRATION_DATE | |
} | |
); | |
// calculate the receipt key for this domain | |
bytes32 receiptKey = getReceiptKey(domain, topLevel); | |
// save the receipt id for this msg.sender | |
paymentReceipts[msg.sender].push(receiptKey); | |
// store the receipt details in storage | |
receiptDetails[receiptKey] = newReceipt; | |
// log domain name Renewed | |
emit LogDomainNameRenewed( | |
block.timestamp, | |
domain, | |
topLevel, | |
msg.sender | |
); | |
// log receipt issuance | |
emit LogReceipt( | |
block.timestamp, | |
domain, | |
DOMAIN_NAME_COST, | |
block.timestamp + DOMAIN_EXPIRATION_DATE | |
); | |
} | |
/* | |
* @dev - function to edit domain name | |
* @param domain - the domain name to be editted | |
* @param topLevel - tld of the domain | |
* @param newIp - the new ip for the domain | |
*/ | |
function edit( | |
bytes memory domain, | |
bytes12 topLevel, | |
bytes15 newIp | |
) | |
public | |
isDomainOwner(domain, topLevel) | |
{ | |
// calculate the domain hash - unique id | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// update the new ip | |
domainNames[domainHash].ip = newIp; | |
// log change | |
emit LogDomainNameEdited(block.timestamp, domain, topLevel, newIp); | |
} | |
/* | |
* @dev - Transfer domain ownership | |
* @param domain - name of the domain | |
* @param topLevel - tld of the domain | |
* @param newOwner - address of the new owner | |
*/ | |
function transferDomain( | |
bytes memory domain, | |
bytes12 topLevel, | |
address newOwner | |
) | |
public | |
isDomainOwner(domain, topLevel) | |
{ | |
// prevent assigning domain ownership to the 0x0 address | |
require(newOwner != address(0)); | |
// calculate the hash of the current domain | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// assign the new owner of the domain | |
domainNames[domainHash].owner = newOwner; | |
// log the transfer of ownership | |
emit LogDomainNameTransferred( | |
block.timestamp, | |
domain, topLevel, | |
msg.sender, | |
newOwner | |
); | |
} | |
/* | |
* @dev - Get ip of domain | |
* @param domain | |
* @param topLevel | |
*/ | |
function getIP( | |
bytes memory domain, | |
bytes12 topLevel | |
) | |
public | |
view | |
returns (bytes15) | |
{ | |
// calculate the hash of the domain | |
bytes32 domainHash = getDomainHash(domain, topLevel); | |
// return the ip property of the domain from storage | |
return domainNames[domainHash].ip; | |
} | |
/* | |
* @dev - Get price of domain | |
* @param domain | |
*/ | |
function getPrice( | |
bytes memory domain | |
) | |
public | |
pure | |
returns (uint) | |
{ | |
// check if the domain name fits in the expensive or cheap categroy | |
if (domain.length < DOMAIN_NAME_EXPENSIVE_LENGTH) { | |
// if the domain is too short - its more expensive | |
return DOMAIN_NAME_COST + DOMAIN_NAME_COST_SHORT_ADDITION; | |
} | |
// otherwise return the regular price | |
return DOMAIN_NAME_COST; | |
} | |
/** | |
* @dev - Get receipt list for the msg.sender | |
*/ | |
function getReceiptList() public view returns (bytes32[] memory) { | |
return paymentReceipts[msg.sender]; | |
} | |
/* | |
* @dev - Get single receipt | |
* @param receiptKey | |
*/ | |
function getReceipt(bytes32 receiptKey) public view returns (uint, uint, uint) { | |
return (receiptDetails[receiptKey].amountPaidWei, | |
receiptDetails[receiptKey].timestamp, | |
receiptDetails[receiptKey].expires); | |
} | |
/* | |
* @dev - Get (domain name + top level) hash used for unique identifier | |
* @param domain | |
* @param topLevel | |
* @return domainHash | |
*/ | |
function getDomainHash(bytes memory domain, bytes12 topLevel) public pure returns(bytes32) { | |
// @dev - tightly pack parameters in struct for keccak256 | |
return keccak256(abi.encodePacked(domain, topLevel)); | |
} | |
/* | |
* @dev - Get recepit key hash - unique identifier | |
* @param domain | |
* @param topLevel | |
* @return receiptKey | |
*/ | |
function getReceiptKey(bytes memory domain, bytes12 topLevel) public view returns(bytes32) { | |
// @dev - tightly pack parameters in struct for keccak256 | |
return keccak256(abi.encodePacked(domain, topLevel, msg.sender, block.timestamp)); | |
} | |
/** | |
* @dev - Withdraw function | |
*/ | |
function withdraw() public onlyOwner { | |
msg.sender.transfer(address(this).balance); | |
} | |
} |
If you are interested in seeing the whole project in action — you can check the whole working project — decentralized DNS in Github.
Stay tuned! Part two and three where we will be focusing on unit tests and UI implementation of the decentralized domain name system (DDNS) are coming soon …
Continue …
- Part two: Unit testing and deploying
- Part three: Building user interface (UI) for our dApp
Useful links
If you want to be informed for our next events and meet-ups — join us:
Blockchain Developers Meet-up Group:Blockchain Developers Meet-up (Bulgaria)
In this group, we will share knowledge about blockchain and smart contract development. We will talk about Ethereum…meetup.com
Blockchain Developers Facebook Group:Log into Facebook | Facebook
Log into Facebook to start sharing and connecting with your friends, family, and people you know.www.facebook.com
Follow us on social media:
Also published on Medium.