[Part one] Build a Decentralized Domain Name System (DDNS) dApp on top of Ethereum
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).
- 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.
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:
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.
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.
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”.
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.
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
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.
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.
We need a modifier which we will use for checking whether the transaction initiator (msg.sender) is the owner of the certain domain — isDomainOwner.
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.
State variables and constants
We are going to add some constants which names speak for what they are actually used for:
and the state variables where we are going to store the domain names, payment receipts:
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.
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.
- We need to have a unique
idfor each domain name, combined with its TLD.
- Solidity is bad with string manipulation
The key concept regarding the type
stringis 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.
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
serialNumber for the payment receipts that we issue — function getReceiptKey:
* 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:
… 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.
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.
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).
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).
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.
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.
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.
Finally — here is our smart contract:
And here’s the whole smart contract for the DDNS:
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 …
- Part two: Unit testing
- Part three: Building user interface (UI) for our dApp [coming soon]
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…
Blockchain Developers Facebook Group:
Log into Facebook | Facebook
Log into Facebook to start sharing and connecting with your friends, family, and people you know.
Follow us on social media:
About the author: Milen Radkov /Founder & CEO at hack/ is also heading the technical team at @hackbg. He has experience building and delivering successful complex software systems and projects for big enterprises and small startups. A software developed by him and his colleagues is being used by over 1000+ retail stores today. Milen has also extensive experience in blockchain development and is a well-known figure in Bulgaria’s blockchain ecosystem.