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

ERC1538: Transparent Contract Standard #1538

Closed
mudgen opened this issue Oct 30, 2018 · 78 comments
Closed

ERC1538: Transparent Contract Standard #1538

mudgen opened this issue Oct 30, 2018 · 78 comments
Labels

Comments

@mudgen
Copy link
Contributor

mudgen commented Oct 30, 2018


eip: 1538
title: Transparent Contract Standard
author: Nick Mudge nick@perfectabstractions.com
status: Draft
type: Standards Track
category: ERC
created: 31 October 2018

None: An EIP has been written for this standard and is here: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1538.md

Simple Summary

This standard provides a contract architecture that makes upgradeable contracts flexible, unlimited in size, and transparent.

A transparent contract publicly documents the full history of all changes made to it.

All changes to a transparent contract are reported in a standard format.

Abstract

A transparent contract is a proxy contract design pattern that provides the following:

  1. A way to add, replace and remove multiple functions of a contract atomically (at the same time).
  2. Standard events to show what functions are added, replaced and removed from a contract, and why the changes are made.
  3. A standard way to query a contract to discover and retrieve information about all functions exposed by it.
  4. Solves the 24KB maximum contract size limitation, making the maximum contract size of a transparent contract practically unlimited. This standard makes the worry about contract size a thing of the past.
  5. Enables an upgradeable contract to become immutable in the future if desired.

Motivation

A fundamental benefit of Ethereum contracts is that their code is immutable, thereby acquiring trust by trustlessness. People do not have to trust others if it is not possible for a contract to be changed.

However, a fundamental problem with trustless contracts that cannot be changed is that they cannot be changed.

Bugs

Bugs and security vulnerabilities are unwittingly written into immutable contracts that ruin them.

Improvements

Immutable, trustless contracts cannot be improved, resulting in increasingly inferior contracts over time.

Contract standards evolve, new ones come out. People, groups and organizations learn over time what people want and what is better and what should be built next. Contracts that cannot be improved not only hold back the authors that create them, but everybody who uses them.

Upgradeable Contracts vs. Centralized Private Database

Why have an upgradeable contract instead of a centralized, private, mutable database?
Here are some reasons:

  1. Because of the openness of storage data and verified code, it is possible to show a provable history of trustworthiness.
  2. Because of the openness, bad behavior can be spotted and reported when it happens.
  3. Independent security and domain experts can review the change history of contracts and vouch for their history of trustworthiness.
  4. It is possible for an upgradeable contract to become immutable and trustless.
  5. An upgradeable contract can have parts of it that are not upgradeable and so are partially immutable and trustless.

Immutability

In some cases immutable, trustless contracts are the right fit. This is the case when a contract is only needed for a short time or it is known ahead of time that there will never be any reason to change or improve it.

Middle Ground

Transparent contracts provide a middle ground between immutable trustless contracts that can't be improved and upgradeable contracts that can't be trusted.

Purposes

  1. Create upgradeable contracts that earn trust by showing a provable history of trustworthiness.
  2. Document the development of contracts so their development and change is provably public and can be understood.
  3. Create upgradeable contracts that can become immutable in the future if desired.
  4. Create contracts that are not limited by a max size.

Benefits & Use Cases

This standard is for use cases that benefit from the following:

  1. The ability to add, replace or remove multiple functions of a contract atomically (at the same time).
  2. Each time a function is added, replaced or removed, it is documented with events.
  3. Build trust over time by showing all changes made to a contract.
  4. Unlimited contract size.
  5. The ability to query information about functions currently supported by the contract.
  6. One contract address that provides all needed functionality and never needs to be replaced by another contract address.
  7. The ability for a contract to be upgradeable for a time, and then become immutable.
  8. Add trustless guarantees to a contract with "unchangeable functions".

New Software Possibilities

This standard enables a form of contract version control software to be written.

Software and user interfaces can be written to filter the FunctionUpdate and CommitMessage events of a contract address. Such software can show the full history of changes of any contract that implements this standard.

User interfaces and software can also use this standard to assist or automate changes of contracts.

Specification

Note:
The solidity delegatecall opcode enables a contract to execute a function from another contract, but it is executed as if the function was from the calling contract. Essentially delegatecall enables a contract to "borrow" another contract's function. Functions executed with delegatecall affect the storage variables of the calling contract, not the contract where the functions are defined.

General Summary

A transparent contract delegates or forwards function calls to it to other contracts using delegatecode.

A transparent contract has an updateContract function that enables multiple functions to be added, replaced or removed.

An event is emitted for every function that is added, replaced or removed so that all changes to a contract can be tracked in a standard way.

A transparent contract is a contract that implements and complies with the design points below.

Terms

  1. In this standard a delegate contract is a contract that a transparent contract fallback function forwards function calls to using delegatecall.
  2. In this standard an unchangeable function is a function that is defined directly in a transparent contract and so cannot be replaced or removed.

Design Points

