ERC-4886 - Proxy Ownership Register

Created 2022-09-03
Status Stagnant
Category ERC
Type Standards Track
Authors

Abstract

A proxy protocol that allows users to nominate a proxy address to act on behalf of another wallet address, together with a delivery address for new assets. Smart contracts and applications making use of the protocol can take a proxy address and lookup holding information for the nominator address. This has a number of practical applications, including allowing users to store valuable assets safely in a cold wallet and interact with smart contracts using a proxy address of low value. The assets in the nominator are protected as all contract interactions take place with the proxy address. This eliminates a number of exploits seen recently where users' assets are drained through a malicious contract interaction. In addition, the register holds a delivery address, allowing new assets to be delivered directly to a cold wallet address.

Motivation

To make full use of Ethereum users often need to prove their ownership of existing assets. For example: * Discord communities require users to sign a message with their wallet to prove they hold the tokens or NFTs of that community. * Whitelist events (for example recent airdrops, or NFT mints), require the user to interact using a given address to prove eligibility. * Voting in DAOs and other protocols require the user to sign using the address that holds the relevant assets.

There are more examples, with the unifying theme being that the user must make use of the address with the assets to derive the platform benefit. This means the addresses holding these assets cannot be truly 'cold', and is a gift to malicious developers seeking to steal valuable assets. For example, a new project can offer free NFTs to holders of an existing NFT asset. The existing holders have to prove ownership by minting from the wallet with the asset that determined eligibility. This presents numerous possible attack vectors for a malicious developer who knows that all users interacting with the contract have an asset of that type.

Possibly even more damaging is the effect on user confidence across the whole ecosystem. Users become reluctant to interact with apps and smart contracts for fear of putting their assets at risk. They may also decide not to store assets in cold wallet addresses as they need to prove they own them on a regular basis. A pertinent example is the user trying to decide whether to 'vault' their NFT and lose access to a discord channel, or keep their NFT in another wallet, or even to connect their 'vault' to discord.

Ethereum is amazing at providing trustless proofs. The only time a user should need to interact using the wallet that holds an asset is if they intend to sell or transfer that asset. If a user merely wishes to prove ownership (to access a resource, get an airdrop, mint an NFT, or vote in a DAO), they should do this through a trustless proof stored on-chain.

Furthermore, users should be able to decide where new assets are delivered, rather than them being delivered to the wallet providing the interaction. This allows hot wallets to acquire assets sent directly to a cold wallet 'vault', possibly even the one they are representing in terms of asset ownership.

The aim of this EIP is to provide a convenient method to avoid this security concern and empower more people to feel confident leveraging the full scope of Ethereum functionality. Our vision is an Ethereum where users setup a new hardware wallet for assets they wish to hold long-term, then make one single contract interaction with that wallet: to nominate a hot wallet proxy. That user can always prove they own assets on that address, and they can specify it as a delivery address for new asset delivery.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Definitions

EPS Specification

There are two main parts to the register - a nomination and a proxy record:

Contract / Dapp                        Register

Nominator: 0x1234..             Nominator: 0x1234..
Proxy: 0x5678..     --------->  Proxy: 0x4567..
                                Delivery: 0x9876..

The first step to creating a proxy record is for an address to nominate another address as its proxy. This creates a nomination that maps the nominator (the address making the nomination) to the proposed proxy address.

This is not a proxy record on the register at this stage, as the proxy address needs to first accept the nomination. Until the nomination is accepted it can be considered to be pending. Once the proxy address has accepted the nomination a proxy record is added to the register.

When accepting a nomination the proxy address sets the delivery address for that proxy record. The proxy address remains in control of updating that delivery address as required. Both the nominator and proxy can delete the proxy record and nomination at any time. The proxy will continue forever if not deleted - it is eternal.

The register is a single smart contract that stores all nomination and register records. The information held for each is as follows: * Nomination: * The address of the Nominator * The address of the Proposed Proxy

Any address can act as a Nominator or a Proxy. A Nomination must have been made first in order for an address to accept acting as a Proxy.

A Nomination cannot be made to an address that is already active as either a Proxy or a Nominator, i.e. that address is already in an active proxy relationship.

