Ethereum Tutorials

SMART CONTRACTS

"Duke" Contract: Final Version

In "Duke of Ether" code that I used in previous chapters contains intentional errors. Some of them were fixed and explained, some remained hidden for the reader to discover.

Now it is time to create the working code, to verify it and to deploy it to real blockchain.

The first and most important difference between "educational" contract and the "real" one should be its usability: I hope you have noticed that SO FAR our "Duke" was not intended for long life. Someone would take the throne, then someone else... and soon enough the price too high for the game to continue.

To fix the problem, let's make changes to the rules: a Duke can occupy the throne for two months, then the price of a throne will decrease at a rate of 3% (of an original price) a day. It means that the price will go down linearly, from max. price at day 60, to min. allowed price at day 60+(100/3).

This rule makes it possible to compete for the throne, but if the competition stops, it will gradually decrease the price until the Throne becomes attractive enough for new candidates.

By the way, it may or may not be considered bad for the current Duke. On one hand, the reward decreases. On the other hand, it is better than nothing, and if no one comes to overthrow the current Duke, then the current Duke gets nothing. Reducing price resolves this issue.

Another change we are going to make is creating multiple countries. After all, isn't it better to become Duke of USA, rather than Duke of Ether? We are going to allow up to 12 countries; also, for an extra fee, it will be possible to both create new countries and to destroy existing ones.

Creating and destroying should be less rewarding, otherwise the game will be too erradic. So, we are going to a) make min. price for creating/destroying a country high enough and b) reward the creator/destroyer (when overthrown) with his money only: no extra bonus. This will make creating/destroying countries an act of heroism... well, kind of.

Finally, we are going to make "Duke of Ether" Web UI a bit different: after all, what does it mean exactly, "the Duke of USA"? Let's use something more meaningful, like "King", "Emperor", or, say, "Overlord". Well, maybe a simple "Ruler" or "Master" will do...

Final code of a contract:

                    
pragma solidity ^0.4.16;

// --- ShareHolder forward declaration ---
contract ShareHolder
{
    function addToShareHoldersProfit(string, string) payable
    {
        // ...
    }
}
// --- End of ShareHolder forward declaration ---

contract Ownable 
{
    address m_addrOwner;

    function Ownable()     
    { 
        m_addrOwner = msg.sender; 
    }

    modifier onlyOwner() 
    {
        if (msg.sender != m_addrOwner) 
        {
            revert();
        }
        _;
    }

    // ---

    function transferOwnership(address newOwner) onlyOwner 
    {
        m_addrOwner = newOwner;
    }

    // ---

    function isOwner() constant returns (bool bIsOwner) 
        { return (m_addrOwner == msg.sender); }

}

// ------

