Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC20D: rent-compatible ERC20 migration #1892

Closed
Agusx1211 opened this issue Apr 1, 2019 · 0 comments
Closed

ERC20D: rent-compatible ERC20 migration #1892

Agusx1211 opened this issue Apr 1, 2019 · 0 comments

Comments

@Agusx1211
Copy link
Contributor

Agusx1211 commented Apr 1, 2019

eip: 1892
title: ERC20D rent-compatible ERC20 migration
author: Agustin Aguilar <[email protected]>
status: Work in progress (WIP)
type: Informational
created: 01-04-2019
resolution: --

Abstract

The following document describes a proposal on how to migrate an existing ERC20 (#20) token to a rent compatible implementation of the ERC20 (#20) standard.

Motivation

The implementation of any rent mechanism in the Ethereum network (#35) (#87) (#88) (#755) (#1418) may open a griefing channel on almost all existing ERC20 tokens.

Most of the existing implementations relay on the main contract to store all balances and allowances, this can be exploited to bloat the contract storage, rendering the rent expensive or impossible to pay.

This proposal describes a process to migrate an ERC20 token to a new contract, invulnerable to such griefing.

Glossary

This document references to the Distributed implementation of ERC20 as ERC20D.

Implementation

There is no need to change the existing ERC20 interface; the problem can be solved by splitting each balance/allowance registry into its own storage space.

The CREATE2 opcode allows us to use contracts as storage slots, without using any of the parent contract storage.

Storage unit contract

This is a Solidity implementation of a storage contract, the deployer of the contract is the owner, and can write values to the storage.

contract StorageUnit {
    address private owner;
    mapping(bytes32 => bytes32) private store;

    constructor() public {
        owner = msg.sender;
    }

    function write(bytes32 _key, bytes32 _value) external {
        require(msg.sender == owner);
        store[_key] = _value;
    }

    function read(bytes32 _key) external view returns (bytes32) {
        return store[_key];
    }
}

Library to read and write storage units

The library is used to read and write storage units with a syntax similar to the native storage of Solidity.

library DistributedStorage {

    // Returns true if _addr is not a contract
    function _isNotContract(address _addr) internal view returns (bool i) {
        assembly {
            i := iszero(extcodesize(_addr))
        }
    }

    // Returns the address of a contract slot for a given _space
    function _contractSlot(bytes32 _space) private view returns (address) {
        return address(
            uint256(
                keccak256(
                    abi.encodePacked(
                        byte(0xff),
                        address(this),
                        _space,
                        keccak256(type(StorageUnit).creationCode)
                    )
                )
            )
        );
    }

    // Deploys the contract for the _space slot
    function _deploy(bytes32 _space) private {
        bytes memory slotcode = type(StorageUnit).creationCode;
        assembly{
            pop(create2(0, add(slotcode, 0x20), mload(slotcode), _space))
        }
    }

    // Writes a value using another contract storage
    // the contract used is determined by the _space value
    // the storage slot is determined by the _key value
    function write(
        bytes32 _space,
        bytes32 _key,
        bytes32 _value
    ) internal {
        address store = _contractSlot(_space);
        
        // If the contract does not exist
        // deploy it before writing to the storage
        if (_isNotContract(store)) {
            deploy(_struct);
        }

        StorageUnit(store).write(_key, _value);
    }

    // Reads a value from another contract storage
    // the contract used is determined by the _space value
    // the storage slot is determined by the _key value
    function read(
        bytes32 _space,
        bytes32 _key
    ) internal view returns (bytes32) {
        address store = _contractSlot(_space);

        // If the contract does not exist
        // the readed value must be zero
        if (_isNotContract(store)) {
            return bytes32(0x0);
        }

        return StorageUnit(store).read(_key);
    }
}

ERC20D Implementation

The ERC20 interface can be implemented using the DistributedStorage library.

contract ERC20D {
    using DistributedStorage for bytes32;

    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);

    uint256 public totalSupply;

    // ///
    // ERC20 Implementation
    // ///

    function balanceOf(address _addr) external view returns (uint256) {
        return _balanceOf(_addr);
    }
    
    function allowance(address _addr, address _spender) external view returns (uint256) {
        return _allowance(_addr, _spender);
    }
    
    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }

    function approve(address _spender, uint256 _value) external returns (bool) {
        emit Approval(msg.sender, _spender, _value);
        _setAllowance(msg.sender, _spender, _value);
        return true;
    }
    
    function transfer(address _to, uint256 _value) external returns (bool) {
        _transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) external returns (bool) {
        uint256 allowanceFrom = _allowance(_from, msg.sender);
        require(allowanceFrom >= _value);
        _setAllowance(_from, msg.sender, allowanceFrom - _value);
        _transfer(_from, _to, _value);
        return true;
    }

    // ///
    // ERC20 Internal methods
    // ///

    function _transfer(address _from, address _to, uint256 _value) internal {
        uint256 balanceFrom = _balanceOf(_from);
        require(balanceFrom >= _value);
        _setBalance(_from, balanceFrom - _value);
        _setBalance(_to, _balanceOf(_to) + _value);
        emit Transfer(_from, _to, _value);
    }

    function _mint(address _to, uint256 _value) internal {
        emit Transfer(address(0), _to, _value);
        totalSupply = totalSupply +_value;
        _setBalance(_to, _balanceOf(_to + _value));
    }

    function _burn(address _from, uint256 _value) internal {
        uint256 balanceFrom = _balanceOf(msg.sender);
        require(balanceFrom >= _value);
        require(totalSupply >= _value);
        totalSupply = totalSupply - _value;
        _setBalance(_from, balanceFrom - _value);
        emit Transfer(_from, address(0), _value);
    }

    // ///
    // Read and write storage
    // ///

    function _balanceOf(address _addr) internal view returns (uint256) {
        return uint256(bytes32(_addr).read(keccak256("balance")));
    }

    function _allowance(address _addr, address _spender) internal view returns (uint256) {
        return uint256(bytes32(_addr).read(keccak256(abi.encodePacked("allowance", _spender))));
    }

    function _setBalance(address _addr, uint256 _balance) private {
        bytes32(_addr).write(
            keccak256("balance"),
            bytes32(_balance)
        );
    }

    function _setAllowance(address _addr, address _spender, uint256 _value) private {
        bytes32(_addr).write(
            keccak256(abi.encodePacked("allowance", _spender)),
            bytes32(_value)
        );
    }
}