A contract is a transparent contract if it implements the following design points:

  1. A transparent contract is a contract that contains a fallback function, a constructor, and zero or more unchangeable functions that are defined directly within it.
  2. The constructor of a transparent contract associates the updateContract function with a contract that implements the ERC1538 interface. The updateContract function can be an "unchangeable function" that is defined directly in the transparent contract or it can be defined in a delegate contract. Other functions can also be associated with contracts in the constructor.
  3. After a transparent contract is deployed functions are added, replaced and removed by calling the updateContract function.
  4. The updateContract function associates functions with contracts that implement those functions, and emits the CommitMessage and FunctionUpdate events that document function changes.
  5. The FunctionUpdate event is emitted for each function that is added, replaced or removed. The CommitMessage event is emitted one time for each time the updateContract function is called and is emitted after any FunctionUpdate events are emitted.
  6. The updateContract function can take a list of multiple function signatures in its _functionSignatures parameter and so add/replace/remove multiple functions at the same time.
  7. When a function is called on a transparent contract it executes immediately if it is an "unchangeable function". Otherwise the fallback function is executed. The fallback function finds the delegate contract associated with the function and executes the function using delegatecall. If there is no delegate contract for the function then execution reverts.
  8. The source code of a transparent contract and all delegate contracts used by it are publicly viewable and verified.

The transparent contract address is the address that users interact with. The transparent contract address never changes. Only delegate addresses can change by using the updateContracts function.

Typically some kind of authentication is needed for adding/replacing/removing functions from a transparent contract, however the scheme for authentication or ownership is not part of this standard.

Example

Here is an example of an implementation of a transparent contract. Please note that the example below is an example only. It is not the standard. A contract is a transparent contract when it implements and complies with the design points listed above.

pragma solidity ^0.5.7;

contract ExampleTransparentContract {
  // owner of the contract
  address internal contractOwner;
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  // maps functions to the delegate contracts that execute the functions
  // funcId => delegate contract
  mapping(bytes4 => address) internal delegates;

  // maps each function signature to its position in the funcSignatures array.
  // signature => index+1
  mapping(bytes => uint256) internal funcSignatureToIndex;
    
  event CommitMessage(string message);
  event FunctionUpdate(bytes4 indexed functionId, address indexed oldDelegate, address indexed newDelegate, string functionSignature);
  
  // this is an example of an "unchangeable function".
  // return the delegate contract address for the supplied function signature
  function delegateAddress(string calldata _functionSignature) external view returns(address) {
    require(funcSignatureToIndex[bytes(_functionSignature)] != 0, "Function signature not found.");
    return delegates[bytes4(keccak256(bytes(_functionSignature)))];
  }
  
  // add a function using the updateContract function
  // this is an internal helper function
  function addFunction(address _erc1538Delegate, address contractAddress, string memory _functionSignatures, string memory _commitMessage) internal {    
    // 0x03A9BCCF == bytes4(keccak256("updateContract(address,string,string)"))
    bytes memory funcdata = abi.encodeWithSelector(0x03A9BCCF, contractAddress, _functionSignatures, _commitMessage);
    bool success;
    assembly {
      success := delegatecall(gas, _erc1538Delegate, add(funcdata, 0x20), mload(funcdata), funcdata, 0)
    }
    require(success, "Adding a function failed");   
  }

  constructor(address _erc1538Delegate) public {
    contractOwner = msg.sender;
    emit OwnershipTransferred(address(0), msg.sender);

    // adding ERC1538 updateContract function
    bytes memory signature = "updateContract(address,string,string)";
    bytes4 funcId = bytes4(keccak256(signature));
    delegates[funcId] = _erc1538Delegate;
    emit FunctionUpdate(funcId, address(0), _erc1538Delegate, string(signature));
    emit CommitMessage("Added ERC1538 updateContract function at contract creation");
	
    // associate "unchangeable functions" with this transparent contract address
    // prevents function selector clashes with delegate contract functions
    // uses the updateContract function
    string memory functions = "delegateAddress(string)";
    addFunction(_erc1538Delegate, address(this), functions, "Associating unchangeable functions");
	
    // adding ERC1538Query interface functions
    functions = "functionByIndex(uint256)functionExists(string)delegateAddresses()delegateFunctionSignatures(address)functionById(bytes4)functionBySignature(string)functionSignatures()totalFunctions()";    
    // "0x01234567891011121314" is an example address of an ERC1538Query delegate contract
    addFunction(_erc1538Delegate, 0x01234567891011121314, functions, "Adding ERC1538Query functions");
    
    // additional functions could be added at this point
  }

  // Making the fallback function payable makes it work for delegate contract functions 
  // that are payable and not payable.
  function() external payable {
    // Delegate every function call to a delegate contract
    address delegate = delegates[msg.sig];
    require(delegate != address(0), "Function does not exist.");
    assembly {
      let ptr := mload(0x40)
      calldatacopy(ptr, 0, calldatasize)
      let result := delegatecall(gas, delegate, ptr, calldatasize, 0, 0)
      let size := returndatasize
      returndatacopy(ptr, 0, size)
      switch result
      case 0 {revert(ptr, size)}
      default {return (ptr, size)}
    }
  }
}

As can be seen in the above example, every function call is delegated to a delegate contract, unless the function is defined directly in the transparent contract (making it an unchangeable function).

The constructor function adds the updateContract function to the transparent contract, which is then used to add other functions to the transparent contract.

Each time a function is added to a transparent contract the events CommitMessage and FunctionUpdate are emitted to document exactly what functions where added or replaced and why.

The delegate contract that implements the updateContract function implements the following interface:

ERC1538 Interface

pragma solidity ^0.5.7;

