This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi~ @olekon Did you verified the scheme?