Ethereum Smart Contract Migration

This is an excerpt from the Concurrence Exploration of Contract Migration.

As bugs are discovered or new functionality is needed, we will want a method of migrating from a predecessor to a descendant. As mentioned in the Contract Lineage section, we will try to keep contracts simple and we will create a linked list of lineage so other contracts and scripts both on and off the blockchain can follow a trail of addresses to the latest version.

Let’s start with a new contract called Store that will hold current prices of the top cryptocurrencies (this will also help demonstrate some aspects of an oracle). This contract will use the mapping data type to store a bytes32 => uint relationship called price:

Only the owner of the contract will be able to run a miner that will call the setPrice() function to update the price mapping. Then, other contracts on the the blockchain can call getPrice() to retrieve the price of a certain currency. We’ll use the internet endpoint https://api.coinmarketcap.com/v1/ticker/ to retrieve data. This url will be passed to the Store constructor to signal to miners where to get their data.

We will also extend a new contract called Predecessor that enables the owner to define a chain of descendant addresses:

Notice that the price mapping in Store is not public like previous contracts and we have a getPrice() function instead. This allows us a little more control when data is requested. Instead of just delivering the price immediately, we can look to see if a descendant is set and forward the getPrice() along.

Let’s compile and deploy the Store:

node compile Store 
node deploy Store

(Deployment transaction on etherscan.io)

0x68a3724eb459c0d70c7f3148b86a1b42e718c27b

(We won’t be including the javascript code necessary to make these queries, they are almost exactly the same as others in previous sections.)

And if we ask it for the current price of a currency, we should get 0x0 back:

node contract getPrice Store null ETH  
PRICE OF [ETH]: $0 USD

Time to build our first version of a request miner to power this simplified, centralized oracle:

If we run that, it should retrieve the prices and update the contract:

node contract minePrice Store null BTC,ETH,XRP,BCH,LTC  
** Calling price source url:https://api.coinmarketcap.com/v1/ticker/ 
** Symbols: [ 'BTC', 'ETH', 'XRP', 'BCH', 'LTC' ]
**== setting price for BTC to 5678190000000000
**== setting price for ETH to 336595000000000
**== setting price for XRP to 263983000000.00003
**== setting price for BCH to 314947000000000
**== setting price for LTC to 65467400000000
ETH 0x40fd6e3097c25fab8f81caae9bd16d44c5de917393e451b27d09b37bd86fdc64 LTC 0x85c55ab4275129132c3f044933ebcca5dec3798a2bc9cf8ba616bee86909774e BCH 0x676aafbfbe07f0d4e0e49599b686d473438113b5358ada27f4349f018ef23ccd XRP 0x1f40ea0c0d776525fc2cd675578f4013d6c00ed4e13feeeb001dccc90168c4df BTC 0xb57d91a2fe56b5c48d9865449e96ef6e171da084bc94a6186ee8d29000517364

(Example of one of the Miner transactions [LTC] on etherscan.io)

Yikes, if this was on the mainnet that would be pretty expensive; it just cost $0.32 to set the price of just the [LTC] mapping. This gives us an idea of what future miner expenses will be and helps us plan our infrastructure. As mentioned in the Off-chain Consensus section, we will want to do as much as we can off-chain, but eventually, in order to make information available to other contracts, we’ll have to write to the blockchain.

Let’s ask our contract what the current price of Litecoin is:

node contract getPrice Store null LTC  
PRICE OF [LTC]: $65.4674 USD

And then let’s run another round of mining:

node contract minePrice Store null BTC,ETH,XRP,BCH,LTC  
** Calling price source url:https://api.coinmarketcap.com/v1/ticker/ ** Symbols: [ 'BTC', 'ETH', 'XRP', 'BCH', 'LTC' ] 
**== setting price for BTC to 5687570000000000
**== setting price for ETH to 336711000000000
**== setting price for XRP to 263952000000.00003
**== setting price for BCH to 315263000000000
**== setting price for LTC to 65718300000000
BTC 0x71a874871cfb9083598a2ed50c49b525bcbcad1a73f8f2214312460d73fedba5 BCH 0x0f2891051e472b915cfadb91f3691bd4733768f129e59ca8709f0deabddf2ee1 LTC 0x291da538e1db6545e34edb73fefea4806bfa0f3155ba6cf7b28665f9b105762c XRP 0xdef369acd5b111e31df1c90dbc5afb67e76d7099f24329fa8e325129b50f625f ETH 0xc767d9a6ca3d782b0daf810de60cd03ce3edde2412fef40172786bccdf9f83a5