/// @title ERC1538 Transparent Contract Standard
/// @dev Required interface
///  Note: the ERC-165 identifier for this interface is 0x61455567
interface ERC1538 {
  /// @dev This emits when one or a set of functions are updated in a transparent contract.
  ///  The message string should give a short description of the change and why
  ///  the change was made.
  event CommitMessage(string message);
  
  /// @dev This emits for each function that is updated in a transparent contract.
  ///  functionId is the bytes4 of the keccak256 of the function signature.
  ///  oldDelegate is the delegate contract address of the old delegate contract if
  ///  the function is being replaced or removed.
  ///  oldDelegate is the zero value address(0) if a function is being added for the
  ///  first time.
  ///  newDelegate is the delegate contract address of the new delegate contract if 
  ///  the function is being added for the first time or if the function is being 
  ///  replaced.
  ///  newDelegate is the zero value address(0) if the function is being removed.
  event FunctionUpdate(
    bytes4 indexed functionId, 
    address indexed oldDelegate, 
    address indexed newDelegate, 
    string functionSignature
  );

  /// @notice Updates functions in a transparent contract.
  /// @dev If the value of _delegate is zero then the functions specified 
  ///  in _functionSignatures are removed.
  ///  If the value of _delegate is a delegate contract address then the functions 
  ///  specified in _functionSignatures will be delegated to that address.
  /// @param _delegate The address of a delegate contract to delegate to or zero
  ///        to remove functions.      
  /// @param _functionSignatures A list of function signatures listed one after the other
  /// @param _commitMessage A short description of the change and why it is made
  ///        This message is passed to the CommitMessage event.          
  function updateContract(address _delegate, string calldata _functionSignatures, string calldata _commitMessage) external;  
}

Function Signatures String Format

The text format for the _functionSignatures parameter is simply a string of function signatures. For example: "myFirstFunction()mySecondFunction(string)" This format is easy to parse and is concise.

Here is an example of calling the updateContract function that adds the ERC721 standard functions to a transparent contract:

functionSignatures = "approve(address,uint256)balanceOf(address)getApproved(uint256)isApprovedForAll(address,address)ownerOf(uint256)safeTransferFrom(address,address,uint256)safeTransferFrom(address,address,uint256,bytes)setApprovalForAll(address,bool)transferFrom(address,address,uint256)"
tx = await transparentContract.updateContract(erc721Delegate.address, functionSignatures, "Adding ERC721 functions");

Removing Functions

Functions are removed by passing address(0) as the first argument to the updateContract function. The list of functions that are passed in are removed.

Source Code Verification

The transparent contract source code and the source code for the delegate contracts should be verified in a provable way by a third party source such as etherscan.io.

Function Selector Clash

A function selector clash occurs when a function is added to a contract that hashes to the same four-byte hash as an existing function. This is unlikely to occur but should be prevented in the implementation of the updateContract function. See the reference implementation of ERC1538 to see an example of how function clashes can be prevented.

ERC1538Query

Optionally, the function signatures of a transparent contract can be stored in an array in the transparent contract and queried to get what functions the transparent contract supports and what their delegate contract addresses are.

The following is an optional interface for querying function information from a transparent contract:

pragma solidity ^0.5.7;

interface ERC1538Query {
    
  /// @notice Gets the total number of functions the transparent contract has.
  /// @return The number of functions the transparent contract has,
  ///  not including the fallback function.
  function totalFunctions() external view returns(uint256);
	
  /// @notice Gets information about a specific function
  /// @dev Throws if `_index` >= `totalFunctions()`
  /// @param _index The index position of a function signature that is stored in an array
  /// @return The function signature, the function selector and the delegate contract address
  function functionByIndex(uint256 _index) 
    external 
    view 
    returns(
      string memory functionSignature, 
      bytes4 functionId, 
      address delegate
    );
	
  /// @notice Checks to see if a function exists
  /// @param The function signature to check
  /// @return True if the function exists, false otherwise
  function functionExists(string calldata _functionSignature) external view returns(bool);
	
  /// @notice Gets all the function signatures of functions supported by the transparent contract
  /// @return A string containing a list of function signatures
  function functionSignatures() external view returns(string memory);
	
  /// @notice Gets all the function signatures supported by a specific delegate contract
  /// @param _delegate The delegate contract address
  /// @return A string containing a list of function signatures
  function delegateFunctionSignatures(address _delegate) external view returns(string memory);
	
  /// @notice Gets the delegate contract address that supports the given function signature
  /// @param The function signature
  /// @return The delegate contract address
  function delegateAddress(string calldata _functionSignature) external view returns(address);
	
  /// @notice Gets information about a function
  /// @dev Throws if no function is found
  /// @param _functionId The id of the function to get information about
  /// @return The function signature and the contract address
  function functionById(bytes4 _functionId) 
    external 
    view 
    returns(
      string memory signature, 
      address delegate
    );
	
  /// @notice Get all the delegate contract addresses used by the transparent contract
  /// @return An array of all delegate contract addresses
  function delegateAddresses() external view returns(address[] memory);
}

See the reference implementation of ERC1538 to see how this is implemented.

The text format for the list of function signatures returned from the delegateFunctionSignatures and functionSignatures functions is simply a string of function signatures. Here is an example of such a string: "approve(address,uint256)balanceOf(address)getApproved(uint256)isApprovedForAll(address,address)ownerOf(uint256)safeTransferFrom(address,address,uint256)safeTransferFrom(address,address,uint256,bytes)setApprovalForAll(address,bool)transferFrom(address,address,uint256)"