The information for both Nominations and Proxy records is held as a mapping. For the Nomination this is address => address for the Nominator to the Proxy address. For the Proxy Record the mapping is from address => struct for the Proxy Address to a struct containing the Nominator and Delivery address.

Mapping between an address and its Nominator and Delivery address is a simple process as shown below:

Contract / Dapp                        Register

  |                                       |
  |------------- 0x4567..---------------> |
  |                                       |
  | <-------nominator: 0x1234..---------- |
  |         delivery: 0x9876..            |
  |                                       |

The protocol is fully backwards compatible. If it is passed an address that does not have an active mapping it will pass back the received address as both the Nominator and Delivery address, thereby preserving functionality as the address is acting on its own behalf.

Contract / Dapp                        Register

  |                                       |
  |------------- 0x0222..---------------> |
  |                                       |
  | <-------nominator: 0x0222..---------- |
  |         delivery: 0x0222..            |
  |                                       |

If the EPS register is passed the address of a Nominator it will revert. This is of vital importance. The purpose of the proxy is that the Proxy address is operating on behalf of the Nominator. The Proxy address therefore can derive the same benefits as the Nominator (for example discord roles based on the Nominator's holdings, or mint NFTs that require another NFT to be held). It is therefore imperative that the Nominator in an active proxy cannot also interact and derive these benefits, otherwise two addresses represent the same holding. A Nominator can of course delete the Proxy Record at any time and interact on it's own behalf, with the Proxy address instantly losing any benefits associated with the proxy relationship.

Solidity Interface Definition

Nomination Exists

function nominationExists(address _nominator) external view returns (bool);

Returns true if a Nomination exists for the address specified.

Nomination Exists for Caller

function nominationExistsForCaller() external view returns (bool);

Returns true if a Nomination exists for the msg.sender.

Proxy Record Exists

function proxyRecordExists(address _proxy) external view returns (bool);

Returns true if a Proxy Record exists for the passed Proxy address.

Proxy Record Exists for Caller

function proxyRecordExistsForCaller() external view returns (bool);

Returns true if a Proxy Record exists for the msg.sender.

Nominator Record Exists

function nominatorRecordExists(address _nominator) external view returns (bool);

Returns true if a Proxy Record exists for the passed Nominator address.

Nominator Record Exists for Caller

function nominatorRecordExistsForCaller() external view returns (bool);

Returns true if a Proxy Record exists for the msg.sender.

Get Proxy Record

function getProxyRecord(address _proxy) external view returns (address nominator, address proxy, address delivery);

Returns Nominator, Proxy and Delivery address for a passed Proxy address.

Get Proxy Record for Caller

function getProxyRecordForCaller() external view returns (address nominator, address proxy, address delivery);

Returns Nominator, Proxy and Delivery address for msg.sender as Proxy address.

Get Nominator Record

function getNominatorRecord(address _nominator) external view returns (address nominator, address proxy, address delivery);

Returns Nominator, Proxy and Delivery address for a passed Nominator address.

Get Nominator Record for Caller

function getNominatorRecordForCaller() external view returns (address nominator, address proxy, address delivery);

Returns Nominator, Proxy and Delivery address for msg.sender address as Nominator.

Address Is Active

function addressIsActive(address _receivedAddress) external view returns (bool);

Returns true if the passed address is Nominator or Proxy address on an active Proxy Record.

Address Is Active for Caller

function addressIsActiveForCaller() external view returns (bool);

Returns true if msg.sender is Nominator or Proxy address on an active Proxy Record.

Get Nomination

function getNomination(address _nominator) external view returns (address proxy);

Returns the proxy address for a Nomination when passed a Nominator.

Get Nomination for Caller

function getNominationForCaller() external view returns (address proxy);

Returns the proxy address for a Nomination if msg.sender is a Nominator

Get Addresses

function getAddresses(address _receivedAddress) external view returns (address nominator, address delivery, bool isProxied);

Returns the Nominator, Proxy, Delivery and a boolean isProxied for the passed address. If you pass an address that is not a Proxy address it will return address(0) for the Nominator, Proxy and Delivery address and isProxied of false. If you pass an address that is a Proxy address it will return the relvant addresses and isProxied of true.

Get Addresses for Caller

function getAddressesForCaller() external view returns (address nominator, address delivery, bool isProxied);

Returns the Nominator, Proxy, Delivery and a boolean isProxied for msg.sender. If msg.sender is not a Proxy address it will return address(0) for the Nominator, Proxy and Delivery address and isProxied of false. If msg.sender is a Proxy address it will return the relvant addresses and isProxied of true.

Get Role

function getRole(address _roleAddress) external view returns (string memory currentRole);

Returns a string value with a role for the passed address. Possible roles are:

None The address does not appear on the Register as either a Record or a Nomination.

Nominator - Pending The address is the Nominator on a Nomination which has yet to be accepted by the nominated Proxy address.

Nominator - Active The address is a Nominator on an active Proxy Record (i.e. the Nomination has been accepted).

Proxy - Active The address is a Proxy on an active Proxy Record.

Get Role for Caller

function getRoleForCaller() external view returns (string memory currentRole);

Returns a string value with a role for msg.sender. Possible roles are:

None The msg.sender does not appear on the Register as either a Record or a Nomination.

Nominator - Pending The msg.sender is the Nominator on a Nomination which has yet to be accepted by the nominated Proxy address.

Nominator - Active The msg.sender is a Nominator on an active Proxy Record (i.e. the Nomination has been accepted).

Proxy - Active The msg.sender is a Proxy on an active Proxy Record.

Make Nomination

function makeNomination(address _proxy, uint256 _provider) external payable;

Can be passed a Proxy address to create a Nomination for the msg.sender.

Provider is a required argument. If you do not have a Provider ID you can pass 0 as the default EPS provider. For details on the EPS Provider Program please see .

Accept Nomination

function acceptNomination(address _nominator, address _delivery, uint256 _provider) external;

Can be passed a Nominator and Delivery address to accept a Nomination for a msg.sender. Note that to accept a Nomination the Nomination needs to exists with the msg.sender as the Proxy. The Nominator passed to the function and that on the Nomination must match.

Provider is a required argument. If you do not have a Provider ID you can pass 0 as the default EPS provider. For details on the EPS Provider Program please see .

Update Delivery Record

function updateDeliveryAddress(address _delivery, uint256 _provider) external;

Can be passed a new Delivery address where the msg.sender is the Proxy on a Proxy Record.

Provider is a required argument. If you do not have a Provider ID you can pass 0 as the default EPS provider. For details on the EPS Provider Program please see .

Delete Record by Nominator

function deleteRecordByNominator(uint256 _provider) external;

Can be called to delete a Record and Nomination when the msg.sender is a Nominator.

Note that when both a Record and Nomination exist both are deleted. If no Record exists (i.e. the Nomination hasn't been accepted by the Proxy address) the Nomination is deleted.

Provider is a required argument. If you do not have a Provider ID you can pass 0 as the default EPS provider. For details on the EPS Provider Program please see .

Delete Record by Proxy

function deleteRecordByProxy(uint256 _provider) external;

Can be called to delete a Record and Nomination when the msg.sender is a Proxy.

Rationale

The rationale for this EIP was to provide a way for all existing and future Ethereum assets to be have a 'beneficial owner' (the proxy) that is different to the address custodying the asset. The use of a register to achieve this ensures that changes to existing tokens are not required. The register stores a trustless proof, signed by both the nominator and proxy, that can be relied upon as a true representation of asset ownership.

Backwards Compatibility

The EIP is fully backwards compatible.

Test Cases

The full SDLC for this proposal has been completed and it is operation at 0xfa3D2d059E9c0d348dB185B32581ded8E8243924 on mainnet, ropsten and rinkeby. The contract source code is validated and available on etherscan. The full unit test suite is available in ../static/assets/eip-4886/, as is the source code and example implementations.

Reference Implementation

Please see ../static/assets/eip-4886/contracts

Security Considerations

The core intention of the EIP is to improve user security by better safeguarding assets and allowing greater use of cold wallet storage.

Potential negative security implications have been considered and none are envisaged. The proxy record can only become operational when a nomination has been confirmed by a proxy address, both addresses therefore having provided signed proof.

From a usability perspective the key risk is in users specifying the incorrect asset delivery address, though it is noted that this burden of accuracy is no different to that currently on the network.

Copyright

Copyright and related rights waived via CC0.