We should see that the price of Litecoin went up slightly:

node contract getPrice Store null LTC  
PRICE OF [LTC]: $65.7183 USD (previously $65.4674)

Awesome, we have good data on the chain, let’s write a client contract called EthVsBch that will tell us if Ethereum or Bitcoin Cash is worth more:

Let’s compile and deploy EthVsBch with the Store address hardcoded in arguments.js:

module.exports = ["0x68a3724eb459c0d70c7f3148b86a1b42e718c27b"]
node compile EthVsBch
node deploy EthVsBch

(Deployment transaction on etherscan.io)

0xc46A9BAE68Fb60402049202521279494D440F493

Now let’s get the state of EthVsBch and it will hit the Store and perform its logic:

node contract getState EthVsBch

CURRENT WINNER: Result { '0': 'ETH', '1': '336711000000000' }

Assuming BCH and ETH prices aren’t going to cross during this exploration, we can’t really test all the possibilities, but let’s go run another round of mining and then check back in:

node contract minePrice Store null BTC,ETH,XRP,BCH,LTC

** Calling price source url:https://api.coinmarketcap.com/v1/ticker/
** Symbols: [ 'BTC', 'ETH', 'XRP', 'BCH', 'LTC' ]
**== setting price for BTC to 5699690000000000
**== setting price for ETH to 336907000000000
**== setting price for XRP to 263843000000
**== setting price for BCH to 316191000000000
**== setting price for LTC to 65846500000000.01

BTC 0xbd5f2ee16e87b7b7d1fa3bf0980af6defd3c0a6033b8d1af7461a7fd56ca78c8
XRP 0x73df00e7bca3f4bb1747056210d904c6badf9ae13e06639f0434e8db2ba0d9b7
ETH 0x92d0ffb887b7967c50d6dea88d11d8c59e93532fd50ab348f7b3ce7b318fbf4d
BCH 0x7fb8f2ae02a88a1d0d2d6618624e2ba1dada2135dfdf976350a141c80caffd8c
LTC 0xe3b60849d769d3351f445d9620166020993e25c59432935dbe1cdebbf997878a

node contract getState EthVsBch

CURRENT WINNER: Result { '0': 'ETH', '1': '336907000000000' }
(previously '336711000000000')

Great, so as our miner continues to supply the Store contract with data, other contracts on the blockchain can source that data to make decisions. But, how would a contract know if the data in Store has grown stale? What if, down the road, we get requests from developers to add in a uint that will tell them how old the prices are? Let’s do that now:

Notice this new version of Store keeps a lastUpdate uint that is set to the block.number as a miner runs an update. This contract also has to be built to be backwards compatible because the first version of EthVsBch needs to continue to work with the previous Store address hardcoded in, but we only want the miner to update one contract (single source of truth) due to gas costs and complexity. Let’s compile and deploy the next version of Store:

node compile Store
node deploy Store

(Deployment transaction on etherscan.io)

0xD0557B2c5A11F8B5F2635Bfa57dEb8dCF6021475
node contract getState Store

OWNER:0xA3EEBd575245E0bd51aa46B87b1fFc6A1689965a
SOURCE:https://api.coinmarketcap.com/v1/ticker/
DESCENDANT:0x0000000000000000000000000000000000000000

node contract getPrice Store null ETH

PRICE OF [ETH]: $0 USD

node contract getLastUpdate Store

LastUpdate:0

So the new contract is empty and the old contract is still chugging along fine. The transition process requires that we get some data in the new contract before setting up the lineage so external developers’ contracts don’t get empty data. Let’s mine into the new Store:

node contract minePrice Store null BTC,ETH,XRP,BCH,LTC

