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

feat: add data request incentives #83

Merged
merged 25 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
08f44e3
feat(core): add request fee
mariocao Jan 9, 2025
02f13a0
refactor(core): use struct for request details
mariocao Jan 9, 2025
3c8e538
feat(core): update interfaces to support fees
mariocao Jan 9, 2025
4498b70
test(core): add tests for result fee
mariocao Jan 9, 2025
f8b5863
feat(core): add request fee without gas refund
mariocao Jan 9, 2025
d09bdaf
feat(core): add request fee refunds based on used gas
mariocao Jan 10, 2025
058b49d
test(core): add test for fee management
mariocao Jan 10, 2025
ec98667
chore(coverage): skip mocks, interfaces and libs
mariocao Jan 10, 2025
a3519ac
feat(prover): add batch sender to mapping
mariocao Jan 13, 2025
7e6118e
test(prover): add tests for batch sender
mariocao Jan 13, 2025
2058fb2
feat(core): add fee distribution
mariocao Jan 13, 2025
d0d2c02
test(core): test fee distribution logic
mariocao Jan 13, 2025
6d846b6
test(core): check events for fee distributions
mariocao Jan 13, 2025
881fbf9
refactor(core): check if submitter fee is positive
mariocao Jan 13, 2025
3398571
test: simplify core tests
mariocao Jan 13, 2025
9293806
feat(core): add increase fees function
mariocao Jan 13, 2025
e2a5962
test(core): add increase fee tests
mariocao Jan 13, 2025
e0774a1
chore: improve code comments
mariocao Jan 13, 2025
8f87d98
feat(core, prover): add pausable support
mariocao Jan 14, 2025
d029083
test(core, prover): check functions while paused
mariocao Jan 14, 2025
7a0be6a
feat(core): extend PendingRequest struct with fee info
mariocao Jan 14, 2025
e4b48a0
test(core): update tests using pending requests
mariocao Jan 14, 2025
31a4b49
chore: bump solhint
mariocao Jan 14, 2025
64d6519
refactor(core): update getPendingRequests pause behavior
mariocao Jan 14, 2025
4a84692
chore(mocks): fix wrong comment for increaseFees
mariocao Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ module.exports = {
grep: '[Gg]as.*[Aa]nalysis',
invert: true,
},
skipFiles: ['mocks/', 'interfaces/', 'libraries/SedaDataTypes.sol'],
};
Binary file modified bun.lockb
Binary file not shown.
222 changes: 194 additions & 28 deletions contracts/core/SedaCoreV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.24;

import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

