[Part one – Smart Contracts] Build a Decentralized Domain Name System (DDNS) on top of Ethereum

Posted underTutorials
Cover Image for [Part one – Smart Contracts] Build a Decentralized Domain Name System (DDNS) on top of Ethereum

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

  1. 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:

Figure 1 — Decentralized DNS (domain name system) Architecture

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;
}
}
SafeMath.sol — Safe Mathematical operations in Solidity

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;
}
}
Ownable.sol — Ownership logic solidity
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);
}
}
Destructible.sol — Smart contract lifecycle logic solidity

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 {
}
}
DDNS initial contract

The directory tree of our project could be looking like this at this point:

Figure 2 — Project structure for Decentralized Domain Name System

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;
}
}
DomainDetails Structure for the DDNS implementation

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;
}
view raw Receipt struct hosted with ❤ by GitHub
Receipt structure for the DDNS implementation

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
_;
}
isAvailable modifier – DDNS

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
_;
}
collectDomainNamePayment modifier for the DDNS implementation

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
_;
}
isDomainOwner modifier – DDNS

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
_;
}
isDomainNameLengthAllowed modifier
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
_;
}
isTopLevelLengthAllowed modifier

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);
view raw DDNS-constants hosted with ❤ by GitHub
Constants used in our DDNS smart contract

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;
State variables for storing our data in the DDNS

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
);
view raw DDNS-events hosted with ❤ by GitHub
Defined events for the DDNS project implementations

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 and hack.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 to bytes. 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 type bytes (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));
}
getDomainHash — calculate the keccak256 hash of domain + TLD for the DDNS

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));
}
Calculate the receipt serial number/key — used as unique id for DDNS purposes

* 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;
}
Domain name price checker function — DDNS

… 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
);
}
Register domain function — DDNS (decentralized domain name system)

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
);
}
Renew Domain Name — DDNS

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);
}
Edit domain name function — DDNS

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
);
}
Transfer domain name ownership — DDNS

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 ids for certain account –getReceiptList, and a single receipt details getter — getReceipt.

/*
* @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;
}
Get IP of the provided domain — DDNS
/**
* @dev - Get receipt list for the msg.sender
*/
function getReceiptList() public view returns (bytes32[] memory) {
return paymentReceipts[msg.sender];
}
Get receipt list function — DDNS
/*
* @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);
}
Get single receipt details function — DDNS

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);
}
Withdraw function — DDNS

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);
}
}
view raw DDNSService.sol hosted with ❤ by GitHub
Decentralized Domain Name System – Smart Contract

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.

In This Article


    Milen
    About the author:

    Milen

    Milen Radkov has experience building and delivering successful complex software systems and projects for both big enterprises and small startups. Being actively developing across different blockchain protocols, platforms and projects for the past 5 years, he has gained extensive experience in blockchain development, architectures, consensus algorithms and token economics. Milen is a well-known figure in the blockchain space.


    More Stories

    Cover Image for The Hidden Threats in Code Review Requests: A Cautionary Tale for Developers

    The Hidden Threats in Code Review Requests: A Cautionary Tale for Developers

    As developers, we’re often approached to troubleshoot or optimize projects. While most requests are legitimate, sometimes they conceal malicious intent. […]

    Read more
    Cover Image for Real-World Assets and the Future of DeFi

    Real-World Assets and the Future of DeFi

    Real-world assets are blockchain-based digital tokens that represent physical and traditional financial assets. The first wave of decentralized finance (DeFi) […]

    Read more

    Have a project in mind?

    We have the expertise! Drop us a line and lets talk!