** Calling price source url:https://api.coinmarketcap.com/v1/ticker/
** Symbols: [ 'BTC', 'ETH', 'XRP', 'BCH', 'LTC' ]
**== setting price for BTC to 5681640000000000
**== setting price for ETH to 336505000000000
**== setting price for XRP to 264403000000
**== setting price for BCH to 315928000000000
**== setting price for LTC to 65956700000000

BCH 0x0fd5fe447795cbff9b33f0a349caa8545bd8fb4db3149a2ed75f36e9dfacd409
ETH 0x6b134c98fe962a7bbfb6f27fb155b49dce980d7ca3eab6a22b6c299465efcc7e
BTC 0x6102e186a2f84447dd9143e5a11a943ddddb0820e4eccb06184c0f616b8986e8
XRP 0x2b7cad141ebc3f4e9687202c7df8d23460f5fa65a34711d88b9ced1b6553374d
LTC 0x6da8151bb660e489d94bb592cc42c526cb88b70b15d3452273d82bf12e1b67d7
node contract getPrice Store null ETH

PRICE OF [ETH]: $336.505 USD

node contract getLastUpdate Store

LastUpdate:1878626

So now we can get the last block number the prices were updated and we can use this in our external contracts to only execute functionality if we are dealing with current prices.

With the new contract populated with data, we are ready for legacy contracts to start interfacing with it instead of previous versions. To trigger this migration, we will call setDescendant() from the Predecessor to start passing getPrice() on to the latest version of Store:

node contract getDescendant Store previous  
DESCENDANT:0x0000000000000000000000000000000000000000  
node contract setDescendant Store previous

(Migration transaction on etherscan.io)

node contract getDescendant Store previous

DESCENDANT:0xD0557B2c5A11F8B5F2635Bfa57dEb8dCF6021475

Fantastic, the predecessor Store now has a descendant Store, let’s see if the EthVsBch contract is happy communicating with the new Store:

node contract getState EthVsBch  
CURRENT WINNER: Result { '0': 'ETH', '1': '336505000000000' }

It is getting the latest price from the latest Store even with the old Store address hardcoded in. Our migration is complete and the miner can continue updating only the latest Store.

Now, let’s say the developer decides to implement the block number check in their EthVsBch contract:

Notice the developer is checking to see if the price information is 10 blocks stale and returning an error if so.

Let’s compile and deploy this to test it out. Again, the developer will hardcode (bruh, isn’t there a better way?) the new Store address:

module.exports = ["0xD0557B2c5A11F8B5F2635Bfa57dEb8dCF6021475"]
node compile EthVsBch
node deploy EthVsBch

(Deployment transaction on etherscan.io)

0x8507E664d156d9deF72759F314629486783CDC49

With the new version of the contract out there, and no mining data posted to the Store for a while, we should get a message about stale data:

node contract getState EthVsBch

CURRENT WINNER: Result { '0': 'OUTOFDATE', '1': '0' }

Now let’s run the miner and get that data updated:

node contract minePrice Store null BTC,ETH,XRP,BCH,LTC

** Calling price source url:https://api.coinmarketcap.com/v1/ticker/
** Symbols: [ 'BTC', 'ETH', 'XRP', 'BCH', 'LTC' ]
**== setting price for BTC to 5686980000000000
**== setting price for ETH to 338896000000000
**== setting price for XRP to 264500000000
**== setting price for BCH to 316889000000000
**== setting price for LTC to 65747699999999.99

BTC 0x5991181b611b27aea7ee199e80f8f94ecf25ff84846f5e8afe97404ed7c0d585
ETH 0xc1649f7d16562b796318ef3c34e69a97a66a0f0f804cc00aa2c7e36355750816
BCH 0x170fca205d15bf57227a38d09e903718a22461a6289f6760b05dd120902b93f7
LTC 0x492ad533be63dfe8892001f6cb9d7c64de03d95829c3ae7adb72c47db847939a
XRP 0xca81f55994fb58b30f1f7bcb42cb072ce5a6665f29e3710bd79577f3cf77f712

node contract getState EthVsBch

CURRENT WINNER: Result { '0': 'ETH', '1': '338896000000000' }

Neat! A full development cycle with both our contract and a developer’s contract getting updated with new functionality without breaking the legacy versions.

This is an excerpt from the Concurrence Exploration of Contract Migration.

Read more about our Decentralized Oracle Exploration.