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

Add ERC: Atomic Push-based Data Feed Among Contracts #235

Merged
merged 20 commits into from
Jun 13, 2024
Merged
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
274 changes: 274 additions & 0 deletions ERCS/erc-7615
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
---
eip: 7615

Check failure on line 2 in ERCS/erc-7615

View workflow job for this annotation

GitHub Actions / EIP Walidator

file name must reflect the preamble header `eip`

error[preamble-file-name]: file name must reflect the preamble header `eip` --> ERCS/erc-7615:2:5 | 2 | eip: 7615 | ^^^^^ this value | = help: this file's name should be `erc-7615.md` = help: see https://ethereum.github.io/eipw/preamble-file-name/
title: Atomic Push-based Data Feed Among Contracts
description: An Atomic Mechanism to Allow Publisher Contract Push Data to Subcriber Contracts
author: Elaine Zhang (@lanyinzly) <lz8aj@virginia.edu>, Jerry <jerrymindflow@gmail.com>, Amandafanny <amandafanny200@gmail.com>, Shouhao Wong (@wangshouh) <wongshouhao@outlook.com>, Doris Che (@Cheyukj) <dorischeyy@gmail.com>
discussions-to: https://ethereum-magicians.org/t/erc-7615-smart-contract-data-push-mechanism/18466
status: Draft
type: Standards Track
category: ERC
created: 2024-02-03
requires:

Check failure on line 11 in ERCS/erc-7615

View workflow job for this annotation

GitHub Actions / EIP Walidator

preamble header `requires` value is too short (min 1)

error[preamble-len-requires]: preamble header `requires` value is too short (min 1) --> ERCS/erc-7615:11:10 | 11 | requires: | ^ too short | = help: see https://ethereum.github.io/eipw/preamble-len-requires/
---
## Abstract
This ERC proposes a push-based mechanism for sending data, allowing publisher contract to automatically push certain data to subscriber contracts during a call. The specific implementation relies on two interfaces: one for publisher contract to push data, and another for the subscriber contract to receive data. When the publisher contract is called, it checks if the called function corresponds to subscriber addresses. If it does, the publisher contract push data to the subscriber contracts.

## Motivation
Currently, there are many keepers rely on off-chain data or seperate data collection process to monitor the events on chain. This proposal aims to establish a system where the publisher contract can atomicly push data to inform subscriber contracts about the updates. The direct on-chain interaction bewteen the publisher and the subscriber allows the system to be more trustless and efficient.

This proposal will offer significant advantages across a range of applications, such as enabling the boundless and permissionless expansion of DeFi, as well as enhancing DAO governance, among others.

### Lending Protocol

An example of publisher contract could be an oracle, which can automatically push the price update through initiating a call to the subscriber protocol. The lending protocol, as the subscriber, can automatically liquidate the lending positions based on the received price.

### Automatic Payment

A service provider can use a smart contract as a publisher contract, so that when a user call this contract, it can push the information to the subsriber contracts, such as, the users' wallets like NFT bound accounts that follows [ERC-6551](./eip-6551.md) or other smart contract wallets. The user's smart contract wallet can thus perform corresponding payment operations automatically. Compared to traditional `approve` needed approach, this solution allows more complex logic in implementation, such as limited payment, etc.

### PoS Without Transferring Assets

For some staking scenarios, especially NFT staking, the PoS contract can be set as the subscriber and the NFT contracts can be set as the publisher. Staking can thus achieved through contracts interation, allowing users to earn staking rewards without transferring assets.

When operations like `transfer` of NFT occur, the NFT contract can push this information to the PoS contract, which can then perform unstaking or other functions.

### DAO Voting

The DAO governance contract as a publisher could automatically triggers the push mechanism after the vote is completed, calling relevant subscriber contracts to directly implement the voting results, such as injecting funds into a certain account or pool.

## 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.

### Overview

The push mechanism can be divided into the following four steps:

1. The publisher contract is called.
2. The publisher contract query the subscriber list from the `selector` of the function called. The subscriber contract can put the selected data into `inbox`.
3. The publisher contract push `selector` and data through calling `exec` function of the subscriber contract.
4. The subscriber contract executes based on pushed `selector` and data, or it may request information from the publisher contract's inbox function as needed.