contract DukeOfEther is Ownable
{
    address m_addrShareHolder = 0;      // addr. of ShareHolder contract
    uint m_deployedAtBlock = 0;            // Initial block of a contract
    uint m_nOwnersMoney = 0;
    uint m_nNewCountryPrice = 1 ether;  // Pay to create NEW country
    uint m_nMinDukePrice = 1 finney;
    uint m_nDeterioration = 3;          // After day 60, -3% a day till m_nMinDukePrice
    uint m_NextDukePaymentIncrease = 150; // Next Duke pays 150% of current
    uint m_nNumberOfActiveCountries = 0;

    struct Country
    {
        bool m_bIsDestroyed;
        string m_strNickName;
        uint m_nLastDukeRiseDate;
        address m_addrCurrentDuke;
        uint m_nCurrentDukePaid;
        string m_strCountry;
    }
    
    mapping (string => Country) m_Countries;    // Russia, USA, China...
    
    // --- Events ---

    event updateDukeHistory(string strCountry, bool bIsDestroyed, 
        string strNickName, address indexed addrCurrentDuke, 
        uint nCurrentDukePaid, uint date);
    event errorMessage(string strMessage);

    // --- Functions ---

    function DukeOfEther()
    {
        m_deployedAtBlock = block.number;
        // ---
        addCountry("USA");
        addCountry("Russia");
        addCountry("China");
        addCountry("Japan");
        addCountry("Taiwan");
        addCountry("Ukraine");
    }

    function addCountry(string strCountry) internal
    {
        Country memory newCountryInfo;
        
        newCountryInfo.m_bIsDestroyed = false;
        newCountryInfo.m_strNickName = "Vacant";
        newCountryInfo.m_addrCurrentDuke = m_addrOwner;
        newCountryInfo.m_nCurrentDukePaid = m_nMinDukePrice;

        newCountryInfo.m_strCountry = strCountry;
        newCountryInfo.m_nLastDukeRiseDate = now;
        m_Countries[strCountry] = newCountryInfo;
        
        updateDukeHistory(strCountry, false, "Vacant", m_addrOwner, 0, now);
        
        m_nNumberOfActiveCountries++;
    }
    
    function verifyNickNameAndCountry(string strCountry, string strNickName) internal
    {
        if(bytes(strNickName).length > 30 || bytes(strCountry).length > 30)
        {
            errorMessage("String too long: keep strNickName and strCountry <= 30");
            revert();
        }
    }

    // ---
    
    function processShareHolderFee(uint nFee, string strNickName) internal
    {    
        // --- ShareHolder interface ---
        if(m_addrShareHolder != 0)
        {
            ShareHolder contractShareHolder = ShareHolder(m_addrShareHolder);
            contractShareHolder.addToShareHoldersProfit.value(nFee)(strNickName, "");
        }
    }

    // ---
    
    function addRemoveCountry(string strCountry, string strNickName, bool bDestroy) payable
    {
        verifyNickNameAndCountry(strCountry, strNickName);

        if(!bDestroy && m_nNumberOfActiveCountries >= 12)
        {
            errorMessage("Too many active countries. Consider destroying few.");
            revert();
        }
        else if(bDestroy && m_nNumberOfActiveCountries <= 3)
        {
            errorMessage("There should be at least 3 countries alive");
            revert();
        }
        
        // Demiurg pays more, then gets even
        if(msg.value < getPaymentToAddRemoveCountry(strCountry, bDestroy))
        {
            errorMessage("Sorry, but country costs more");
            revert();
        }
      
        // Note that we do not check if the country exists: 
        // we take money and promote next Duke or Destroyer

        address addrPrevDuke = m_Countries[strCountry].m_addrCurrentDuke;
        
        uint nFee = msg.value / 25;    // 4%
        uint nAmount = msg.value - nFee;
        uint nDemiurgsEffectiveAmount = 100 * nAmount / m_NextDukePaymentIncrease;
    
        processShareHolderFee(nFee, strNickName);
        
        updateDukeHistory(strCountry, bDestroy, strNickName, msg.sender, msg.value, now);    
        
        Country memory newCountryInfo;
        newCountryInfo.m_bIsDestroyed = bDestroy;
        newCountryInfo.m_strCountry = strCountry;
        newCountryInfo.m_strNickName = strNickName;
        newCountryInfo.m_nLastDukeRiseDate = now;
        newCountryInfo.m_addrCurrentDuke = msg.sender;
        newCountryInfo.m_nCurrentDukePaid = nDemiurgsEffectiveAmount;
        
        m_Countries[strCountry] = newCountryInfo;
        
        if(bDestroy)
            m_nNumberOfActiveCountries--;
        else
            m_nNumberOfActiveCountries++;
        
        m_nOwnersMoney += (nAmount - nDemiurgsEffectiveAmount);
                
        addrPrevDuke.transfer(nDemiurgsEffectiveAmount);
    }
    
    // ---
    
    function becomeDuke(string strCountry, string strNickName) payable
    {
        if(msg.value < getMinNextBet(strCountry))
            revert();

        if(bytes(strNickName).length > 30 || bytes(strCountry).length > 30)
        {
            errorMessage("String too long: keep strNickName and strCountry <= 30");
            revert();
        }
            
        Country memory countryInfo = m_Countries[strCountry];
        if(countryInfo.m_addrCurrentDuke == 0 || countryInfo.m_bIsDestroyed == true)
        {
            errorMessage("This country does not exist: use addRemoveCountry first");
            revert();
        }
        
        address addrPrevDuke = m_Countries[strCountry].m_addrCurrentDuke;
            
        uint nFee = msg.value / 25;    // 4%
        uint nOwnersFee = msg.value / 100;    // 1%
        m_nOwnersMoney += nOwnersFee;

        uint nPrevDukeReceived = msg.value - nFee - nOwnersFee;
       
        countryInfo.m_bIsDestroyed = false;
        countryInfo.m_strNickName = strNickName;
        countryInfo.m_nLastDukeRiseDate = now;
        countryInfo.m_addrCurrentDuke = msg.sender;
        countryInfo.m_nCurrentDukePaid = msg.value;
        countryInfo.m_strCountry = strCountry;
        
        m_Countries[strCountry] = countryInfo;
        
        updateDukeHistory(strCountry, false, strNickName, msg.sender, msg.value, now); 

        processShareHolderFee(nFee, strNickName);
        
        addrPrevDuke.transfer(nPrevDukeReceived);
    }
    
    // ---
    
    function withdrawDukeOwnersMoney() onlyOwner
    {
        m_addrOwner.transfer(m_nOwnersMoney);
    }
    
    // ---
    
    function setShareHolder(address addr) onlyOwner 
        { m_addrShareHolder = addr; }
    
    function isDestroyed(string strCountry) constant returns (bool) 
        { return m_Countries[strCountry].m_bIsDestroyed; }
    function getInitBlock() constant returns (uint nInitBlock) 
        { return m_deployedAtBlock; }
    function getDukeNickName(string strCountry) constant returns (string) 
        { return m_Countries[strCountry].m_strNickName; }
    function getDukeDate(string strCountry) constant returns (uint date) 
        { return m_Countries[strCountry].m_nLastDukeRiseDate; }
    function getCurrentDuke(string strCountry) constant returns (address addr) 
        { return m_Countries[strCountry].m_addrCurrentDuke; }
    function getCurrentDukePaid(string strCountry) constant returns (uint nPaid) 
        { return m_Countries[strCountry].m_nCurrentDukePaid; }
    function getMinNextBet(string strCountry) constant returns (uint nNextBet) 
    {
        if(m_Countries[strCountry].m_nCurrentDukePaid == 0)
            return 1 finney;

        uint nDaysSinceLastRise = 
            (now - m_Countries[strCountry].m_nLastDukeRiseDate) / 86400;
        uint nDaysMax = 60 + 100 / m_nDeterioration;
        if(nDaysSinceLastRise >= nDaysMax)
            return 1 finney;

        uint nCurrentDukeDue = m_Countries[strCountry].m_nCurrentDukePaid;
        if(nDaysSinceLastRise > 60)
            nCurrentDukeDue = nCurrentDukeDue * (nDaysSinceLastRise - 60) 
                * m_nDeterioration / 100; 
            
        return  m_NextDukePaymentIncrease * nCurrentDukeDue / 100; 
    }

    function getPaymentToAddRemoveCountry(string strCountry, bool bRemove) 
        constant returns (uint)
    {
        if(bRemove && m_Countries[strCountry].m_addrCurrentDuke == 0)
            return 0;
        else if(!bRemove && m_Countries[strCountry].m_addrCurrentDuke != 0 
            && m_Countries[strCountry].m_bIsDestroyed == false)    
            return 0;

        uint nPrice = m_NextDukePaymentIncrease * getMinNextBet(strCountry) / 100;
        if(nPrice < m_nNewCountryPrice)
            nPrice = m_nNewCountryPrice;
        return nPrice;    
    }
    
    // TBD: make it deletable
}
    

To test this contract, follow the link to Duke of Ether, note that you have to be connected to the main Ethereum blockchain, not to test or private ones.







(C) snowcron.com, all rights reserved

Please read the disclaimer