Skip to content

Commit

Permalink
Add EIP: On-Chain EIP-712 Visualization
Browse files Browse the repository at this point in the history
Merged by EIP-Bot.
  • Loading branch information
a6-dou committed May 20, 2023
1 parent 749bb69 commit ec29e01
Show file tree
Hide file tree
Showing 5 changed files with 390 additions and 0 deletions.
155 changes: 155 additions & 0 deletions EIPS/eip-6865.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
eip: 6865
title: On-Chain EIP-712 Visualization
description: Visualize structured data highlighting the potential consequences for users' assets
author: Abderrahmen Hanafi (@a6-dou)
discussions-to: https://ethereum-magicians.org/t/eip-6865-on-chain-eip-712-visualization/13800
status: Draft
type: Standards Track
category: ERC
created: 2023-04-10
requires: 712
---

## Abstract

Numerous protocols employ distinct [EIP-712](./eip-712.md) schemas, leading to unavoidable inconsistencies across the ecosystem. To address this issue, we propose a standardized approach for dApps to implement an on-chain view function called `visualizeEIP712Message`. This function takes an abi encoded EIP-712 payload message as input and returns a universally agreed-upon structured data format that emphasizes the potential impact on users' assets. Wallets can then display this structured data in a user-friendly manner, ensuring a consistent experience for end-users when interacting with various dApps and protocols.

## Motivation

The rapid expansion of the web3.0 ecosystem has unlocked numerous opportunities and innovations. However, this growth has also heightened users' vulnerability to security threats, such as phishing scams. Ensuring that users have a comprehensive understanding of the transactions they sign is crucial for mitigating these risks.

In an attempt to address this issue, we developed an in-house, open-source off-chain SDK for wallets to visualize various protocols. However, we encountered several challenges along the way:

- Scalability: Identifying and understanding all protocols that utilize EIP-712 and their respective business logic is a daunting task, particularly with limited resources. Crafting an off-chain solution for all these protocols is nearly impossible.
- Reliability: Grasping each protocol's business logic is difficult and may lead to misunderstandings of the actual implementation. This can result in inaccurate visualizations, which could be more detrimental than providing no visualization at all.
- Maintainability: Offering support for protocols with an off-chain solution is insufficient in a rapidly evolving ecosystem. Protocols frequently upgrade their implementations by extending features or fixing bugs, further complicating the maintenance process.

To overcome these challenges, we propose a standardized, on-chain solution that can accommodate the diverse and ever-changing web3.0 ecosystem. This approach would enhance scalability, reliability, and maintainability by shifting the responsibility of visualizing EIP-712 payloads from the wallets to the protocols themselves. Consequently, wallets can use a consistent and effective approach to EIP-712 message visualization.

The adoption of a universal solution will not only streamline the efforts and reduce the maintenance burden for wallet providers, but it will also allow for faster and more extensive coverage across the ecosystem. This will ultimately result in users gaining a clearer understanding of the transactions they're signing, leading to increased security and an improved overall user experience within the crypto space.

Currently, most of the wallets display something similar to image below

![](../assets/eip-6865/current-EIP-712-signature-wallet-interface.png)

With visualization we can achieve something similar to image below where more insightful details are revealed to user thanks to the structured data returned from the EIP

![](../assets/eip-6865/vision-EIP-712-signature-wallet-interface.png)

## Specification

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

Contracts implementing this proposal MUST include the `visualizeEIP712Message` function in the `verifyingContract` implementation so that wallets upon receiving a request to sign an EIP-712 message(`eth_signTypedData`) MAY call the function `visualizeEIP712Message` at the smart contract and chain specified in the EIP-712 message domain separator `verifyingContract` and `chainId` fields, respectively.

Wallets SHOULD ignore this proposal if the domain separator does not include the `verifyingContract` and `chainId` fields.

```solidity
/**
* @notice This function processes an EIP-712 payload message and returns a structured data format emphasizing the potential impact on users' assets.
* @dev The function returns assetsOut (assets the user is offering), assetsIn (assets the user would receive), and liveness (validity duration of the EIP-712 message).
* @param encodedMessage The ABI-encoded EIP-712 message (abi.encode(types, params)).
* @param domainHash The hash of the EIP-712 domain separator as defined in the EIP-712 proposal; see https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator.
* @return Result struct containing the user's assets impact and message liveness.
*/
function visualizeEIP712Message(
bytes memory encodedMessage,
bytes32 domainHash
) external view returns (Result memory);
```

### Params

`encodedMessage` is bytes that represents the encoded EIP-712 message with `abi.encode` and it can be decoded using `abi.decode`

`domainHash` is the bytes32 hash of the EIP-712 domain separator as defined in the EIP-712 proposal

### Outputs

The function MUST return `Result`, a struct that contains information's about user’s assets impact and the liveness of such a message if it gets signed.

```solidity
struct Liveness {
uint256 from;
uint256 to;
}
struct UserAssetMovement {
address assetTokenAddress;
uint256 id;
uint256[] amounts;
}
struct Result {
UserAssetMovement[] assetsIn;
UserAssetMovement[] assetsOut;
Liveness liveness;
}
```