How To Deploy A Transparent Contract

  1. Create and deploy to a blockchain a contract that implements the ERC1538 interface. You can skip this step if there is already such a contract deployed to the blockchain.
  2. Create your transparent contract with a fallback function as given above. Your transparent contract also needs a constructor that adds the updateContract function.
  3. Deploy your transparent contract to a blockchain. Pass in the address of the ERC1538 delegate contract to your constructor if it requires it.

See the reference implementation for examples of these contracts.

Wrapper Contract for Delegate Contracts that Depend on Other Delegate Contracts

In some cases some delegate contracts may need to call external/public functions that reside in other delegate contracts. A convenient way to solve this problem is to create a contract that contains empty implementations of functions that are needed and import and extend this contract in delegate contracts that call functions from other delegate contracts. This enables delegate contracts to compile without having to provide implementations of the functions that are already given in other delegate contracts. This is a way to save gas, prevent reaching the max contract size limit, and prevent duplication of code. This strategy was given by @amiromayer. See his comment for more information. Another way to solve this problem is to use assembly to call functions provided by other delegate contracts.

Decentralized Authority

It is possible to extend this standard to add consensus functionality such as an approval function that multiple different people call to approve changes before they are submitted with the updateContract function. Changes only go into effect when the changes are fully approved. The CommitMessage and FunctionUpdate events should only be emitted when changes go into effect.

Security

This standard refers to owner(s) as one or more individuals that have the power to add/replace/remove functions of an upgradeable contract.

General

The owners(s) of an upgradeable contract have the ability to alter, add or remove data from the contract's data storage. Owner(s) of a contract can also execute any arbitrary code in the contract on behalf of any address. Owners(s) can do these things by adding a function to the contract that they call to execute arbitrary code. This is an issue for upgradeable contracts in general and is not specific to transparent contracts.

Note: The design and implementation of contract ownership is not part of this standard. The examples given in this standard and in the reference implementation are just examples of how it could be done.

Unchangeable Functions

"Unchangeable functions" are functions defined in a transparent contract itself and not in a delegate contract. The owner(s) of a transparent contract are not able to replace these functions. The use of unchangeable functions is limited because in some cases they can still be manipulated if they read or write data to the storage of the transparent contract. Data read from the transparent contract's storage could have been altered by the owner(s) of the contract. Data written to the transparent contract's storage can be undone or altered by the owner(s) of the contract.

In some cases unchangeble functions add trustless guarantees to a transparent contract.

Transparency

Contracts that implement this standard emit an event every time a function is added, replaced or removed. This enables people and software to monitor the changes to a contract. If any bad acting function is added to a contract then it can be seen. To comply with this standard all source code of a transparent contract and delegate contracts must be publicly available and verified.

Security and domain experts can review the history of change of any transparent contract to detect any history of foul play.

Rationale

String of Function Signatures Instead of bytes4[] Array of Function Selectors

The updateContract function takes a string list of functions signatures as an argument instead of a bytes4[] array of function selectors for three reasons:

  1. Passing in function signatures enables the implementation of updateContract to prevent selector clashes.
  2. A major part of this standard is to make upgradeable contracts more transparent by making it easier to see what has changed over time and why. When a function is added, replaced or removed its function signature is included in the FunctionUpdate event that is emitted. This makes it relatively easy to write software that filters the events of a contract to display to people what functions have been added/removed and changed over time without needing access to the source code or ABI of the contract. If only four-byte function selectors were provided this would not be possible.
  3. By looking at the source code of a transparent contract it is not possible to see all the functions that it supports. This is why the ERC1538Query interface exists, so that people and software have a way to look up and examine or show all functions currently supported by a transparent contract. Function signatures are used so that ERC1538Query functions can show them.

Gas Considerations

Delegating function calls does have some gas overhead. This is mitigated in two ways:

  1. Delegate contracts can be small, reducing gas costs. Because it costs more gas to call a function in a contract with many functions than a contract with few functions.
  2. Because transparent contracts do not have a max size limitation it is possible to add gas optimizing functions for use cases. For example someone could use a transparent contract to implement the ERC721 standard and implement batch transfer functions from the ERC1412 standard to help reduce gas (and make batch transfers more convenient).

Storage

The standard does not specify how data is stored or organized by a transparent contract. But here are some suggestions:

Inherited Storage

  1. The storage variables of a transparent contract consist of the storage variables defined in the transparent contract source code and the source code of delegate contracts that have been added.

  2. A delegate contract can use any storage variable that exists in a transparent contract as long as it defines within it all the storage variables that exist, in the order that they exist, up to and including the ones being used.

  3. A delegate contract can create new storage variables as long as it has defined, in the same order, all storage variables that exist in the transparent contract.

Here is a simple way inherited storage could be implemented:

  1. Create a storage contract that contains the storage variables that your transparent contract and delegate contracts will use.
  2. Make your delegate contracts inherit the storage contract.
  3. If you want to add a new delegate contract that adds new storage variables then create a new storage contract that adds the new storage variables and inherits from the old storage contract. Use your new storage contract with your new delegate contract.
  4. Repeat steps 2 or 3 for every new delegate contract.

Unstructured Storage

Assembly is used to store and read data at specific storage locations. An advantage to this approach is that previously used storage locations don't have to be defined or mentioned in a delegate contract if they aren't used by it.

Eternal Storage

