Atomic Push-based Data Feed Among Contracts
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 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:
- The publisher contract is called.
- The publisher contract query the subscriber list from the
selector
of the function called. The subscriber contract can put the selected data intoinbox
. - The publisher contract push
selector
and data through callingexec
function of the subscriber contract. - 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:
- Unconditional Push: Any call to the configured
selector
triggers a push - 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.
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:
interface IPushForce {
event ForceApprove(bytes4 indexed selector, address indexed target);
event ForceCancel(bytes4 indexed selector, address indexed target);
event RenounceForceApprove();
event RenounceForceCancel();
error MustRenounce();
error ForceApproveRenounced();
error ForceCancelRenounced();
function isForceApproved(bytes4 selector, address target) external returns (bool);
function forceApprove(bytes4 selector, address target) external;
function forceCancel(bytes4 selector, address target) external;
function isRenounceForceApprove() external returns (bool);
function isRenounceForceCancel() external returns (bool);
function renounceForceApprove(bytes memory) external;
function renounceForceCancel(bytes memory) 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.
renounceForceApprove
is used to relinquish the forceApprove
permission. After calling the renounceForceApprove
function, forceApprove
can no longer be called. Similarly, renounceForceCancel
is used to relinquish the forceCancel
permission. After calling the renounceForceCancel
function, forceCancel
can no longer be called.
To implement Conditional Push, the publisher contract SHOULD implement the following interface:
interface IPushFree {
event Approve(bytes4 indexed selector, address indexed target, bytes data);
event Cancel(bytes4 indexed selector, address indexed target, bytes data);
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:
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.
Renounce Safety Enhancement
Both forceApprove
and forceCancel
permissions can be relinquished using their respective renounce functions. When both renounceForceApprove
and renounceForceCancel
are called, the registered push targets can longer be changed, greatly enhancing security.
Reference Implementation
pragma solidity ^0.8.24;
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IPushFree, IPushForce} from "./interfaces/IPush.sol";
import {IExec} from "./interfaces/IExec.sol";
contract Foo is IPushFree, IPushForce {
using EnumerableSet for EnumerableSet.AddressSet;
bool public override isRenounceForceApprove;
bool public override isRenounceForceCancel;
mapping(bytes4 selector => mapping(uint256 tokenId => EnumerableSet.AddressSet targets)) private _registry;
mapping(bytes4 selector => EnumerableSet.AddressSet targets) private _registryOfAll;
// mapping(bytes4 => bytes) public inbox;
modifier notLock(bytes4 selector, address target, bytes memory data) {
require(!IExec(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 {
if (isRenounceForceApprove) revert ForceApproveRenounced();
_registryOfAll[selector].add(target);
}
function forceCancel(bytes4 selector, address target) external override notLock(selector, target, "") {
if (isRenounceForceCancel) revert ForceCancelRenounced();
_registryOfAll[selector].remove(target);
}
function renounceForceApprove(bytes memory data) external override {
(bool burn) = abi.decode(data, (bool));
if (burn != true) {
revert MustRenounce();
}
isRenounceForceApprove = true;
emit RenounceForceApprove();
}
function renounceForceCancel(bytes memory data) external override {
(bool burn) = abi.decode(data, (bool));
if (burn != true) {
revert MustRenounce();
}
isRenounceForceCancel = true;
emit RenounceForceCancel();
}
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++) {
IExec(targets[i]).exec(selector, abi.encode(message));
}
targets = _registryOfAll[selector].values();
for (uint256 i = 0; i < targets.length; i++) {
IExec(targets[i]).exec(selector, abi.encode(message));
}
}
}
contract Bar is IExec {
event Log(bytes4 indexed selector, bytes data, bytes inboxData);
function isLocked(bytes4, bytes calldata) external pure override returns (bool) {
return true;
}
function exec(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.