In the second step, the relationship between a called function and the corresponding subscriber can be configured in the publisher contract. Two configuration schemes are proposed:

1. Unconditional Push: Any call to the configured `selector` triggers a push
2. Conditional Push: Only the conditioned calls to the configured `selector` trigger a push based on the configuration.

It's allowed to configure multiple, different types of subscriber contracts for a single `selector`. The publisher contract will call the `exec` function of each subscriber contract to push the request.

When unsubscribing a contract from a `selector`, publisher contract MUST check whether `isLocked` function of the subscriber contract returns `true`.

It is OPTIONAL for a publisher contract to use the `inbox` mechanism to store data.

In the fourth step, the subscriber contract SHOULD handle all possible `selector` requests and data in the implementation of `exec` function. In some cases, `exec` MAY call `inbox` function of publisher contract to obtain the pushed data in full.


```
+---------+ +-----------+ +-------------+
| Client | | Publisher | | Subscriber |
+---------+ +-----------+ +-------------+
| | |
| Call somefunc(...) | |
|------------------------->| |
| | |
| | Query Subscriber |
| |----------------- |
| | | |
| |<---------------- |
| | |
| | Call exec(bytes4 selector, bytes calldata data) |
| |------------------------------------------------------->|
| | |
| | Result |
| |<-------------------------------------------------------|
| | |
| Result | |
|<-------------------------| |
| | |
```

### Contract interface

As mentioned above, there are Unconditional Push and Conditional Push two types of implementation. To implement Unconditional Push, the publisher contract SHOULD implement the following interface:

```solidity
interface IPushForce {
function isForceApproved(bytes4 selector, address target) external returns (bool);
function forceApprove(bytes4 selector, address target) external;
function forceCancel(bytes4 selector, address target) external;
}
```

`isForceApproved` is to query whether `selector` has already unconditionally bound to the subscriber contract with the address `target`.
`forceApprove` is to bind `selector` to the subscriber contract `target`. `forceCancel` is to cancel the binding relationship between `selector` and `target`, where `isLocked` function of `target` returns `true` is REQUIRED.

To implement Conditional Push, the publisher contract SHOULD implement the following interface:

```solidity
interface IPushFree {
function inbox(bytes4 selector) external returns (bytes memory);
function isApproved(bytes4 selector, address target, bytes calldata data) external returns (bool);
function approve(bytes4 selector, address target, bytes calldata data) external;
function cancel(bytes4 selector, address target, bytes calldata data) external;
}
```

`isApproved`, `approve`, and `cancel` have functionalities similar to the corresponding functions in `IPushForce`. However, an additional `data` parameter is introduced here for checking whether a push is needed.
The `inbox` here is used to store data in case of being called from the subscriber contract.

The publisher contract SHOULD implement `_push(bytes4 selector, bytes calldata data)` function, which acts as a hook. Any function within the publisher contract that needs to implement push mechanism must call this internal function. The function MUST include querying both unconditional and conditional subscription contracts based on `selector` and `data`, and then calling corresponding `exec` function of the subscribers.

A subscriber need to implement the following interface:

```solidity
interface IExec {
function isLocked(bytes4 selector, bytes calldata data) external returns (bool);
function exec(bytes4 selector, bytes calldata data) external;
}
```

`exec` is to receive requests from the publisher contracts and further proceed to execute.
`isLocked` is to check the status of whether the subscriber contract can unsubscribe the publisher contract based on `selector` and `data`. It is triggered when a request to unsubscribe is received.

## Rationale

### Unconditional and Conditional Configuration

When the sending contract is called, it is possible to trigger a push, requiring the caller to pay the resulting gas fees.
In some cases, an Unconditional Push is necessary, such as pushing price changes to a lending protocol. While, Conditional Push will reduce the unwanted gas consumption.

### Check `isLocked` Before Unsubscribing

Before `forceCancel` or `cancel`, the publisher contract MUST call the `isLocked` function of the subscriber contract to avoid unilateral unsubscribing. The subscriber contract may have a significant logical dependence on the publisher contract, and thus unsubscription could lead to severe issues within the subscriber contract. Therefore, the subscriber contract should implement `isLocked` function with thorough consideration.

### `inbox` Mechanism

In certain scenarios, the publisher contract may only push essential data with `selector` to the subscriber contracts, while the full data might be stored within `inbox`. Upon receiving the push from the publisher contract, the subscriber contract is optional to call `inbox`.
`inbox` mechanism simplifies the push information while still ensuring the availability of complete data, thereby reducing gas consumption.

### Using Function Selectors as Parameters

Using function selectors to retrieve the addresses of subscriber contracts allows
more detailed configuration.
For the subscriber contract, having the specific function of the request source based on the push information enables more accurate handling of the push information.

## Reference Implementation

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IPushFree, IPushForce} from "./interfaces/IPush.sol";
import {IPull} from "./interfaces/IPull.sol";

contract Foo is IPushFree, IPushForce {
using EnumerableSet for EnumerableSet.AddressSet;

mapping(bytes4 selector => mapping(uint256 tokenId => EnumerableSet.AddressSet targets)) private _registry;
mapping(bytes4 selector => EnumerableSet.AddressSet targets) private _registryOfAll;

modifier notLock(bytes4 selector, address target, bytes memory data) {
require(!IPull(target).isLocked(selector, data), "Foo: lock");
_;
}

function inbox(bytes4 selector) public view returns (bytes memory data) {
uint256 loadData;
assembly {
loadData := tload(selector)
}

data = abi.encode(loadData);
}

function isApproved(bytes4 selector, address target, bytes calldata data) external view override returns (bool) {
uint256 tokenId = abi.decode(data, (uint256));
return _registry[selector][tokenId].contains(target);
}

function isForceApproved(bytes4 selector, address target) external view override returns (bool) {
return _registryOfAll[selector].contains(target);
}

function approve(bytes4 selector, address target, bytes calldata data) external override {
uint256 tokenId = abi.decode(data, (uint256));
_registry[selector][tokenId].add(target);
}

function cancel(bytes4 selector, address target, bytes calldata data)
external
override
notLock(selector, target, data)
{
uint256 tokenId = abi.decode(data, (uint256));
_registry[selector][tokenId].remove(target);
}

function forceApprove(bytes4 selector, address target) external override {
_registryOfAll[selector].add(target);
}

function forceCancel(bytes4 selector, address target) external override notLock(selector, target, "") {
_registryOfAll[selector].remove(target);
}

function send(uint256 message) external {
_push(this.send.selector, message);
}

function _push(bytes4 selector, uint256 message) internal {
assembly {
tstore(selector, message)
}

address[] memory targets = _registry[selector][message].values();
for (uint256 i = 0; i < targets.length; i++) {
IPull(targets[i]).pull(selector, abi.encode(message));
}

targets = _registryOfAll[selector].values();
for (uint256 i = 0; i < targets.length; i++) {
IPull(targets[i]).pull(selector, abi.encode(message));
}
}
}

contract Bar is IPull {
event Log(bytes4 indexed selector, bytes data, bytes inboxData);

function isLocked(bytes4, bytes calldata) external pure override returns (bool) {
return true;
}

function pull(bytes4 selector, bytes calldata data) external {
bytes memory inboxData = IPushFree(msg.sender).inbox(selector);
emit Log(selector, data, inboxData);
}
}
```

## Security Considerations

### `exec` Attacks

The `exec` function is `public`, therefore, it is vulnerable to malicious calls where arbitrary push information can be inserted. Implementations of `exec` should carefully consider the arbitrariness of calls and should not directly use data passed by the exec function without verification.

### Reentrancy Attack

The publisher contract's call to the subscriber contract's `exec` function could lead to reentrancy attacks. Malicious subscription contracts might construct reentrancy attacks to the publisher contract within `exec`.

### Arbitrary Target Approve

Implementation of `forceApprove` and `approve` should have reasonable access controls; otherwise, unnecessary gas losses could be imposed on callers.

Check the gas usage of the `exec` function.

### isLocked implementation

Subscriber contracts should implement the `isLocked` function to avoid potential loss brought by unsubscription. This is particularly crucial for lending protocols implementing this proposal. Improper unsubscription can lead to abnormal clearing, causing considerable losses.

Similarly, when subscribing, the publisher contract should consider whether `isLocked` is properly implemented to prevent irrevocable subscriptions.

## Copyright
Copyright and related rights waived via [CC0](../LICENSE.md).