#### `Liveness`

`Liveness` is a struct that defines the timestamps which the message is valid where:

- `from` is the starting timestamp.
- `to` is the expiry timestamp
- `from` MUST be less than `to`

#### `UserAssetMovement`

`UserAssetMovement` defines the user’s asset where:

- `assetTokenAddress` is the token ([ERC-20](./eip-20.md), [ERC-721](./eip-721.md), [ERC-1155](./eip-1155.md)) smart contract address where the zero address MUST represents the Native coin (Native ETH in the case of Ethereum network).
- `id` is the NFT ID, this item MUST ignored if the asset is not an NFT
- if token with `id` doesn’t exist in an NFT collection, this SHOULD be considered as any token within that collection
- `amounts` is an Array of `uint256` where items MUST define the amount per time curve, with time defined within liveness boundaries
- the first amount in `amounts` Array (amounts[0]) MUST be the amount of the asset at `liveness.from` timestamp
- the last amount in `amounts` Array (amounts[amounts.length-1]) MUST be the amount of the asset at `liveness.to` timestamp
- in most of the cases, `amounts` will be an Array with a single item which is MUST be the minimum amount of the asset.

#### `assetsIn`

`assetsIn` are the minimum assets which the user MUST get if the message is signed and fulfilled

#### `assetsOut`

`assetsOut` are the maximum assets which the user MUST offer if the message is signed and fulfilled

## Rationale

### on-chain

One might argue that certain processes can be done off-chain, which is true, but our experience building an off-chain TypeScript SDK to solve this matter revealed some issues:

- Reliability: Protocols developers are the ones responsible for developing the protocol itself, thus crafting the visualization is much more accurate when done by them.
- Scalability: Keeping up with the rapidly expanding ecosystem is hard. Wallets or 3rd party entities must keep an eye on each new protocol, understand it carefully (which poses the reliability issues mentioned above), and then only come up with an off-chain implementation.
- Maintainability: Many protocols implement smart contracts in an upgradable manner. This causes the off-chain visualization to differ from the real protocol behaviors (if updated), making the solution itself unreliable and lacking the scalability to handle various protocols.

### `DomainHash`

The `domainHash` is much needed by protocols to revert against unsupported versions of its EIP-712 implementation. It identifies the needed implementation in case the protocol implements various EIP-712 implementations (`name`) or to revert if the `domainHash` belongs to a different protocol.

In the future, if there is a registry that reroutes this EIP implementation for already deployed protocols that can't upgrade the existing deployed smart contract, `domainHash` can be used to identify protocols.

### Amounts Array

We suggest using an array of amounts (uint256[]) instead of a single uint256 to cover auctions, which are common in NFT protocols.

## Backwards Compatibility

No backward compatibility issues found.

## Reference Implementation

openSea Seaport NFT marketplace implementation example is available [here](../assets/eip-6865/contracts/SeaPortEIP712Visualizer.sol)

## Security Considerations

`visualizeEIP712Message` function should be reliable and accurately represent the potential impact of the EIP-712 message on users' assets. Wallet providers and users must trust the protocol's implementation of this function to provide accurate and up-to-date information.

`visualizeEIP712Message` function results should be treated based on the reputation of its `verifyingContract`, if the `verifyingContract` is trusted it means the `visualizeEIP712Message` function results are trusted as the this proposal implementation lives at the same address of `verifyingContract`.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
43 changes: 43 additions & 0 deletions assets/eip-6865/contracts/IEIP712Visualizer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pragma solidity ^0.8.0;

interface IEIP712Visualizer {
struct Liveness {
uint256 from;
uint256 to;
}

struct UserAssetMovement {
address assetTokenAddress;
uint256 id;
uint256[] amounts;
}

struct Result {
UserAssetMovement[] assetsIn;
UserAssetMovement[] assetsOut;
Liveness liveness;
}

/**
* @notice This function processes an EIP-712 payload message and returns a structured data format emphasizing the potential impact on users' assets.
* @dev The function returns assetsOut (assets the user is offering), assetsIn (assets the user would receive), and liveness (validity duration of the EIP-712 message).
*
* - MUST revert if the domainHash identifier is not supported (require(domainHash == DOMAIN_SEPARATOR, "message")).
* - MUST NOT revert if there are no assetsIn, assetsOut, or liveness values; returns nullish values instead.
* - assetsIn MUST include only assets for which the user is the recipient.
* - assetsOut MUST include only assets for which the user is the sender.
* - MUST returns liveness.to as type(uint256).max if the message never expires.
* - MUST returns liveness.from as block.timestamp if the message does not have a validity starting date.
* - MUST returns a set (array) of amounts in assetsIn.amounts and assetsOut.amount where items define the amount per time curve, with time defined within liveness boundaries.
* - amounts items MUST include the minimum amount.
* - MUST returns the minimum amount if amounts set contains only one item
*
* @param encodedMessage The ABI-encoded EIP-712 message (abi.encode(types, params)).
* @param domainHash The hash of the EIP-712 domain separator as defined in the EIP-712 proposal; see https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator.
* @return Result struct containing the user's assets impact and message liveness.
*/
function visualizeEIP712Message(
bytes memory encodedMessage,
bytes32 domainHash
) external view returns (Result memory);
}
192 changes: 192 additions & 0 deletions assets/eip-6865/contracts/SeaPortEIP712Visualizer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {IEIP712Visualizer} from "./IEIP712Visualizer.sol";