Data can be stored using a generic API based on the type of data. See ERC930 for more information.

Becoming Immutable

It is possible to make a transparent contract become immutable. This is done by calling the updateContract function to remove the updateContract function. With this gone it is no longer possible to add, replace and remove functions.

Versions of Functions

Software or a user can verify what version of a function is called by getting the delegate contract address of the function. This can be done by calling the delegateAddress function from the ERC1538Query interface if it is implemented. This function takes a function signature as an argument and returns the delegate contract address where it is implemented.

Best Practices, Tools and More Information

More information, tools, tutorials and best practices concerning transparent contracts need to be developed and published.

Below is a growing list of articles concerning transparent contracts and their use. If you have an article about transparent contracts you would like to share then please submit a comment to this issue about it to get it added.

ERC1538: Future Proofing Smart Contracts and Tokens

The ERC1538 improving towards the “transparent contract” standard

Inspiration

This standard was inspired by ZeppelinOS's implementation of Upgradeability with vtables.

This standard was also inspired by the design and implementation of the Mokens contract from the Mokens project. The Mokens contract has been upgraded to implement this standard.

Backwards Compatibility

This standard makes a contract compatible with future standards and functionality because new functions can be added and existing functions can be replaced or removed.

This standard future proofs a contract.

Implementation

A reference implementation of this standard is given in the transparent-contracts-erc1538 repository.

Copyright

Copyright and related rights waived via CC0.

@mudgen mudgen changed the title ERC1538 - Transparent Contract Standard ERC1538: Transparent Contract Standard Oct 30, 2018
@mattlockyer
Copy link

First Questions:

  1. what else is out there?
  2. how come the only delegate is payable? What about non-payable? or did I miss something?
  3. any security issues?
  4. how come you don't inherit Ownable.sol, is this because the owner of root will never change?

@mudgen
Copy link
Contributor Author

mudgen commented Nov 1, 2018

Great questions. I'll answer the last three questions first:

First Questions:

  1. what else is out there?
  2. how come the only delegate is payable? What about non-payable? or did I miss something?
  3. any security issues?
  4. how come you don't inherit Ownable.sol, is this because the owner of root will never change?
  1. The fallback function delegates every function call to a delegate contract. The fallback function is payable because some of the functions in a delegate contract may be payable. Making the fallback function payable makes it work for delegate contract functions that are payable and not payable.

  2. See the newly added Security section in the standard.

  3. A transparent contract could inherit Ownable.sol if someone wants the owner() function to be unchangeable. The above implementation is just an example of implementing ERC1538.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 1, 2018

First Questions:

  1. what else is out there?

There are various ideas about different designs and implementations of upgradeable contracts. These are found on the web.

I didn't find any that provide the same list of benefits that this standard provides. The closest I found was zepplin's vtable implementation. ERC1538 was inspired by the vtable implementation, specifically from the idea of managing an upgradeable contract by function, instead of by contract address.

@AC0DEM0NK3Y
Copy link
Contributor

AC0DEM0NK3Y commented Nov 2, 2018

Looks pretty good on the face of it. I'll have to go deeper and think any caveats when I get a little more time to jump in but yeah, nice format to allow flexibility while making things have some standard visibility on change history/reason.

I think there could be some improvements or cuts to ERC1538Query perhaps, quick example being in what format should functionSignatures() return, comma/semi-colon delimited for eg. but that is minor detail.

Question on this: "Delegate contracts can be small, reducing gas costs. Because it costs more gas to call a function in a contract with many functions than a contract with few functions."

Do you have numbers or have links to numbers?

edit: One thing you should perhaps think about adding/talking about is this is not something that should be done by default, and if it is used then people should be ideally putting in capability to pull authority out on being able to change the functions and where they delegate to.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 2, 2018

@AC0DEM0NK3Y thanks for the great feedback!

When a function is called on a contract there is a linear search to find the function in the contract. So every function costs a different amount of gas to be called. I did a simple test with a simple function. I made a contract with one function. The gas cost to execute it was 384. I made a second contract with the same exact function 26 times but with slightly different function names. The most expensive function to call was 934.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 2, 2018

@AC0DEM0NK3Y Yes, people can call the updateContract function to remove the updateContract function. That would remove the ability to change the contract, making it immutable. I added that in.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 2, 2018

From solidity documentation:

The low-level call, delegatecall and callcode will return success if the called account is non-existent, as part of the design of EVM. Existence must be checked prior to calling if desired.

This is not a security vulnerability in a transparent contract because the code above does check that a contract address exists before making a delegate call. It is possible for a user to submit an invalid address to the updateContract function, but that function could also check for the existence of code for the submitted address, and throw if the address is not a contract address.

@mwherman2000
Copy link

Why not simply create a generic worklow engine SC that understands BPMN workflow templates that have been transcompiled into a BPMN specific byecode?

@mudgen
Copy link
Contributor Author

mudgen commented Nov 5, 2018

@mwherman2000 Sorry, I am not familiar with how that works so I can't answer that question.

@theblockstalk
Copy link

I would suggest that you get the Aaragon and zepplinOS team involved in this EIP, they are leading upgradeable contract researchers.

There are in fact multiple ways to implement upgradeable contracts using delegate call and the would not all be compatible.

  • Some include a method for initialization which is not considered in this EIP.
  • Some of them implement an owner contract in different ways
  • Some of them delegate calls to one contract, instead of different contracts for each function

