Skip to content

Instantly share code, notes, and snippets.

@Arachnid
Created May 24, 2016 09:49
Star You must be signed in to star a gist
Save Arachnid/4ca9da48d51e23e5cfe0f0e14dd6318f to your computer and use it in GitHub Desktop.
/**
* 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
throw;
}
function() {
bytes4 sig;
assembly { sig := calldataload(0) }
var len = _sizes[sig];
var target = _dest;
assembly {
// return _dest.delegatecall(msg.data)
calldatacopy(0x0, 0x0, calldatasize)
delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len)
return(0, len)
}
}
}
contract Example is Upgradeable {
uint _value;
function initialize() {
_sizes[bytes4(sha3("getUint()"))] = 32;
}
function getUint() returns (uint) {
return _value;
}
function setUint(uint value) {
_value = value;
}
}
@tjade273
Copy link

An interesting technique that I just though of to deal with the storage variable issue is that the contracts that this forwards to could write sto storage starting at sha3(this) for that particular contract. In a delegatecall is this maintained?

@tjade273
Copy link

I just tested it, it's not. So maybe the contract should accept a storage pointer where it is allowed to write to storage....

@romanman
Copy link

@Arachnid : Nick, that contract is a great practice during development
stages of any platform, however to give strong security to the users value
we should add to that contract something to make updateable functionality
obsolete.

something like:

function cancelUpdates(){

      if (!msg.sender == owner) availableUpdate = false;

}

function replace(address target) internal {
        
        if (!availableUpdates) throw; 

        _dest = target;
        target.delegatecall(bytes4(sha3("initialize()")));
 }

@alesl
Copy link

alesl commented Apr 5, 2017

What is the reason 10000 gas is deducted when making delegatecall, since 40 (or 700 after EIP150) should suffice?

@NiklasKunkel
Copy link

@alesl

Actually if you look at the latest revision (specification 2) of EIP150 you'll see that the gas cost of delegatecall has been increased to 4000 for every block after Metropolis. So having a contract only subtract 700 would fail as soon as Metropolis is released. This is just a guess, but I think to answer your original question the author is envisioning future gas cost increases for delegatecall and trying to give himself some cushion room.

@olekon
Copy link

olekon commented Oct 12, 2017

I just wanted to make sure I understood the concept and usage scenarios correctly. In order for Dispatcher to work properly it should have the same storage scheme as Example contracts, right? I've made a fork to this gist and extended it with modified Example contracts and test file with supposed usage scenario here https://gist.github.com/olekon/27710c731c58fd0e0bd2503e02f4e144
Could you please look at it and correct me if anything is wrong.

@olekon
Copy link

olekon commented Oct 13, 2017

@Arachnid, Is it possible to chain dispatchers?
Imagine there are numerous contracts that need to be upgradeable. They should inherit Dispatcher contract. When changes are needed to be 'uploaded', every contract should call 'replace' method. In case of numerous contract that can be difficult and uncontrollable.
What I ask is how can one build such scheme: all the contracts are targeted to the main dispatcher which can replace its own 'working' target.

I tried to play with Dispatcher classes (https://gist.github.com/olekon/27710c731c58fd0e0bd2503e02f4e144) but getUint in the last test returns garbage

@junjizhi
Copy link

In this gist I didn't see any access control logic, or did I miss anything? The question is, can anyone upgrade the target contract?

@cyberience
Copy link

I guess this method would not work with the Zeppelin contracts, as they have BasicToken that has balances as a mapping, and if you change that basicToken to a newer version, you would lose all the stored data within balances, unless there is a way to override balances with the calling contract. I might be confused here, but if any of you could shed some light, its appreciated.

@avatar-lavventura
Copy link

Is it possible to add new function into upgraded contract? @Arachnid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment