Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
pragma solidity ^0.4.10;
/**
* Base contract that all upgradeable contracts should use.
*
* Contracts implementing this interface are all called using delegatecall from
* a dispatcher. As a result, the _sizes and _dest variables are shared with the
* dispatcher contract, which allows the called contract to update these at will.
*
* _sizes is a map of function signatures to return value sizes. Due to EVM
* limitations, these need to be populated by the target contract, so the
* dispatcher knows how many bytes of data to return from called functions.
* Unfortunately, this makes variable-length return values impossible.
*
* _dest is the address of the contract currently implementing all the
* functionality of the composite contract. Contracts should update this by
* calling the internal function `replace`, which updates _dest and calls
* `initialize()` on the new contract.
*
* When upgrading a contract, restrictions on permissible changes to the set of
* storage variables must be observed. New variables may be added, but existing
* ones may not be deleted or replaced. Changing variable names is acceptable.
* Structs in arrays may not be modified, but structs in maps can be, following
* the same rules described above.
*/
contract Upgradeable {
mapping(bytes4=>uint32) _sizes;
address _dest;
/**
* This function is called using delegatecall from the dispatcher when the
* target contract is first initialized. It should use this opportunity to
* insert any return data sizes in _sizes, and perform any other upgrades
* necessary to change over from the old contract implementation (if any).
*
* Implementers of this function should either perform strictly harmless,
* idempotent operations like setting return sizes, or use some form of
* access control, to prevent outside callers.
*/
function initialize();
/**
* Performs a handover to a new implementing contract.
*/
function replace(address target) internal {
_dest = target;
target.delegatecall(bytes4(sha3("initialize()")));
}
}
/**
* The dispatcher is a minimal 'shim' that dispatches calls to a targeted
* contract. Calls are made using 'delegatecall', meaning all storage and value
* is kept on the dispatcher. As a result, when the target is updated, the new
* contract inherits all the stored data and value from the old contract.
*/
contract Dispatcher is Upgradeable {
function Dispatcher(address target) {
replace(target);
}
function initialize() {
// Should only be called by on target contracts, not on the dispatcher
require(false);
}
function changeTarget(address target) {
replace(target);
}
function() {
bytes4 sig;
assembly { sig := calldataload(0) }
var len = _sizes[sig];
var target = _dest;
uint8 callResult;
assembly {
// return _dest.delegatecall(msg.data)
calldatacopy(0x0, 0x0, calldatasize)
callResult := delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
return(0, len)
}
}
}
/* Example contracts storage scheme */
contract ExampleStorage {
uint public _value;
uint public _value2;
}
/* Dispatcher for Example contracts */
contract ExampleDispatcher is ExampleStorage, Dispatcher {
function ExampleDispatcher(address target)
Dispatcher(target) {
}
function initialize() {
_sizes[bytes4(sha3("getUint()"))] = 32;
_sizes[bytes4(sha3("getValues()"))] = 32 + 32;
}
}
/* Example contracts interface */
contract IExample {
function getUint() returns (uint);
function getValues() returns (uint256 v1, uint256 v2);
function setUint(uint value);
}
/* Base version of Example class */
contract ExampleV1 is ExampleStorage, IExample, Upgradeable {
function ExampleV1() {}
function initialize() {
_sizes[bytes4(sha3("getUint()"))] = 32;
_sizes[bytes4(sha3("getValues()"))] = 32 + 32;
}
function getUint() returns (uint) {
return _value;
}
function getValues() returns (uint256 v1, uint256 v2) {
v1 = _value;
v2 = 2;
}
function setUint(uint value) {
_value = value;
}
}
/* The 'upgraded' version of ExampleV1 which modifies getUint to return _value+10 */
contract ExampleV2 is ExampleStorage, IExample, Upgradeable {
function ExampleV2() {}
function initialize() {
_sizes[bytes4(sha3("getUint()"))] = 32;
_sizes[bytes4(sha3("getValues()"))] = 32 + 32;
}
function getUint() returns (uint) {
return _value + 10;
}
function getValues() returns (uint256 v1, uint256 v2) {
v1 = 100;
v2 = _value;
}
function setUint(uint value) {
_value = value;
}
}
var Dispatcher = artifacts.require("ExampleDispatcher");
var ExampleV1 = artifacts.require("ExampleV1");
var ExampleV2 = artifacts.require("ExampleV2");
var IExample = artifacts.require("IExample");
/*
Scenario:
1. Dispatcher points to ExampleV1 contract and changes its state.
2. Switch dispatcher to ExampleV2 contract.
Storage state should be preserved, but call to getUint should call modified version (+10)
3. Target contracts state should remain untouched.
*/
contract("Test upgradable behaviour", function(accounts) {
it("Dispatcher. Change targets", async function() {
var contract1 = await ExampleV1.new();
//set target to first contract
var dispatcher = await Dispatcher.new(contract1.address);
var iex = IExample.at(dispatcher.address);
assert.equal((await iex.getUint.call()).toNumber(), 0, "Initially should be 0");
await iex.setUint(20);
assert.equal(await iex.getUint.call(), 20, "Then should be 20");
assert.equal((await contract1.getUint.call()).toNumber(), 0, "Contract1 should be unchanged (0)");
//create new target - second contract
var contract2 = await ExampleV2.new();
assert.equal((await contract2.getUint.call()).toNumber(), 10, "Contract2 getvalue should be 10");
//change target to new
dispatcher.changeTarget(contract2.address);
assert.equal((await iex.getUint.call()).toNumber(), 30, "Initially should be 30 (20 from old + 10 from getter)");
await iex.setUint(12);
assert.equal((await iex.getUint.call()).toNumber(), 22, "Then should be 22");
//second contract state should remain unchanged
assert.equal((await contract2.getUint.call()).toNumber(), 10, "Contract2 should be unchanged (10)");
})
})
contract("Dispatcher chain", function(accounts) {
it("Connect dispatcher to dispatcher", async function() {
var contract1 = await ExampleV1.new();
var d1 = await Dispatcher.new(contract1.address);
var id1 = IExample.at(d1.address);
var d2 = await Dispatcher.new(d1.address);
var id2 = IExample.at(d2.address);
console.log(await id2.getUint.call());
})
})
@junjizhi

This comment has been minimized.

Copy link

commented Oct 22, 2017

When you upgrade from ExampleV1 to ExampleV2, if there is any ether balance in ExampleV1 account and we want to transfer to ExampleV2, then we can write a function in ExampleV1 to implement this transfer logic. Is that right? Wonder if this balance transfer step should happen automatically along with the upgrade process.

@cristicmf

This comment has been minimized.

Copy link

commented Nov 15, 2017

Hi~ @olekon Did you verified the scheme?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.