I would closely check that this is in line with the zepplinOS unstructured storage pattern, as I think this will be the most widely adopted.

@satyamakgec
Copy link

@mudgen It is a very nice approach to make the size of the contract unlimited. My worry point is how you can make communication between two different implementation contracts. AFAICT It only works when we have independent functions that don't depend on the other functions or internal functions.

Ex - some X function is used to change the state that used by other functions and it is implemented in the contract1 and then some Y function is implemented in the contract2 and want to use the X function the how do contract1 and contract2 function will communicate (communication between two implementation contract).

@bitcoinwarrior1
Copy link
Contributor

@mudgen this seems like something quite handy, however it seems that a big bottleneck is the admin key which could be compromised to lead to a dodgy contract or simply lost. Seems you would need a sort of consortium to manage it...

@adibas03
Copy link

adibas03 commented Nov 6, 2018

@mudgen One thing I think is missing is a default delegate address, so in a case where the first deployment has all the contracts in one address, this becomes important, this can always be removed by upgrading a function to address(0) if the function is no longer supported. In a scenario where there is no default delegate, it is simply set as address(0).
@James-Sangalli Adding a consortium needs not be part of the standard, as that is no longer miimum Viable. As for the issue with the ownership, the delegate of the contractOwner() can be set to zero to return null for the contractOwner and remove the compromise issue, or in another option is to make the contract non-upgradable if that is the case just as @AC0DEM0NK3Y mentioned

@mudgen
Copy link
Contributor Author

mudgen commented Nov 6, 2018

@jackandtheblockstalk Thanks for your feedback. I will check with the Aaragon and zepplinOS teams.

To be clear, multiple functions from the same delegate contract can be added to a transparent contract at the same time with the updateContracts function. Some people have the idea that each function that is added has to be from a different delegate contract.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 6, 2018

@satyamakgec communication between functions from different contracts is not a problem.

If contract2 needs to use a function that is defined in contract1 then the solution is to add the same function, with the same definition, to contract2. Or if contract2 is already deployed then create a contract3 contract that has all the needed functionality, deploy it, and use that contract to add the functions to the transparent contract.

The problem can also be avoided by putting all the related functionality into one contract and then adding all the external/public functions to the transparent contract with one call to updateContract.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 6, 2018

@James-Sangalli yes, I think it is possible to build various kinds of systems on top of a transparent contract, such as governance, decentralized schemes, authentication schemes etc.

I modified the proposal, saying that the scheme for ownership or authentication is not part of the standard.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 6, 2018

@adibas03 I see that a default delegate address could be convenient. Would it be necessary?

The initial functions of a transparent contract could be added to it in the transparent contract's constructor. Various kinds of initialization of a transparent contract could be done in the constructor. Here is an example that adds initial functions to a transparent contract in the constructor.

 constructor(address _erc1538Delegate) public {
    contractOwner = msg.sender;
    emit OwnershipTransferred(address(0), msg.sender);

    // adding ERC1538 updateContract function
    bytes memory signature = "updateContract(address,string,string)";
    bytes4 funcId = bytes4(keccak256(signature));
    delegates[funcId] = _erc1538Delegate;
    emit FunctionUpdate(funcId, address(0), _erc1538Delegate, string(signature));
    emit CommitMessage("Added ERC1538 updateContract function at contract creation");
  
    // add initial functions
    // uses the updateContract function to add initial functions
    bytes memory initialFunctions = "myFirstFunction()mySecondFunction(string)";
    address initialDelegate = 0x343434353222222;
    bytes memory calldata = abi.encodeWithSelector(signature, initialDelegate, initialFunctions, "Adding initial functions");
    bool success;
    assembly {
      success := delegatecall(gas, _erc1538Delegate, add(calldata, 0x20), mload(calldata), calldata, 0)
    }
    require(success, "Adding initial functions failed.");   
  }

@adibas03 I agree with you that a consortium need not be part of the standard.

@Droopy78
Copy link

Droopy78 commented Nov 7, 2018

@mudgen This is great. There is no chance of selector clashing exploit with this implementation.

As far as storage goes, the reference implementation is the way to go as best practice (i.e. inherited storage). All delegates will probably also want to inherit from the ERC1538Delegate reference implementation for updateContract().

I'm trying to remember... assume a transparent contract's storage variables are all in the inherited base contract, and delegate contract 1 and delegate contract 2 also inherit these variables from the same base contract. Can delegate contract 1 and 2 then define their own separate storage vars on top of that or do they also need to line up in memory? I was assuming the former, as that is how it is worded in the description (also, it would be a pretty clear, hassle-free implementation at that point).

@mudgen
Copy link
Contributor Author

mudgen commented Nov 7, 2018

@Droopy78 Yes! The lack of functions directly in a transparent contract completely avoids selector clashing and the complexity of dealing with that issue.

Yes, the variable storage of contract1 and contract2 need to line up. I found this sort of thing easiest to manage by creating a new storage contract for each new delegate contract that inherits the storage class from the previous delegate contract. This strategy keeps the storage variables lined up without duplicate code and without the chance of creating bugs.

Here's some examples of storage scenarios:

Let's say that contract1 was created and added to a transparent contract. Now you want to add contract2 to the transparent contract.

  1. If contract2 does not use variables defined by contract1 (or later contracts) then it does not need to define variables created in contract1.
  2. If contract2 uses variables defined in contract1 then contract2 also needs to define those variables.
  3. If contract2 defines new variables then it also needs to define all the variables defined by contract1.

