@ricmoo/rooted

0.0.1 • Public • Published

Rooted: Far-Too-Easy-To-Upgrade Contracts

The goal of Rooted is to simplify making upgradable Ethereum contracts.

With Rooted, any contract deployed by the same account will be deployed to the same Rooted Address, everytime.

So, to upgrade a contract, have it self-destruct itself (in some safe, gaurded manner) and then redeploy using the same account originally used.

How To Deploy

Since any given EOA will have exactly one Rooted Address, it is recommended you create a new EOA for each contract you wish Rooted. Otherwise, complication seeps back in, as you need contracts to dispatch deployment calls.

  1. Create a new EOA for your Contract
  2. Fund that account
  3. Deploy your contract as usual, except: instead of an empty to address, use v0-beta-0.rooted.eth.

That's it! When you wish to upgrade your contract, simply use whatever method exists on your contract to self-destruct it and then follow the above steps again, with the new contract bytecode.

Command-Line Interface

The command-line interface has the same options as the other ethers CLI utilities. To install, use npm install @ricmoo/rooted, and can then be used with the following usage:

Usage:
   rooted FILENAME [ OPTIONS ]

OPTIONS
  --contract                  specify the contract to deploy
  --args                      specify JSON encoded constructor args
  --no-optimize               do not run the optimizer

Example:

These operations were run on ropsten (the --network ropsten is omitted for brevity), so each transaction hash can be looked up on Etherscan.

/home/ricmoo> npm install -g @ricmoo/rooted

/home/ricmoo> cat Test1.sol
contract Test1 {
    address public owner = msg.sender;
    string public value;

    constructor(string _value) public {
        value = _value;
    }

    function die() public {
        require(msg.sender == owner);
        selfdestruct(owner);
    }
}

/home/ricmoo> cat Test2.sol
contract Test2 {
    address payable public owner = msg.sender;

    function value() public view returns (string memory) {
        return "The cat came back...";
    }

    function die() public {
        require(msg.sender == owner);
        selfdestruct(owner);
    }
}

# Deploy
/home/ricmoo> rooted --account wallet.json Test1.sol --args '[ "Hello World" ]'
Deploy: contracts/tests/test1.sol
  Contract Address:  0x266bBB07e802890024eBd03512FbED1E3c961d83
Response:
  Hash:  0x35317670d0edf57c4eb8c75c0fa080c81f8ab30cde27e7d2db30b50b96be7293
  
/home/ricmoo> export ADDR="0x266bBB07e802890024eBd03512FbED1E3c961d83"

/home/ricmoo> ethers --account wallet.json eval 'provider.getCode(process.env.ADDR)'
"0x608060405234801561001057600080fd5b50600436106100415760003560e01c806335f46994146100465780633fa4f245146100505780638da5cb5b146100cd575b600080fd5b61004e6100f1565b005b610058610116565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009257818101518382015260200161007a565b50505050905090810190601f1680156100bf5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100d56101a3565b604080516001600160a01b039092168252519081900360200190f35b6000546001600160a01b0316331461010857600080fd5b6000546001600160a01b0316ff5b60018054604080516020600284861615610100026000190190941693909304601f8101849004840282018401909252818152929183018282801561019b5780601f106101705761010080835404028352916020019161019b565b820191906000526020600020905b81548152906001019060200180831161017e57829003601f168201915b505050505081565b6000546001600160a01b03168156fea2646970667358221220eb669057926d1e6d788f7eb4df1322e2d04c3e49790744f9c31fdca1a8e3074f64736f6c63430006040033"

/home/ricmoo> ethers eval '(new Contract(process.env.ADDR, [ "function value() view returns (string)" ], provider)).value()'
Hello World


# Destroy
/home/ricmoo> ethers --account wallet.json eval '(new Contract(process.env.ADDR, [ "function die()" ], accounts[0])).die()'
Response:
  Hash:  0xd74a31f8fb0ec36bcbae174a665e1f183d269bf122605a44355dfc6d0b7235ca

/home/ricmoo> ethers --account wallet.json eval 'provider.getCode(process.env.ADDR)'
"0x"


# Redeploy (same address, new code)
/home/ricmoo> rooted --account wallet.json Test2.sol
Deploy: contracts/tests/test2.sol
  Contract Address:  0x266bBB07e802890024eBd03512FbED1E3c961d83
Response:
  Hash:  0x89cb3165cb8a27d08094b198dbe8279147c226de293b69b86ccca91fedf621f1

/home/ricmoo> ethers --account wallet.json eval 'provider.getCode(process.env.ADDR)'
"0x608060405234801561001057600080fd5b50600436106100415760003560e01c806335f46994146100465780633fa4f245146100505780638da5cb5b146100cd575b600080fd5b61004e6100f1565b005b610058610116565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009257818101518382015260200161007a565b50505050905090810190601f1680156100bf5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100d5610144565b604080516001600160a01b039092168252519081900360200190f35b6000546001600160a01b0316331461010857600080fd5b6000546001600160a01b0316ff5b6040805180820190915260148152732a34329031b0ba1031b0b6b2903130b1b597171760611b602082015290565b6000546001600160a01b03168156fea26469706673582212205320b56a7ffb50e87057e9097f43c461aeeda9bd441fa781680ac5a46535cea164736f6c63430006040033"

/home/ricmoo> ethers --account wallet.json eval '(new Contract(process.env.ADDR, [ "function value() view returns (string)" ], accounts[0])).value()'
The cat came back...

Persistent Storage

Since a self-destructed contract loses all its storage, there is an external Storage contract available. To use it, simply specify the interface you'd like (the selector is ignored) with keys which must fit within a bytes32 and values which must also fit within a bytes32.

For example:

interface StorageUint256 {
    function get(uint256 key) view external returns (uint256);
    function set(uint256 key, uint256 value) external;
}        

interface StorageBytes32 {
    function get(bytes32 key) view external returns (bytes32);
    function set(bytes32 key, bytes32 value) external;
}        

interface StorageMishMash {
    function get(uint8 key) view external returns (bytes32);
    function set(uint8 key, bytes32 value) external;
}        

And in your contract, you can use:

function persistentValue() view public returns (uint256) {
    return StorageUint256(0x760158D4613e8851D0C5Ae906a81698da89f903a).get(0);
}

function setPersistentValue(uint256 _value) public {
    StorageUint256(0x760158D4613e8851D0C5Ae906a81698da89f903a).set(0, _value);
}                            

Notes:

  • The slots are shared per-caller, so make sure the keys do no collide; for example, bytes1(0x05) and uint8(5) would both occupy the same storage.
  • Accessing values in an external contract will incur addition gas costs; cache values and limit calls if possible

How does it work?

It works by using a combination of two of our previous Hackathon projects, Wisps and Lurch.

Wisps allow using create2 to deploy contracts with different bytecode to the same address, although it was intended to only live within a single transaction, this allows contracts to live as long as is desired.

Lurch allows execution of EVM bytecode within an Ethereum contract, which allows for hooks to alter the environment during runtime. In this case, opcodes like codecopy and caller are hijacked so the executing initcode thinks it is being deployed normally.

Caveats

  • This shold be thought of as fairly experimental at this point; please use it responsibly
  • On average Lurch costs quite a bit more to run a contract through, around 300k gas more

License

MIT License.

Package Sidebar

Install

npm i @ricmoo/rooted

Weekly Downloads

0

Version

0.0.1

License

MIT

Unpacked Size

15.2 kB

Total Files

4

Last publish

Collaborators

  • ricmoo