/
OevDataFeedServer.sol
205 lines (197 loc) · 8.42 KB
/
OevDataFeedServer.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "./DataFeedServer.sol";
import "./interfaces/IOevDataFeedServer.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "./proxies/interfaces/IOevProxy.sol";
/// @title Contract that serves OEV Beacons and Beacon sets
/// @notice OEV Beacons and Beacon sets can be updated by the winner of the
/// respective OEV auctions. The beneficiary can withdraw the proceeds from
/// this contract.
contract OevDataFeedServer is DataFeedServer, IOevDataFeedServer {
using ECDSA for bytes32;
/// @notice Data feed with ID specific to the OEV proxy
/// @dev This implies that an update as a result of an OEV auction only
/// affects contracts that read through the respective proxy that the
/// auction was being held for
mapping(address => mapping(bytes32 => DataFeed))
internal _oevProxyToIdToDataFeed;
/// @notice Accumulated OEV auction proceeds for the specific proxy
mapping(address => uint256) public override oevProxyToBalance;
/// @notice Updates a data feed that the OEV proxy reads using the
/// aggregation signed by the absolute majority of the respective Airnodes
/// for the specific bid
/// @dev For when the data feed being updated is a Beacon set, an absolute
/// majority of the Airnodes that power the respective Beacons must sign
/// the aggregated value and timestamp. While doing so, the Airnodes should
/// refer to data signed to update an absolute majority of the respective
/// Beacons. The Airnodes should require the data to be fresh enough (e.g.,
/// at most 2 minutes-old), and tightly distributed around the resulting
/// aggregation (e.g., within 1% deviation), and reject to provide an OEV
/// proxy data feed update signature if these are not satisfied.
/// @param oevProxy OEV proxy that reads the data feed
/// @param dataFeedId Data feed ID
/// @param updateId Update ID
/// @param timestamp Signature timestamp
/// @param data Update data (an `int256` encoded in contract ABI)
/// @param packedOevUpdateSignatures Packed OEV update signatures, which
/// include the Airnode address, template ID and these signed with the OEV
/// update hash
function updateOevProxyDataFeedWithSignedData(
address oevProxy,
bytes32 dataFeedId,
bytes32 updateId,
uint256 timestamp,
bytes calldata data,
bytes[] calldata packedOevUpdateSignatures
) external payable override onlyValidTimestamp(timestamp) {
require(
timestamp > _oevProxyToIdToDataFeed[oevProxy][dataFeedId].timestamp,
"Does not update timestamp"
);
bytes32 oevUpdateHash = keccak256(
abi.encodePacked(
block.chainid,
address(this),
oevProxy,
dataFeedId,
updateId,
timestamp,
data,
msg.sender,
msg.value
)
);
int224 updatedValue = decodeFulfillmentData(data);
uint32 updatedTimestamp = uint32(timestamp);
uint256 beaconCount = packedOevUpdateSignatures.length;
if (beaconCount > 1) {
bytes32[] memory beaconIds = new bytes32[](beaconCount);
uint256 validSignatureCount;
for (uint256 ind = 0; ind < beaconCount; ) {
bool signatureIsNotOmitted;
(
signatureIsNotOmitted,
beaconIds[ind]
) = unpackAndValidateOevUpdateSignature(
oevUpdateHash,
packedOevUpdateSignatures[ind]
);
if (signatureIsNotOmitted) {
unchecked {
validSignatureCount++;
}
}
unchecked {
ind++;
}
}
// "Greater than or equal to" is not enough because full control
// of aggregation requires an absolute majority
require(
validSignatureCount > beaconCount / 2,
"Not enough signatures"
);
require(
dataFeedId == deriveBeaconSetId(beaconIds),
"Beacon set ID mismatch"
);
emit UpdatedOevProxyBeaconSetWithSignedData(
dataFeedId,
oevProxy,
updateId,
updatedValue,
updatedTimestamp
);
} else if (beaconCount == 1) {
{
(
bool signatureIsNotOmitted,
bytes32 beaconId
) = unpackAndValidateOevUpdateSignature(
oevUpdateHash,
packedOevUpdateSignatures[0]
);
require(signatureIsNotOmitted, "Missing signature");
require(dataFeedId == beaconId, "Beacon ID mismatch");
}
emit UpdatedOevProxyBeaconWithSignedData(
dataFeedId,
oevProxy,
updateId,
updatedValue,
updatedTimestamp
);
} else {
revert("Did not specify any Beacons");
}
_oevProxyToIdToDataFeed[oevProxy][dataFeedId] = DataFeed({
value: updatedValue,
timestamp: updatedTimestamp
});
oevProxyToBalance[oevProxy] += msg.value;
}
/// @notice Withdraws the balance of the OEV proxy to the respective
/// beneficiary account
/// @dev This does not require the caller to be the beneficiary because we
/// expect that in most cases, the OEV beneficiary will be a contract that
/// will not be able to make arbitrary calls. Our choice can be worked
/// around by implementing a beneficiary proxy.
/// @param oevProxy OEV proxy
function withdraw(address oevProxy) external override {
address oevBeneficiary = IOevProxy(oevProxy).oevBeneficiary();
require(oevBeneficiary != address(0), "Beneficiary address zero");
uint256 balance = oevProxyToBalance[oevProxy];
require(balance != 0, "OEV proxy balance zero");
oevProxyToBalance[oevProxy] = 0;
emit Withdrew(oevProxy, oevBeneficiary, balance);
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = oevBeneficiary.call{value: balance}("");
require(success, "Withdrawal reverted");
}
/// @notice Reads the data feed as the OEV proxy with ID
/// @param dataFeedId Data feed ID
/// @return value Data feed value
/// @return timestamp Data feed timestamp
function _readDataFeedWithIdAsOevProxy(
bytes32 dataFeedId
) internal view returns (int224 value, uint32 timestamp) {
DataFeed storage oevDataFeed = _oevProxyToIdToDataFeed[msg.sender][
dataFeedId
];
DataFeed storage dataFeed = _dataFeeds[dataFeedId];
if (oevDataFeed.timestamp > dataFeed.timestamp) {
(value, timestamp) = (oevDataFeed.value, oevDataFeed.timestamp);
} else {
(value, timestamp) = (dataFeed.value, dataFeed.timestamp);
}
require(timestamp > 0, "Data feed not initialized");
}
/// @notice Called privately to unpack and validate the OEV update
/// signature
/// @param oevUpdateHash OEV update hash
/// @param packedOevUpdateSignature Packed OEV update signature, which
/// includes the Airnode address, template ID and these signed with the OEV
/// update hash
/// @return signatureIsNotOmitted If the signature is omitted in
/// `packedOevUpdateSignature`
/// @return beaconId Beacon ID
function unpackAndValidateOevUpdateSignature(
bytes32 oevUpdateHash,
bytes calldata packedOevUpdateSignature
) private pure returns (bool signatureIsNotOmitted, bytes32 beaconId) {
(address airnode, bytes32 templateId, bytes memory signature) = abi
.decode(packedOevUpdateSignature, (address, bytes32, bytes));
beaconId = deriveBeaconId(airnode, templateId);
if (signature.length != 0) {
require(
(
keccak256(abi.encodePacked(oevUpdateHash, templateId))
.toEthSignedMessageHash()
).recover(signature) == airnode,
"Signature mismatch"
);
signatureIsNotOmitted = true;
}
}
}