I am also interested in unstructured storage. The advantage of this approach is that later contracts can create new storage locations without having to define previous ones that it doesn't use.

@adibas03
Copy link

adibas03 commented Nov 7, 2018

@mudgen You are right, it seems not necessary, but it might help to have it as part of the standard.
Another use case, might be if a another proxy or Identity contract is used to handle the call, and therefore should handle all calls by default.

@wighawag
Copy link
Contributor

wighawag commented Nov 8, 2018

That's a cool pattern.

One thing to add would be to add a security mechanism that invalidate transaction if the contract changed while the transaction was in transit, or simply if the user was not yet aware of the changes.

Whenever the contract change, a version number is generated and that version is always used as part of the transaction data.

Implementation could then check the version number matches the current contract version. If it matches, the transaction is allowed. If not it is rejected since the sender was not aware of the changes at that point.

This would prevent frontrunning attack where the owner of the contract could change the contract logic just in time to compromise a wealthy sender.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 8, 2018

@wighawag Yes, you bring up a good point here. Thanks for this insight. It would definitely be good to prevent frontrunning attacks.

@mudgen
Copy link
Contributor Author

mudgen commented Nov 8, 2018

@wighawag One way to handle frontrunning attacks is to verify the expected state after the call to the function you care about in the same transaction. If the state is not as expected then revert. This is how Project Wyvern handles frontrunning. Project Wyvern or its protocol handles the exchange contracts for OpenSea.io and other exchanges.

So here is an example of this. Let's say that ContractA is a transparent contract and you want to call function myFunction(). So you create ContractB with a function verifyMyFunction(). The verifyMyFunction() function calls myFunction() in ContractA and then it checks that state has been changed correctly and throws if not.

Handling frontrunning like this works. No change to ERC1538 is needed to implement this.

@adibas03
Copy link

adibas03 commented Nov 8, 2018

Another implementation of this is openZeppelin's re-entrancy guard.
Though it is still a little tricky how to implement in this.

@mudgen
Copy link
Contributor Author

mudgen commented Mar 4, 2019

It would really help the standard is people wrote some blog posts or made some videos about it to help educate more more about it, because I think it is a great and very useful standard.

@fulldecent
Copy link
Contributor

Hello! Will you seek to publish this as draft document?

Right now you have a reference from ERC-1155 which is in Last Call status. If possible I would like that document to link to the published (DRAFT) version of this document rather than this issue.

@mudgen
Copy link
Contributor Author

mudgen commented Mar 26, 2019

@fulldecent Yes, I will create a draft later today.

@spalladino
Copy link
Contributor

Before going into a draft, can I make one last push to suggest a rename? We have been using the "transparent proxy" name for quite some time in ZeppelinOS for something entirely different, and I'd like to avoid clashes. Thanks!

@mudgen
Copy link
Contributor Author

mudgen commented Mar 26, 2019

@spalladino @fulldecent I'll postpone making a draft for a day to give changing the name some thought.

Here are some immediate ideas for a different name:

  1. Open Contract Standard
  2. Open Source Contract Standard
  3. Open Proxy Contract Standard
  4. Unlimited Proxy Contract Standard
  5. Unlimited & Open Proxy Contract Standard
  6. Unlimited Open Proxy Contract Standard
  7. Unlimited and Documented Proxy Contract Standard
  8. Open Proxy Unlimited Standard

Ideas? Likes?

@markusj1201
Copy link

markusj1201 commented Mar 26, 2019 via email

@Droopy78
Copy link

By "Unlimited" do you mean unlimited size? Might be nice to also have such words as "upgradeable functions" since that's at the heart of what you can do with this (besides make unlimited size contracts). Though I know you want to convey that the functions can also be locked down and made immutable again, so it's a flexible standard.

What about something like "Flexible & Upgradable Proxy Contract Standard" or "Upgradable Functions & Unlimited Size Contract Standard"

@mudgen
Copy link
Contributor Author

mudgen commented Mar 26, 2019

@Droopy78 yes, I am liking your ideas.

@nventuro
Copy link
Contributor

I'd personally try to keep the name short, rather than having it be fifteen words long and noting every single available feature. 'Upgradeable functions' has a nice ring to it, and is precisely what this does (as opposed to e.g. upgrading the whole contract at once).

@mudgen
Copy link
Contributor Author

mudgen commented Mar 26, 2019

@nventuro yes, good point.

@mudgen
Copy link
Contributor Author

mudgen commented Mar 26, 2019

How about the Proxy Functions Standard ?

@nventuro
Copy link
Contributor

That could also work, but you then need the reader to understand what a proxy is. Everyone gets 'upgradeable'.

@mudgen
Copy link
Contributor Author

mudgen commented Mar 27, 2019

Okay, I didn't find a suitable, agreeable, alternative name so I'm keeping the name as is.

Thank you for everyone's input.

@mudgen
Copy link
Contributor Author

mudgen commented Mar 27, 2019

@fulldecent I just made a pull request to add draft EIP 1538.

@spalladino
Copy link
Contributor

Why not "function-upgradeable proxy", a twist on the original suggestion by @nventuro?

@AC0DEM0NK3Y
Copy link
Contributor

I was just knocking up an example using this standard and some of the ref code and was considering does the "function updateContract" actually need to be part of the standard?

