lip | title | status | author | discussions-to | created | updated |
---|---|---|---|---|---|---|
7 |
Introduce a composite oracle beacon report receiver |
Implemented |
Eugene Mamin, Sam Kozin, Eugene Pshenichnyy |
2022-01-14 |
2022-06-06 |
To have a possibility of using two or even more Lido Oracle beacon report intercepting callbacks, we introduce a composite oracle beacon report receiver. The last one implements the IBeaconReceiver
interface and internally holds an array of nested IBeaconReceiver
instances and supplies iterative execution by calling processLidoOracleReport
in a loop.
The OrderedCallbacksArray
contract implements the IOrderedCallbacksArray
interface (to add/insert/remove/view the callbacks) adding the access modifiers to allow storage changing calls (add/insert/remove) only originated by the Voting
contract (having require(msg.sender == Voting.address)
). The second proposed contract (CompositePostRebaseBeaconReceiver
) inherits OrderedCallbacksArray
simultaneously implementing the IBeaconReportReceiver
interface. It is allowed to call the processLidoOracleReport
function only by the LidoOracle
contract (having require(msg.sender == LidoOracle.address)
).
In the end, the CompositePostRebaseBeaconReceiver
contract is a top-level entity implementing the current proposal.
Currently, the LidoOracle
contract provides only one slot for a quorum report intercepting callback (see LidoOracle.setBeaconReportReceiver). We plan to occupy this slot soon with a newly developed contract implementing the LIP-6 coverage application mechanism. In contrast, we have a technical vision of the future cross-chain/L2 upgrades, which may require appending additional callbacks to propagate oracle beacon reports.
We propose the following contracts interface. The code below presumes the Solidity v0.8 syntax.
The contract provides callbacks storage in the form of addresses dynamic array.
modifier onlyVoting()
Enforces msg.sender
to equal voting
address set upon the contract construction.
constructor(address _voting, bytes4 _requiredIface)
Init contract and store the provided _voting
address to arm the onlyVoting
modifier. Store the provided _requiredIface
to avoid mistake with callback incorrect implementation
See: onlyVoting
.
function addCallback(address _callback) external override onlyVoting;
Adds the provided _callback
element at the end of the callbacks array (i.e., after its current last element).
- Reverts if
_callback
address is zero. - Reverts if
msg.sender
is not equal to the storedvoting
address. - Reverts if the new length of the callbacks array becomes greater than the
MAX_CALLBACKS_CNT
contract-wide constant. - Emits the
CallbackAdded
event. See:callbacksLength
.
function insertCallback(address _callback, uint256 _atIndex) external override onlyVoting;
Inserts the _callback
element at the specified by the _atIndex
param location in the callbacks array.
Elements at and after the _atIndex
position are shifted one position right to preserve the existing invocation order.
- Reverts if
_callback
address is zero. - Reverts if
_callback
doesn't support provided interface. - Reverts if
msg.sender
is not equal to the storedvoting
address. - Reverts if
_atIndex
greater than the length of the callbacks array. - Reverts if new length of the callbacks array becomes greater than the
MAX_CALLBACKS_CNT
contract-wide constant. - Emits the
CallbackAdded
event. See:callbacksLength
.
function removeCallback(uint256 _atIndex) external override onlyVoting
Removes element with a given by the _atIndex
param position from the callbacks array.
Elements after the _atIndex
position are shifted one position left to preserve the existing invocation order.
- Reverts if
_atIndex
is equal to or greater than the length of the callbacks array. - Reverts if
msg.sender
is not equal to the storedvoting
address. - Emits the
CallbackRemoved
event. See:callbacksLength
.
function callbacks(uint256 _atIndex) external view returns (address);
Get the callback element at the specified by the _atIndex
param position.
- Reverts if
_atIndex
is equal to or greater than the length of the callbacks array. See:callbacksLength
.
function callbacksLength() external view returns (uint256);
Get the current callbacks array length.
event CallbackAdded(address indexed callback, uint256 atIndex);
Emitted when a new callback
is added at the atIndex
position.
See: insertCallback
, removeCallback
.
event CallbackRemoved(address indexed callback, uint256 atIndex);
Emitted when a callback
was removed from the atIndex
position.
See: removeCallbacks
.
The contract inherited from OrderedCallbacksArray
to implement a composite design pattern applicable for the LidoOracle
contract as IBeaconReportReceiver
.
modifier onlyOracle()
Enforces the msg.sender
to equal oracle
address which is set upon the contract construction.
constructor(address _voting, address _oracle) OrderedCallbacksArray(_voting);
Init contract and store the provided _voting
and _oracle
addresses to arm the onlyVoting
and onlyOracle
modifiers.
See: OrderedCallbacksArray.onlyVoting
, onlyOracle
.
function processLidoOracleReport(
uint256 _postTotalPooledEther,
uint256 _preTotalPooledEther,
uint256 _timeElapsed
) external override onlyOracle
Implements the IBeaconReceiver
interface, which is suitable for the LidoOracle
contract.
Iteratively calls callback.processLidoOracleReport
for each stored callback
in the callbacks array preserving the order.
- The proposed
CompositePostRebaseBeaconReceiver
could be used with only oneIBeaconReportReceiver
callback inside, providing the same behavior as before by using the wrapped callback directly (without wrapping into a composite receiver). - Even if the callbacks array is empty, the existing behavior preserves, i.e., there are no additional side-effects incurred for the lido oracle reports except the gas spendings.
In the case of CompositePostRebaseBeaconReceiver
used with only one IBeaconReportReceiver
callback set, spending increase approximately by the ~4600 gas compared to direct usage of the underlying callback with LidoOracle
(without composite adapter).
The proposed contracts are non-upgradable for the sake of simplicity. In case of emergency, we can disable them entirely by calling LidoOracle.setBeaconReportReceiver(address(0))
and redeploy newly developed versions later from scratch without incurring additional proxy-initialization logic and state transitions.
There are two permissioned addresses introduced explicitly: Voting
and LidoOracle
. Only Voting
can add/insert/remove callbacks into the CompositePostRebaseBeaconReceiver
to make it feasible only by the Lido DAO direct vote-backed will. Only LidoOracle
is allowed to call processLidoOracleReport
to prevent propagating various fake oracle reports by the added callbacks or execution in an arbitrary time moment.
If one of the callbacks throws an exception then the whole transaction reverts and other callbacks effects also rollbacks.
A misbehaving callback could be easily removed from the current bunch by the DAO vote calling the remove(misbehavingCallbackIndex)
- LIP-6: in-protocol coverage application mechanism
- Original request launched this proposal
- An overview of the design options to break into the L2/cross-chains — contains proposals to setup additional beacon report receivers.
LidoOracle
documentationCompositePostRebaseBeaconReceiver
reference implementationOrderedCallbacksArray
reference implementationIOrderedCallbacksArray
interface definitionIBeaconReportReceiver
interface definition