contract SeaPortEIP712Visualizer is IEIP712Visualizer {
bytes32 public constant DOMAIN_SEPARATOR =
0xb50c8913581289bd2e066aeef89fceb9615d490d673131fd1a7047436706834e; //v1.1

enum OrderType {
FULL_OPEN,
PARTIAL_OPEN,
FULL_RESTRICTED,
PARTIAL_RESTRICTED,
CONTRACT
}

enum ItemType {
NATIVE,
ERC20,
ERC721,
ERC1155,
ERC721_WITH_CRITERIA,
ERC1155_WITH_CRITERIA
}
struct OrderComponents {
address offerer;
address zone;
OfferItem[] offer;
ConsiderationItem[] consideration;
OrderType orderType;
uint256 startTime;
uint256 endTime;
bytes32 zoneHash;
uint256 salt;
bytes32 conduitKey;
uint256 counter;
}

struct OfferItem {
ItemType itemType;
address token;
uint256 identifierOrCriteria;
uint256 startAmount;
uint256 endAmount;
}

struct ConsiderationItem {
ItemType itemType;
address token;
uint256 identifierOrCriteria;
uint256 startAmount;
uint256 endAmount;
address payable recipient;
}

constructor() {}

function visualizeEIP712Message(
bytes memory encodedMessage,
bytes32 domainHash
) external view returns (Result memory) {
require(
domainHash == DOMAIN_SEPARATOR,
"SeaPortEIP712Visualizer: unsupported domain"
);

OrderComponents memory order = abi.decode(
encodedMessage,
(OrderComponents)
);

UserAssetMovement[] memory assetsOut = new UserAssetMovement[](
order.offer.length
);

for (uint256 i = 0; i < order.offer.length; ) {
uint256[] memory amounts = extractAmounts(order.offer[i]);
assetsOut[i] = UserAssetMovement({
assetTokenAddress: order.offer[i].token,
id: order.offer[i].identifierOrCriteria,
amounts: amounts
});

unchecked {
++i;
}
}

ConsiderationItem[] memory userConsiderations = fliterByRecepient(
order.consideration,
order.offerer
);
UserAssetMovement[] memory assetsIn = new UserAssetMovement[](
userConsiderations.length
);

for (uint256 i = 0; i < userConsiderations.length; ) {
uint256[] memory amounts = extractAmounts(userConsiderations[i]);

assetsIn[i] = UserAssetMovement({
assetTokenAddress: userConsiderations[i].token,
id: userConsiderations[i].identifierOrCriteria,
amounts: amounts
});

unchecked {
++i;
}
}

return
Result({
assetsIn: assetsIn,
assetsOut: assetsOut,
liveness: Liveness({from: order.startTime, to: order.endTime})
});
}

function fliterByRecepient(
ConsiderationItem[] memory consideration,
address recepient
) private view returns (ConsiderationItem[] memory) {
uint256 recepientItemsCount;
for (uint256 i = 0; i < consideration.length; ) {
if (consideration[i].recipient == recepient) {
unchecked {
++recepientItemsCount;
}
}

unchecked {
++i;
}
}
ConsiderationItem[] memory result = new ConsiderationItem[](
recepientItemsCount
);
uint256 resultIndex;
for (uint256 i = 0; i < recepientItemsCount; ) {
if (consideration[i].recipient == recepient) {
result[resultIndex] = consideration[i];
unchecked {
++resultIndex;
}
}

unchecked {
++i;
}
}

return result;
}

function extractAmounts(
OfferItem memory offer
) private pure returns (uint256[] memory) {
uint256[] memory amounts = new uint256[](2);
if (offer.endAmount == offer.startAmount) {
uint256[] memory amounts = new uint256[](1);
amounts[0] = offer.startAmount;
return amounts;
} else if (offer.endAmount > offer.startAmount) {
amounts[0] = offer.startAmount;
amounts[1] = offer.endAmount;
} else if (offer.endAmount < offer.startAmount) {
amounts[0] = offer.endAmount;
amounts[1] = offer.startAmount;
}

return amounts;
}

function extractAmounts(
ConsiderationItem memory consideration
) private pure returns (uint256[] memory) {
uint256[] memory amounts = new uint256[](2);
if (consideration.endAmount == consideration.startAmount) {
uint256[] memory amounts = new uint256[](1);
amounts[0] = consideration.startAmount;
return amounts;
} else if (consideration.endAmount > consideration.startAmount) {
amounts[0] = consideration.startAmount;
amounts[1] = consideration.endAmount;
} else {
amounts[0] = consideration.endAmount;
amounts[1] = consideration.startAmount;
}
return amounts;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit ec29e01

Please sign in to comment.