It seems like an implementation could update the signatures (and any other data) however they wish, but must obey the requirement of outputting the FunctionUpdate and CommitMessage events as necessary.
I understand part of the transparency should be that people can watch that particular signature being called to update but there's nothing really preventing an impl from walking around that anyway.

For my example code for instance, it would be convenient/simpler to update the delegate but also pass in extra info at the same time.

@mudgen
Copy link
Contributor Author

mudgen commented Apr 5, 2019

I was just knocking up an example using this standard and some of the ref code and was considering does the "function updateContract" actually need to be part of the standard?

It seems like an implementation could update the signatures (and any other data) however they wish, but must obey the requirement of outputting the FunctionUpdate and CommitMessage events as necessary.
I understand part of the transparency should be that people can watch that particular signature being called to update but there's nothing really preventing an impl from walking around that anyway.

For my example code for instance, it would be convenient/simpler to update the delegate but also pass in extra info at the same time.

@AC0DEM0NK3Y You have an excellent point about this. I will think about this some. I am interested in what others think about this point as well.

@mudgen
Copy link
Contributor Author

mudgen commented Apr 24, 2019

@AC0DEM0NK3Y I considered more about the point that you brought up. The updateContract function is useful in the future when there is user interface software that helps people write contracts or modify contracts. Such software can be used with any contract that has the updateContract function.

That being said, the ERC1538 standard does not say you can't have other functions that also add/remove/replace functions and it does not violate the standard to add those. So in your use case if that will help your contract then do it.

@AC0DEM0NK3Y
Copy link
Contributor

AC0DEM0NK3Y commented Apr 25, 2019

If you force an API with the updateContract function being standardized, doing impl specific side updates may be infeasible as you can't rely on logic in those side functions anymore always being called, given there will always be the standard path that has to be catered for.
I ended up using the standardized version in my case for example because of that reason.

So its a trade-off, do you sacrifice flexibility in implementation now in order for the possible future software having a rigid api to call into?
Do you have an example/scenario in mind for that future software? Would be easier to weigh up the options with that sort of projection.

edit: Maybe one option would be to add a bytes32 "data" parameter so extra impl specific info can still be passed.

@Amxx
Copy link
Contributor

Amxx commented Aug 5, 2019

I don't know if that has been addressed before, but I had to modify my implementation (which was initially based on the example implementation) to support delegation of messages without a signature. This is necessary when the delegate contains a fallback.

My implementation is available here

More details:
I want to forward calls with msg.sig = bytes4(0). Calls are forwarded to m_funcDelegates[msg.sig], which is set by the _setFunc method. Generally, _setFunc reconstructs the function signature from its prototype (bytes4 funcId = bytes4(keccak256(funcSignature));. Finding a prototype who's signature is 0 is not THAT difficult, but is unnecessarily complex. My solution is to check if the signature is fallback and override funcIdin that case.
I'm able to consider a signature without parenthesis because the separator is ; in my case (parenthesis are complex to count when dealing with ABIEncoderV2 so I included a dedicated separator months ago)

@mudgen
Copy link
Contributor Author

mudgen commented Jan 31, 2020

I am considering making a major breaking change to this standard:

  1. Replace all string arguments and return string values that contain multiple items with string arrays. This is now made possible with ABIEncoderV2 which is no longer experimental.

Doing this would simplify the code and make transparent contracts easier to implement. It will also give the ability to use transparent contracts with functions that take structs and string/byte arrays as arguments.

In addition I will update the code shown in the standard to use the latest production release of Solidity.

Any thoughts, ideas or arguments about this?

@mudgen
Copy link
Contributor Author

mudgen commented Feb 9, 2020

I am considering making another major change to the standard:

  1. Send and use and store the 4 byte function selectors instead of sending and storing the string function signatures. Replace the string function signatures with the 4 byte function selectors.

Here are the reasons for this:

  1. Originally the string function signatures were used so that a transparent contract could be queried for its ABI and events could show the function signatures directly. This is an easy way to get and see the function signatures used by a transparent contract. All this was so that user interface software could easily use this information to nicely display and use information about transparent contracts. However there is a problem with this: String function signatures are not enough for a good user interface. A good user interface also needs the ABI information about the return values of functions and if functions are read-only or not. With this additional ABI info a user interface can enable users to execute functions of a transparent contract and see the results.

  2. The same desired effect can be done by storing the ABIs of transparent contracts offline and connecting them with the 4 byte function selectors stored in the transparent contracts. For example etherscan provides a web API to retrieve the ABIs of contracts: https://etherscan.io/apis#contracts

  3. It costs less gas to send, use and store 4 byte function selectors vs. string function signatures.

@mudgen
Copy link
Contributor Author

mudgen commented Feb 25, 2020

Okay, so there were a number of major changes to make to this standard. Instead of making them I instead created a whole new standard and I just published it here: #2535

One of the major changes was a new name for the standard. The name of the new standard is Diamond Contract Standard.

@spalladino I finally found a new name for the Transparent Contract Standard, but since there were a number of major changes I created a new standard that replaces the Transparent Contract Standard. Check out the Diamond Contract Standard: #2535

@github-actions
Copy link

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

@github-actions github-actions bot added the stale label Nov 24, 2021
@github-actions
Copy link

github-actions bot commented Dec 8, 2021

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

@github-actions github-actions bot closed this as completed Dec 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests