From 9c06fcd4564db593229cb4f851850203cf650094 Mon Sep 17 00:00:00 2001 From: Burak Benligiray Date: Wed, 14 Aug 2024 12:43:46 +0300 Subject: [PATCH] Finalize Api3ServerV1OevExtension --- .../Api3ServerV1OevExtension.sol | 351 ++++++++++++------ .../interfaces/IApi3ServerV1OevExtension.sol | 92 +++++ 2 files changed, 321 insertions(+), 122 deletions(-) create mode 100644 contracts/api3-server-v1/interfaces/IApi3ServerV1OevExtension.sol diff --git a/contracts/api3-server-v1/Api3ServerV1OevExtension.sol b/contracts/api3-server-v1/Api3ServerV1OevExtension.sol index 9078782..31a2174 100644 --- a/contracts/api3-server-v1/Api3ServerV1OevExtension.sol +++ b/contracts/api3-server-v1/Api3ServerV1OevExtension.sol @@ -1,14 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.17; import "../access/AccessControlRegistryAdminnedWithManager.sol"; import "./DataFeedServer.sol"; +import "./interfaces/IApi3ServerV1OevExtension.sol"; +import "../vendor/@openzeppelin/contracts@4.8.2/utils/Address.sol"; import "../vendor/@openzeppelin/contracts@4.8.2/utils/cryptography/ECDSA.sol"; import "./interfaces/IApi3ServerV1.sol"; contract Api3ServerV1OevExtension is AccessControlRegistryAdminnedWithManager, - DataFeedServer + DataFeedServer, + IApi3ServerV1OevExtension { using ECDSA for bytes32; @@ -17,103 +20,127 @@ contract Api3ServerV1OevExtension is uint32 endTimestamp; } - string public constant AUCTIONEER_ROLE_DESCRIPTION = "Auctioneer"; + string public constant override AUCTIONEER_ROLE_DESCRIPTION = "Auctioneer"; - bytes32 public immutable auctioneerRole; + string public constant override WITHDRAWER_ROLE_DESCRIPTION = "Withdrawer"; - address public immutable api3ServerV1; + bytes32 public immutable override auctioneerRole; - mapping(uint256 => UpdateAllowance) public dappIdToUpdateAllowance; + bytes32 public immutable override withdrawerRole; + + address public immutable override api3ServerV1; + + mapping(uint256 => UpdateAllowance) public override dappIdToUpdateAllowance; constructor( - address accessControlRegistry_, - string memory adminRoleDescription_, - address manager_, - address api3ServerV1_ + address _accessControlRegistry, + string memory _adminRoleDescription, + address _manager, + address _api3ServerV1 ) AccessControlRegistryAdminnedWithManager( - accessControlRegistry_, - adminRoleDescription_, - manager_ + _accessControlRegistry, + _adminRoleDescription, + _manager ) { - require(api3ServerV1_ != address(0), "Api3ServerV1 address zero"); - api3ServerV1 = api3ServerV1_; + require(_api3ServerV1 != address(0), "Api3ServerV1 address zero"); + api3ServerV1 = _api3ServerV1; auctioneerRole = _deriveRole( - _deriveAdminRole(manager), + _deriveAdminRole(_manager), AUCTIONEER_ROLE_DESCRIPTION ); + withdrawerRole = _deriveRole( + _deriveAdminRole(_manager), + WITHDRAWER_ROLE_DESCRIPTION + ); } // If an auctioneer accidentally provides a signature that is too far in the future, // revoke its auctioneer role and then reset update allowances for the affected dApps. - function resetUpdateAllowance(uint256 dappId) external { + function resetUpdateAllowance(uint256 dappId) external override { require(msg.sender == manager, "Sender not manager"); delete dappIdToUpdateAllowance[dappId]; + emit ResetUpdateAllowance(dappId); + } + + function withdraw(address recipient, uint256 amount) external override { + require( + IAccessControlRegistry(accessControlRegistry).hasRole( + withdrawerRole, + msg.sender + ), + "Sender cannot withdraw" + ); + (bool success, ) = recipient.call{value: amount}(""); + require(success, "Withdrawal reverted"); + emit Withdrew(recipient, amount, msg.sender); } // The updater whose address is specified by the bidder calls this function with the exact bid amount. // Doing so allows the updater to use the signed data until the end timestamp. function payOevBid( - address auctioneer, uint256 dappId, uint32 updateAllowanceEndTimestamp, bytes calldata signature - ) external payable { + ) external payable override { require( - IAccessControlRegistry(accessControlRegistry).hasRole( - auctioneerRole, - auctioneer - ), - "Auctioneer invalid" + updateAllowanceEndTimestamp > block.timestamp, + "Timestamp stale" ); require( updateAllowanceEndTimestamp < block.timestamp + 1 hours, - "Timestamp not valid" + "Timestamp too far from future" ); - UpdateAllowance storage updateAllowance = dappIdToUpdateAllowance[ - dappId - ]; + address auctioneer = ( + keccak256( + abi.encodePacked( + block.chainid, + dappId, + msg.sender, + msg.value, + updateAllowanceEndTimestamp + ) + ).toEthSignedMessageHash() + ).recover(signature); require( - updateAllowance.endTimestamp < updateAllowanceEndTimestamp, - "End timestamp stale" + IAccessControlRegistry(accessControlRegistry).hasRole( + auctioneerRole, + auctioneer + ), + "Signature mismatch" ); require( - ( - keccak256( - abi.encodePacked( - block.chainid, - dappId, - msg.sender, - msg.value, - updateAllowanceEndTimestamp - ) - ).toEthSignedMessageHash() - ).recover(signature) == auctioneer, - "Signature mismatch" + dappIdToUpdateAllowance[dappId].endTimestamp < + updateAllowanceEndTimestamp, + "Timestamp not more recent" ); dappIdToUpdateAllowance[dappId] = UpdateAllowance({ updater: msg.sender, endTimestamp: updateAllowanceEndTimestamp }); - // Emit event - // The auction cop needs to check this event for confirmation/contradiction. - // We may introduce a bid ID (which means we wouldn't need a typehash). - } - - function withdraw(address recipient, uint256 amount) external { - // Add a role - require(msg.sender == manager, "Sender not manager"); - (bool success, ) = recipient.call{value: amount}(""); - require(success, "Withdrawal reverted"); - // Emit event + emit PaidOevBid( + dappId, + msg.sender, + msg.value, + updateAllowanceEndTimestamp, + auctioneer + ); } // templateIds are the actual ones used by the dAPI (and not the once-hashed OEV ones) - function updateDappOevDataFeedWithAllowedSignedData( + function updateDappOevDataFeed( uint256 dappId, bytes[] calldata signedData - ) external { + ) + external + override + returns ( + bytes32 baseDataFeedId, + int224 updatedValue, + uint32 updatedTimestamp + ) + { UpdateAllowance storage updateAllowance = dappIdToUpdateAllowance[ dappId ]; @@ -122,35 +149,67 @@ contract Api3ServerV1OevExtension is block.timestamp < updateAllowance.endTimestamp, "Sender cannot update anymore" ); - updateDappOevDataFeedWithSignedData( + ( + baseDataFeedId, + updatedValue, + updatedTimestamp + ) = _updateDappOevDataFeed( dappId, updateAllowance.endTimestamp, signedData ); - // Emit event } - function simulateDappOevDataFeedUpdateWithSignedData( + function simulateDappOevDataFeedUpdate( uint256 dappId, bytes[] calldata signedData - ) external { - require(tx.origin == address(0), "Tx origin not zero address"); - updateDappOevDataFeedWithSignedData( - dappId, - type(uint256).max, - signedData - ); + ) + external + override + returns ( + bytes32 baseDataFeedId, + int224 updatedValue, + uint32 updatedTimestamp + ) + { + require(msg.sender == address(0), "Sender address not zero"); + ( + baseDataFeedId, + updatedValue, + updatedTimestamp + ) = _updateDappOevDataFeed(dappId, type(uint256).max, signedData); + } + + function simulateExternalCall( + address target, + bytes calldata data + ) external override returns (bytes memory) { + require(msg.sender == address(0), "Sender address not zero"); + return Address.functionCall(target, data); + } + + function dataFeeds( + bytes32 dataFeedId + ) external view override returns (int224 value, uint32 timestamp) { + DataFeed storage dataFeed = _dataFeeds[dataFeedId]; + (value, timestamp) = (dataFeed.value, dataFeed.timestamp); } - function updateDappOevDataFeedWithSignedData( + function _updateDappOevDataFeed( uint256 dappId, uint256 updateAllowanceEndTimestamp, bytes[] calldata signedData - ) private { + ) + private + returns ( + bytes32 baseDataFeedId, + int224 updatedValue, + uint32 updatedTimestamp + ) + { uint256 beaconCount = signedData.length; - bytes32[] memory baseBeaconIds = new bytes32[](beaconCount); - bytes32[] memory oevBeaconIds = new bytes32[](beaconCount); - for (uint256 ind = 0; ind < beaconCount; ind++) { + require(beaconCount > 0, "Signed data empty"); + if (beaconCount == 1) { ( address airnode, bytes32 templateId, @@ -158,58 +217,108 @@ contract Api3ServerV1OevExtension is bytes memory data, bytes memory signature ) = abi.decode( - signedData[ind], + signedData[0], (address, bytes32, uint256, bytes, bytes) ); - bytes32 baseBeaconId = deriveBeaconId(airnode, templateId); + baseDataFeedId = deriveBeaconId(airnode, templateId); bytes32 oevBeaconId = keccak256( - abi.encodePacked(dappId, baseBeaconId) + abi.encodePacked(dappId, baseDataFeedId) ); - baseBeaconIds[ind] = baseBeaconId; - oevBeaconIds[ind] = oevBeaconId; - if (signature.length != 0) { - require( - ( - keccak256( - abi.encodePacked( - keccak256(abi.encodePacked(templateId)), - timestamp, - data - ) - ).toEthSignedMessageHash() - ).recover(signature) == airnode, - "Signature mismatch" - ); - // Cannot use processBeaconUpdate() here because data is not calldata - // Timestamp implicitly can't be more than 1 hours in the future due to the check in payOevBid() - require( - timestamp < updateAllowanceEndTimestamp, - "Timestamp not allowed" - ); - require( - timestamp > _dataFeeds[oevBeaconId].timestamp, - "Does not update timestamp" - ); - _dataFeeds[oevBeaconId] = DataFeed({ - value: decodeFulfillmentData(data), - timestamp: uint32(timestamp) - }); - } + require( + ( + keccak256( + abi.encodePacked( + keccak256(abi.encodePacked(templateId)), + timestamp, + data + ) + ).toEthSignedMessageHash() + ).recover(signature) == airnode, + "Signature mismatch" + ); + require( + timestamp < updateAllowanceEndTimestamp, + "Timestamp not allowed" + ); + require( + timestamp > _dataFeeds[oevBeaconId].timestamp, + "Does not update timestamp" + ); + updatedValue = decodeFulfillmentData(data); + updatedTimestamp = uint32(timestamp); ( int224 baseBeaconValue, uint32 baseBeaconTimestamp - ) = IApi3ServerV1(api3ServerV1).dataFeeds(baseBeaconId); - if (baseBeaconTimestamp > _dataFeeds[oevBeaconId].timestamp) { - _dataFeeds[oevBeaconId] = DataFeed({ - value: baseBeaconValue, - timestamp: baseBeaconTimestamp - }); + ) = IApi3ServerV1(api3ServerV1).dataFeeds(baseDataFeedId); + if (baseBeaconTimestamp > updatedTimestamp) { + updatedValue = baseBeaconValue; + updatedTimestamp = baseBeaconTimestamp; } - } - if (beaconCount > 1) { - (int224 updatedValue, uint32 updatedTimestamp) = aggregateBeacons( - oevBeaconIds - ); + _dataFeeds[oevBeaconId] = DataFeed({ + value: updatedValue, + timestamp: updatedTimestamp + }); + } else { + bytes32[] memory baseBeaconIds = new bytes32[](beaconCount); + bytes32[] memory oevBeaconIds = new bytes32[](beaconCount); + for (uint256 ind = 0; ind < beaconCount; ind++) { + ( + address airnode, + bytes32 templateId, + uint256 timestamp, + bytes memory data, + bytes memory signature + ) = abi.decode( + signedData[ind], + (address, bytes32, uint256, bytes, bytes) + ); + baseBeaconIds[ind] = deriveBeaconId(airnode, templateId); + oevBeaconIds[ind] = keccak256( + abi.encodePacked(dappId, baseBeaconIds[ind]) + ); + if (signature.length != 0) { + require( + ( + keccak256( + abi.encodePacked( + keccak256(abi.encodePacked(templateId)), + timestamp, + data + ) + ).toEthSignedMessageHash() + ).recover(signature) == airnode, + "Signature mismatch" + ); + // Timestamp implicitly can't be more than 1 hours in the future due to the check in payOevBid() + require( + timestamp < updateAllowanceEndTimestamp, + "Timestamp not allowed" + ); + require( + timestamp > _dataFeeds[oevBeaconIds[ind]].timestamp, + "Does not update timestamp" + ); + _dataFeeds[oevBeaconIds[ind]] = DataFeed({ + value: decodeFulfillmentData(data), + timestamp: uint32(timestamp) + }); + } + ( + int224 baseBeaconValue, + uint32 baseBeaconTimestamp + ) = IApi3ServerV1(api3ServerV1).dataFeeds(baseBeaconIds[ind]); + if ( + baseBeaconTimestamp > + _dataFeeds[oevBeaconIds[ind]].timestamp + ) { + _dataFeeds[oevBeaconIds[ind]] = DataFeed({ + value: baseBeaconValue, + timestamp: baseBeaconTimestamp + }); + } + } + baseDataFeedId = deriveBeaconSetId(baseBeaconIds); + (updatedValue, updatedTimestamp) = aggregateBeacons(oevBeaconIds); bytes32 oevBeaconSetId = keccak256( abi.encodePacked(dappId, deriveBeaconSetId(baseBeaconIds)) ); @@ -225,13 +334,11 @@ contract Api3ServerV1OevExtension is timestamp: updatedTimestamp }); } - // Emit event - } - - function dataFeeds( - bytes32 dataFeedId - ) external view returns (int224 value, uint32 timestamp) { - DataFeed storage dataFeed = _dataFeeds[dataFeedId]; - (value, timestamp) = (dataFeed.value, dataFeed.timestamp); + emit UpdatedDappOevDataFeed( + dappId, + baseDataFeedId, + updatedValue, + updatedTimestamp + ); } } diff --git a/contracts/api3-server-v1/interfaces/IApi3ServerV1OevExtension.sol b/contracts/api3-server-v1/interfaces/IApi3ServerV1OevExtension.sol new file mode 100644 index 0000000..9359903 --- /dev/null +++ b/contracts/api3-server-v1/interfaces/IApi3ServerV1OevExtension.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../../access/interfaces/IAccessControlRegistryAdminnedWithManager.sol"; +import "../interfaces/IDataFeedServer.sol"; + +interface IApi3ServerV1OevExtension is + IAccessControlRegistryAdminnedWithManager, + IDataFeedServer +{ + event ResetUpdateAllowance(uint256 indexed dappId); + + event Withdrew(address recipient, uint256 amount, address sender); + + event PaidOevBid( + uint256 indexed dappId, + address updater, + uint256 bidAmount, + uint256 updateAllowanceEndTimestamp, + address auctioneer + ); + + event UpdatedDappOevDataFeed( + uint256 indexed dappId, + bytes32 dataFeedId, + int224 updatedValue, + uint32 updatedTimestamp + ); + + function resetUpdateAllowance(uint256 dappId) external; + + function withdraw(address recipient, uint256 amount) external; + + function payOevBid( + uint256 dappId, + uint32 updateAllowanceEndTimestamp, + bytes calldata signature + ) external payable; + + function updateDappOevDataFeed( + uint256 dappId, + bytes[] calldata signedData + ) + external + returns ( + bytes32 baseDataFeedId, + int224 updatedValue, + uint32 updatedTimestamp + ); + + function simulateDappOevDataFeedUpdate( + uint256 dappId, + bytes[] calldata signedData + ) + external + returns ( + bytes32 baseDataFeedId, + int224 updatedValue, + uint32 updatedTimestamp + ); + + function simulateExternalCall( + address target, + bytes calldata data + ) external returns (bytes memory); + + function dataFeeds( + bytes32 dataFeedId + ) external view returns (int224 value, uint32 timestamp); + + // solhint-disable-next-line func-name-mixedcase + function AUCTIONEER_ROLE_DESCRIPTION() + external + view + returns (string memory); + + // solhint-disable-next-line func-name-mixedcase + function WITHDRAWER_ROLE_DESCRIPTION() + external + view + returns (string memory); + + function auctioneerRole() external view returns (bytes32); + + function withdrawerRole() external view returns (bytes32); + + function api3ServerV1() external view returns (address); + + function dappIdToUpdateAllowance( + uint256 dappId + ) external view returns (address updater, uint32 endTimestamp); +}