import {IRequestHandler} from "../interfaces/IRequestHandler.sol";
Expand All @@ -15,10 +16,26 @@ import {SedaDataTypes} from "../libraries/SedaDataTypes.sol";
/// @title SedaCoreV1
/// @notice Core contract for the Seda protocol, managing requests and results
/// @dev Implements ResultHandler and RequestHandler functionalities, and manages active requests
contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpgradeable, OwnableUpgradeable {
using EnumerableSet for EnumerableSet.Bytes32Set;
contract SedaCoreV1 is
ISedaCore,
RequestHandlerBase,
ResultHandlerBase,
UUPSUpgradeable,
OwnableUpgradeable,
PausableUpgradeable
{
// ============ Types & Constants============

struct RequestDetails {
address requestor;
uint256 timestamp;
uint256 requestFee;
uint256 resultFee;
uint256 batchFee;
uint256 gasLimit;
}

// ============ Constants ============
using EnumerableSet for EnumerableSet.Bytes32Set;

// Constant storage slot for the state following the ERC-7201 standard
bytes32 private constant CORE_V1_STORAGE_SLOT =
Expand All @@ -33,14 +50,12 @@ contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpg

/// @custom:storage-location erc7201:sedacore.storage.v1
struct SedaCoreStorage {
// Enumerable Set to store the request IDs that are pending
// `pendingRequests` keeps track of all active data requests that have been posted but not yet fulfilled.
// This set is used to manage the lifecycle of requests, allowing easy retrieval and status tracking.
// When a request is posted, it is added to `pendingRequests`.
// When a result is posted and the request is fulfilled, it is removed from `pendingRequests`
// Tracks active data requests to ensure proper lifecycle management and prevent
// duplicate fulfillments. Requests are removed only after successful fulfillment
EnumerableSet.Bytes32Set pendingRequests;
// Mapping to store request timestamps for pending DRs
mapping(bytes32 => uint256) requestTimestamps;
// Associates request IDs with their metadata to enable fee distribution and
// timestamp validation during result submission
mapping(bytes32 => RequestDetails) requestDetails;
}

// ============ Constructor & Initializer ============
Expand All @@ -54,9 +69,13 @@ contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpg
/// @param sedaProverAddress The address of the Seda prover contract
/// @dev This function replaces the constructor for proxy compatibility and can only be called once
function initialize(address sedaProverAddress) external initializer {
__ResultHandler_init(sedaProverAddress);
__Ownable_init(msg.sender);
// Initialize inherited contracts
__UUPSUpgradeable_init();
__Ownable_init(msg.sender);
__Pausable_init();

// Initialize derived contracts
__ResultHandler_init(sedaProverAddress);
}

// ============ Public Functions ============
Expand All @@ -65,11 +84,35 @@ contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpg
/// @dev Overrides the base implementation to also add the request ID and timestamp to storage
function postRequest(
SedaDataTypes.RequestInputs calldata inputs
) public override(RequestHandlerBase, IRequestHandler) returns (bytes32) {
bytes32 requestId = super.postRequest(inputs);
) public payable override(RequestHandlerBase, IRequestHandler) whenNotPaused returns (bytes32) {
return postRequest(inputs, 0, 0, 0);
}

function postRequest(
SedaDataTypes.RequestInputs calldata inputs,
uint256 requestFee,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to fix here but we should really have a wrapper or something to simplify the 3 fees and the rest of the inputs properties. (even if the user is overpaying, they'll get it refunded.)

uint256 resultFee,
uint256 batchFee
) public payable whenNotPaused returns (bytes32) {
// Validate that the sent ETH matches exactly the sum of all specified fees
// This prevents users from accidentally overpaying or underpaying fees
if (msg.value != requestFee + resultFee + batchFee) {
revert InvalidFeeAmount();
}

// Call parent contract's postResult base implementation
bytes32 requestId = RequestHandlerBase.postRequest(inputs);

// Store pending request and request details
_addRequest(requestId);
// Store the request timestamp
_storageV1().requestTimestamps[requestId] = block.timestamp;
_storageV1().requestDetails[requestId] = RequestDetails({
requestor: msg.sender,
timestamp: block.timestamp,
requestFee: requestFee,
resultFee: resultFee,
batchFee: batchFee,
gasLimit: inputs.execGasLimit + inputs.tallyGasLimit
gluax marked this conversation as resolved.
Show resolved Hide resolved
});

return requestId;
}
Expand All @@ -80,41 +123,155 @@ contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpg
SedaDataTypes.Result calldata result,
uint64 batchHeight,
bytes32[] calldata proof
) public override(ResultHandlerBase, IResultHandler) returns (bytes32) {
uint256 requestTimestamp = _storageV1().requestTimestamps[result.drId];
// Validate result timestamp comes after request timestamp
// Note: requestTimestamp = 0 for requests not tracked by this contract (always passes validation)
if (result.blockTimestamp <= requestTimestamp) {
revert InvalidResultTimestamp(result.drId, result.blockTimestamp, requestTimestamp);
) public payable override(ResultHandlerBase, IResultHandler) whenNotPaused returns (bytes32) {
RequestDetails memory requestDetails = _storageV1().requestDetails[result.drId];

// Ensures results can't be submitted with timestamps from before the request was made,
// preventing potential replay or front-running attacks
// Note: Validation always passes for non-tracked requests (where requestDetails.timestamp is 0)
if (result.blockTimestamp <= requestDetails.timestamp) {
revert InvalidResultTimestamp(result.drId, result.blockTimestamp, requestDetails.timestamp);
}

bytes32 resultId = super.postResult(result, batchHeight, proof);
// Call parent contract's postResult implementation and retrieve both the result ID
// and the batch sender address for subsequent fee distribution logic
(bytes32 resultId, address batchSender) = super.postResultAndGetBatchSender(result, batchHeight, proof);

// Clean up state
_removeRequest(result.drId);
delete _storageV1().requestTimestamps[result.drId];
delete _storageV1().requestDetails[result.drId];

// Fee distribution: handles three types of fees (request, result, batch)
// and manages refunds back to the requestor when applicable

// Amount to refund to requestor
uint256 refundAmount;

// Request fee distribution:
// - if invalid payback address, send all request fee to requestor
// - if valid payback address, split request fee proportionally based on gas used vs gas limit
if (requestDetails.requestFee > 0) {
address payableAddress = result.paybackAddress.length == 20
? address(bytes20(result.paybackAddress))
: address(0);

if (payableAddress == address(0)) {
refundAmount += requestDetails.requestFee;
} else {
// Split request fee proportionally based on gas used vs gas limit
uint256 submitterFee = (result.gasUsed * requestDetails.requestFee) / requestDetails.gasLimit;
if (submitterFee > 0) {
_transferFee(payableAddress, submitterFee);
emit FeeDistributed(result.drId, payableAddress, submitterFee, ISedaCore.FeeType.REQUEST);
}
refundAmount += requestDetails.requestFee - submitterFee;
}
}

// Result fee distribution:
// - send all result fee to `msg.sender` (result sender/solver)
if (requestDetails.resultFee > 0) {
_transferFee(msg.sender, requestDetails.resultFee);
emit FeeDistributed(result.drId, msg.sender, requestDetails.resultFee, ISedaCore.FeeType.RESULT);
}

// Batch fee distribution:
// - if no batch sender, send all batch fee to requestor
// - if valid batch sender, send batch fee to batch sender
if (requestDetails.batchFee > 0) {
if (batchSender == address(0)) {
// If no batch sender, send all batch fee to requestor
refundAmount += requestDetails.batchFee;
} else {
// Send batch fee to batch sender
_transferFee(batchSender, requestDetails.batchFee);
emit FeeDistributed(result.drId, batchSender, requestDetails.batchFee, ISedaCore.FeeType.BATCH);
}
}

// Aggregate refund to requestor containing:
// - unused request fees (when gas used < gas limit)
// - full request fee (when invalid payback address)
// - batch fee (when no batch sender)
if (refundAmount > 0) {
_transferFee(requestDetails.requestor, refundAmount);
emit FeeDistributed(result.drId, requestDetails.requestor, refundAmount, ISedaCore.FeeType.REFUND);
}

return resultId;
}

/// @inheritdoc ISedaCore
/// @dev Allows the owner to increase fees for a pending request
function increaseFees(
bytes32 requestId,
uint256 additionalRequestFee,
uint256 additionalResultFee,
uint256 additionalBatchFee
) public payable override whenNotPaused {
// Validate ETH payment matches fee sum to prevent over/underpayment
if (msg.value != additionalRequestFee + additionalResultFee + additionalBatchFee) {
revert InvalidFeeAmount();
}

RequestDetails storage details = _storageV1().requestDetails[requestId];
if (details.timestamp == 0) {
revert RequestNotFound(requestId);
}

details.requestFee += additionalRequestFee;
details.resultFee += additionalResultFee;
details.batchFee += additionalBatchFee;

emit FeesIncreased(requestId, additionalRequestFee, additionalResultFee, additionalBatchFee);
}

/// @notice Pauses all contract operations
/// @dev Can only be called by the contract owner
/// @dev When paused, all state-modifying functions will revert
function pause() external onlyOwner {
_pause();
}

/// @notice Unpauses contract operations
/// @dev Can only be called by the contract owner
/// @dev Restores normal contract functionality after being paused
function unpause() external onlyOwner {
_unpause();
}

// ============ Public View Functions ============

/// @notice Retrieves a list of active requests
/// @dev This function is gas-intensive due to iteration over the pendingRequests array.
/// Users should be cautious when using high `limit` values in production environments, as it can result in high gas consumption.
/// @dev This function will revert when the contract is paused
/// @param offset The starting index in the pendingRequests array
/// @param limit The maximum number of requests to return
/// @return An array of SedaDataTypes.Request structs
function getPendingRequests(uint256 offset, uint256 limit) public view returns (SedaDataTypes.Request[] memory) {
function getPendingRequests(
uint256 offset,
uint256 limit
) public view whenNotPaused returns (PendingRequest[] memory) {
uint256 totalRequests = _storageV1().pendingRequests.length();
if (offset >= totalRequests) {
return new SedaDataTypes.Request[](0);
return new PendingRequest[](0);
}

uint256 actualLimit = (offset + limit > totalRequests) ? totalRequests - offset : limit;
SedaDataTypes.Request[] memory queriedPendingRequests = new SedaDataTypes.Request[](actualLimit);
PendingRequest[] memory queriedPendingRequests = new PendingRequest[](actualLimit);
for (uint256 i = 0; i < actualLimit; i++) {
bytes32 requestId = _storageV1().pendingRequests.at(offset + i);
queriedPendingRequests[i] = getRequest(requestId);
RequestDetails memory details = _storageV1().requestDetails[requestId];

queriedPendingRequests[i] = PendingRequest({
request: getRequest(requestId),
requestor: details.requestor,
timestamp: details.timestamp,
requestFee: details.requestFee,
resultFee: details.resultFee,
batchFee: details.batchFee
});
}

return queriedPendingRequests;
Expand Down Expand Up @@ -149,6 +306,15 @@ contract SedaCoreV1 is ISedaCore, RequestHandlerBase, ResultHandlerBase, UUPSUpg
_storageV1().pendingRequests.remove(requestId);
}

/// @dev Helper function to safely transfer fees
/// @param recipient Address to receive the fee
/// @param amount Amount to transfer
function _transferFee(address recipient, uint256 amount) internal {
// Using low-level call instead of transfer()
(bool success, ) = payable(recipient).call{value: amount}("");
if (!success) revert FeeTransferFailed();
}

/// @dev Required override for UUPSUpgradeable. Ensures only the owner can upgrade the implementation.
/// @inheritdoc UUPSUpgradeable
/// @param newImplementation Address of the new implementation contract
Expand Down
2 changes: 1 addition & 1 deletion contracts/core/abstract/RequestHandlerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ abstract contract RequestHandlerBase is IRequestHandler {
/// @inheritdoc IRequestHandler
function postRequest(
SedaDataTypes.RequestInputs calldata inputs
) public virtual override(IRequestHandler) returns (bytes32) {
) public payable virtual override(IRequestHandler) returns (bytes32) {
if (inputs.replicationFactor == 0) {
revert InvalidReplicationFactor();
}
Expand Down
53 changes: 38 additions & 15 deletions contracts/core/abstract/ResultHandlerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ abstract contract ResultHandlerBase is IResultHandler, Initializable {
bytes32[] calldata proof
) external view returns (bytes32) {
bytes32 resultId = SedaDataTypes.deriveResultId(result);
if (!_resultHandlerStorage().sedaProver.verifyResultProof(resultId, batchHeight, proof)) {
(bool isValid, ) = _resultHandlerStorage().sedaProver.verifyResultProof(resultId, batchHeight, proof);
if (!isValid) {
revert InvalidResultProof(resultId);
}

Expand All @@ -75,36 +76,58 @@ abstract contract ResultHandlerBase is IResultHandler, Initializable {
// ============ Public Functions ============

/// @inheritdoc IResultHandler
/// @dev This is left abstract as implementations need to decide how to handle the batch sender
/// @dev See postResultAndGetBatchSender for the core result posting logic
function postResult(
SedaDataTypes.Result calldata result,
uint64 batchHeight,
bytes32[] calldata proof
) public virtual override(IResultHandler) returns (bytes32) {
) public payable virtual override(IResultHandler) returns (bytes32);

/// @inheritdoc IResultHandler
function getResult(bytes32 requestId) public view override(IResultHandler) returns (SedaDataTypes.Result memory) {
SedaDataTypes.Result memory result = _resultHandlerStorage().results[requestId];
if (bytes(result.version).length == 0) {
revert ResultNotFound(requestId);
}
return result;
}

// ============ Internal Functions ============

/// @notice Posts a result and returns both the result ID and batch sender address
/// @dev Similar to postResult but also returns the batch sender address for fee distribution
/// @param result The result data to post
/// @param batchHeight The height of the batch containing the result
/// @param proof The Merkle proof verifying the result
/// @return resultId The unique identifier for the posted result
/// @return batchSender The address of the solver that posted the batch
function postResultAndGetBatchSender(
SedaDataTypes.Result calldata result,
uint64 batchHeight,
bytes32[] calldata proof
) internal returns (bytes32, address) {
bytes32 resultId = SedaDataTypes.deriveResultId(result);
if (_resultHandlerStorage().results[result.drId].drId != bytes32(0)) {
revert ResultAlreadyExists(resultId);
}
if (!_resultHandlerStorage().sedaProver.verifyResultProof(resultId, batchHeight, proof)) {

(bool isValid, address batchSender) = _resultHandlerStorage().sedaProver.verifyResultProof(
resultId,
batchHeight,
proof
);

if (!isValid) {
revert InvalidResultProof(resultId);
}

_resultHandlerStorage().results[result.drId] = result;

emit ResultPosted(resultId);
return resultId;
return (resultId, batchSender);
}

/// @inheritdoc IResultHandler
function getResult(bytes32 requestId) public view override(IResultHandler) returns (SedaDataTypes.Result memory) {
SedaDataTypes.Result memory result = _resultHandlerStorage().results[requestId];
if (bytes(result.version).length == 0) {
revert ResultNotFound(requestId);
}
return _resultHandlerStorage().results[requestId];
}

// ============ Internal Functions ============

/// @notice Returns the storage struct for the contract
/// @dev Uses ERC-7201 storage pattern to access the storage struct at a specific slot
/// @return s The storage struct containing the contract's state variables
Expand Down
Loading
Loading