Underflow and overflow checks must be added before using in production

Please note that the contracts above are an example only. It is not the standard.

Migration

The ERC20 to ERC20D migration should be performed in a progressive manner to avoid disruptions and loss of funds.

Migration interface

function origin() public view returns (address);
function migrate(uint256 _value) public;
function immigrate(uint256 _value) public;

Origin

Returns the address of the original ERC20 token.

function origin() public view returns (address);

Native migration methods

Total supply

At the beginning of the migration, the totalSupply of the origin ERC20 is minted to the address of the original ERC20 contract to maintain an equal total supply.

All subsequent migrations and immigrations are transferred from and to the origin ERC20 token.

    constructor(ERC20 _origin) public {
        origin = _origin;
        _mint(address(_origin), _origin.totalSupply());
    }

Migrate

It pulls ERC20 tokens and mints the same amount in ERC20D tokens, it should revert on failure.

    function _migrate(address _from, uint256 _value) internal {
        require(origin.transferFrom(_from, address(this), _value));
        _transfer(address(origin), _from, _value);
        emit Migrated(_from, _value);
    }

    function migrate(uint256 _value) external {
        _migrate(msg.sender, _value);
    }

Immigrate

It burs ERC20D and transfers back the original ERC20 tokens, it should revert on failure.

    function _immigrate(address _from, uint256 _value) internal {
        _transfer(_from, address(origin), _value);
        require(origin.transfer(_from, _value));
        emit Immigrated(_from, _value);
    }

    function immigrate(uint256 _value) external {
        _immigrate(msg.sender, _value);
    }

Live swap migration

The following would allow transferring ERC20D without having to migrate the ERC20 tokens previously.

If the user approved the ERC20D contract on the original ERC20 contract, his balance on the origin ERC20 contract could also be accessed with the transfer and transferFrom functions of ERC20D.

    function _transfer(address _from, address _to, uint256 _value) internal {
        uint256 balanceFrom = _from.balanceFrom(_from);

        // _from has no balance, try migrate
        if (balaceFrom < _value) {
            uint256 required = _value - balaceFrom;
            _migrate(_from, required);
            emit LiveSwapMigrated(msg.sender, _from, required);
        }

        super._transfer(_from, _to, _value);
    }

Events

Migrated

    event Migrated(address indexed _from, uint256 _value);

Triggered when tokens are migrated from ERC20 to ERC20D.

Immigrated

    event Migrated(address indexed _from, uint256 _value);

Triggered when tokens are migrated from ERC20D to ERC20.

LiveSwapMigrated

    event LiveSwapMigrated(address indexed _by, address indexed _from, uint256 _value);

Triggered when tokens are migrated by a LiveSwap.

Backwards compatibility

The ERC20D is fully compliant with the ERC20 specification and can be treated as any other implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant