mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
fix: key rewards replay guard by window
This commit is contained in:
parent
3cbf7d432c
commit
9cbc1a0825
4 changed files with 86 additions and 66 deletions
|
|
@ -83,8 +83,8 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
/// `contracts/deployments/<chain>.json`.
|
||||
string private _version;
|
||||
|
||||
/// @notice Tracks whether rewards have already been submitted for a source-chain era and token.
|
||||
mapping(uint32 => mapping(address => bool)) public rewardsSubmittedForEra;
|
||||
/// @notice Tracks whether rewards have already been submitted for a reward window and token.
|
||||
mapping(uint32 => mapping(uint32 => mapping(address => bool))) public rewardsSubmittedForWindow;
|
||||
|
||||
/// @notice Storage gap for upgradeability (must be at end of state variables)
|
||||
// solhint-disable-next-line var-name-mixedcase
|
||||
|
|
@ -526,7 +526,6 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
|
||||
/// @inheritdoc IDataHavenServiceManager
|
||||
function submitRewards(
|
||||
uint32 eraIndex,
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission calldata submission
|
||||
) external override onlySnowbridgeInitiator {
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory translatedSubmission =
|
||||
|
|
@ -570,10 +569,13 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
|
|||
}
|
||||
|
||||
address token = address(submission.token);
|
||||
uint32 startTimestamp = submission.startTimestamp;
|
||||
uint32 duration = submission.duration;
|
||||
require(
|
||||
!rewardsSubmittedForEra[eraIndex][token], RewardsAlreadySubmittedForEra(eraIndex, token)
|
||||
!rewardsSubmittedForWindow[startTimestamp][duration][token],
|
||||
RewardsAlreadySubmittedForWindow(startTimestamp, duration, token)
|
||||
);
|
||||
rewardsSubmittedForEra[eraIndex][token] = true;
|
||||
rewardsSubmittedForWindow[startTimestamp][duration][token] = true;
|
||||
|
||||
submission.token.safeIncreaseAllowance(address(_REWARDS_COORDINATOR), totalAmount);
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ interface IDataHavenServiceManagerErrors {
|
|||
/// @notice Thrown when the caller is not the ProxyAdmin
|
||||
error NotProxyAdmin();
|
||||
|
||||
/// @notice Thrown when rewards for an era and token have already been submitted
|
||||
error RewardsAlreadySubmittedForEra(uint32 eraIndex, address token);
|
||||
/// @notice Thrown when rewards for a reward window and token have already been submitted
|
||||
error RewardsAlreadySubmittedForWindow(uint32 startTimestamp, uint32 duration, address token);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -190,12 +190,14 @@ interface IDataHavenServiceManager is
|
|||
) external view returns (address);
|
||||
|
||||
/**
|
||||
* @notice Returns whether rewards have already been submitted for an era and token
|
||||
* @param eraIndex The source-chain era index for the rewards submission
|
||||
* @notice Returns whether rewards have already been submitted for a reward window and token
|
||||
* @param startTimestamp The reward window start timestamp
|
||||
* @param duration The reward window duration in seconds
|
||||
* @param token The reward token address
|
||||
*/
|
||||
function rewardsSubmittedForEra(
|
||||
uint32 eraIndex,
|
||||
function rewardsSubmittedForWindow(
|
||||
uint32 startTimestamp,
|
||||
uint32 duration,
|
||||
address token
|
||||
) external view returns (bool);
|
||||
|
||||
|
|
@ -347,16 +349,14 @@ interface IDataHavenServiceManager is
|
|||
|
||||
/**
|
||||
* @notice Submit rewards to EigenLayer
|
||||
* @param eraIndex The source-chain era index associated with the submission
|
||||
* @param submission The operator-directed rewards submission containing all reward parameters
|
||||
* @dev Only callable by the authorized Snowbridge Agent
|
||||
* @dev Strategies must be sorted in ascending order by address
|
||||
* @dev Operators must be sorted in ascending order by address
|
||||
* @dev Token must be pre-approved or held by the ServiceManager
|
||||
* @dev Only one submission is allowed per era and reward token
|
||||
* @dev Only one submission is allowed per reward window and token
|
||||
*/
|
||||
function submitRewards(
|
||||
uint32 eraIndex,
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission calldata submission
|
||||
) external;
|
||||
|
||||
|
|
|
|||
|
|
@ -100,12 +100,6 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
});
|
||||
}
|
||||
|
||||
function _eraIndexForStart(
|
||||
uint32 startTimestamp
|
||||
) internal pure returns (uint32) {
|
||||
return (startTimestamp - GENESIS_REWARDS_TIMESTAMP) / TEST_CALCULATION_INTERVAL;
|
||||
}
|
||||
|
||||
// ============ Configuration Tests ============
|
||||
|
||||
function test_setSnowbridgeInitiator() public {
|
||||
|
|
@ -134,7 +128,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
|
||||
vm.prank(operator1);
|
||||
vm.expectRevert(abi.encodeWithSignature("OnlyRewardsInitiator()"));
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
// ============ Success Tests ============
|
||||
|
|
@ -151,7 +145,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit(false, false, false, true);
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(rewardAmount, 1);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_multipleOperators() public {
|
||||
|
|
@ -198,7 +192,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit(false, false, false, true);
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(totalAmount, 2);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_multipleSubmissions() public {
|
||||
|
|
@ -211,7 +205,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
submission0.startTimestamp = GENESIS_REWARDS_TIMESTAMP;
|
||||
vm.warp(submission0.startTimestamp + duration + 1);
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission0.startTimestamp), submission0);
|
||||
serviceManager.submitRewards(submission0);
|
||||
|
||||
// Submit for period 1
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission1 =
|
||||
|
|
@ -219,7 +213,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
submission1.startTimestamp = GENESIS_REWARDS_TIMESTAMP + duration;
|
||||
vm.warp(submission1.startTimestamp + duration + 1);
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission1.startTimestamp), submission1);
|
||||
serviceManager.submitRewards(submission1);
|
||||
|
||||
// Submit for period 2
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission2 =
|
||||
|
|
@ -227,32 +221,68 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
submission2.startTimestamp = GENESIS_REWARDS_TIMESTAMP + 2 * duration;
|
||||
vm.warp(submission2.startTimestamp + duration + 1);
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission2.startTimestamp), submission2);
|
||||
serviceManager.submitRewards(submission2);
|
||||
}
|
||||
|
||||
function test_submitRewards_revertsIfEraAlreadySubmittedForToken() public {
|
||||
function test_submitRewards_revertsIfWindowAlreadySubmittedForToken() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
|
||||
_buildSubmission(1000e18, operator1);
|
||||
uint32 eraIndex = _eraIndexForStart(submission.startTimestamp);
|
||||
|
||||
vm.warp(submission.startTimestamp + submission.duration + 1);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(eraIndex, submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
|
||||
assertTrue(
|
||||
serviceManager.rewardsSubmittedForEra(eraIndex, address(rewardToken)),
|
||||
"replay guard should be set for the submitted era and token"
|
||||
serviceManager.rewardsSubmittedForWindow(
|
||||
submission.startTimestamp, submission.duration, address(rewardToken)
|
||||
),
|
||||
"replay guard should be set for the submitted window and token"
|
||||
);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
vm.expectRevert(
|
||||
abi.encodeWithSignature(
|
||||
"RewardsAlreadySubmittedForEra(uint32,address)", eraIndex, address(rewardToken)
|
||||
"RewardsAlreadySubmittedForWindow(uint32,uint32,address)",
|
||||
submission.startTimestamp,
|
||||
submission.duration,
|
||||
address(rewardToken)
|
||||
)
|
||||
);
|
||||
serviceManager.submitRewards(eraIndex, submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_allowsDifferentDurationForSameStartAndToken() public {
|
||||
_registerOperator(operator1, operator1);
|
||||
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory firstSubmission =
|
||||
_buildSubmission(1000e18, operator1);
|
||||
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory secondSubmission =
|
||||
_buildSubmission(500e18, operator1);
|
||||
|
||||
secondSubmission.duration = 2 * TEST_CALCULATION_INTERVAL;
|
||||
|
||||
vm.warp(secondSubmission.startTimestamp + secondSubmission.duration + 1);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(firstSubmission);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(secondSubmission);
|
||||
|
||||
assertTrue(
|
||||
serviceManager.rewardsSubmittedForWindow(
|
||||
firstSubmission.startTimestamp, firstSubmission.duration, address(rewardToken)
|
||||
),
|
||||
"first window should be tracked independently"
|
||||
);
|
||||
assertTrue(
|
||||
serviceManager.rewardsSubmittedForWindow(
|
||||
secondSubmission.startTimestamp, secondSubmission.duration, address(rewardToken)
|
||||
),
|
||||
"second window should be tracked independently"
|
||||
);
|
||||
}
|
||||
|
||||
function test_submitRewards_withCustomDescription() public {
|
||||
|
|
@ -282,7 +312,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
vm.warp(submission.startTimestamp + submission.duration + 1);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_withDifferentToken() public {
|
||||
|
|
@ -318,23 +348,26 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
});
|
||||
|
||||
vm.warp(submission.startTimestamp + submission.duration + 1);
|
||||
uint32 eraIndex = _eraIndexForStart(submission.startTimestamp);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(eraIndex, firstSubmission);
|
||||
serviceManager.submitRewards(firstSubmission);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit(false, false, false, true);
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(500e18, 1);
|
||||
serviceManager.submitRewards(eraIndex, submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
|
||||
assertTrue(
|
||||
serviceManager.rewardsSubmittedForEra(eraIndex, address(rewardToken)),
|
||||
"original token should be marked as submitted for the era"
|
||||
serviceManager.rewardsSubmittedForWindow(
|
||||
submission.startTimestamp, submission.duration, address(rewardToken)
|
||||
),
|
||||
"original token should be marked as submitted for the window"
|
||||
);
|
||||
assertTrue(
|
||||
serviceManager.rewardsSubmittedForEra(eraIndex, address(otherToken)),
|
||||
"different token should be independently tracked for the same era"
|
||||
serviceManager.rewardsSubmittedForWindow(
|
||||
submission.startTimestamp, submission.duration, address(otherToken)
|
||||
),
|
||||
"different token should be independently tracked for the same window"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +427,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
"submission should use solochain operator"
|
||||
);
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_skipsUnknownSolochainAddress() public {
|
||||
|
|
@ -406,7 +439,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit();
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(0, 0);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_afterAllowlistRemovalStillTranslatesDuringDeallocationDelay()
|
||||
|
|
@ -454,7 +487,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
);
|
||||
|
||||
vm.prank(snowbridgeAgent);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_mergesDuplicateTranslatedOperators() public {
|
||||
|
|
@ -537,7 +570,7 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit(false, false, false, true);
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(totalAmount, 2);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
|
||||
function test_submitRewards_sortsTranslatedOperatorsByAddress() public {
|
||||
|
|
@ -617,6 +650,6 @@ contract RewardsSubmitterTest is AVSDeployer {
|
|||
vm.prank(snowbridgeAgent);
|
||||
vm.expectEmit(false, false, false, true);
|
||||
emit IDataHavenServiceManagerEvents.RewardsSubmitted(totalAmount, 2);
|
||||
serviceManager.submitRewards(_eraIndexForStart(submission.startTimestamp), submission);
|
||||
serviceManager.submitRewards(submission);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ sol! {
|
|||
}
|
||||
|
||||
/// The submitRewards function on DataHavenServiceManager.
|
||||
function submitRewards(uint32 eraIndex, OperatorDirectedRewardsSubmission submission);
|
||||
function submitRewards(OperatorDirectedRewardsSubmission submission);
|
||||
}
|
||||
|
||||
/// Configuration for rewards submission.
|
||||
|
|
@ -186,7 +186,6 @@ fn build_rewards_message<C: RewardsSubmissionConfig>(
|
|||
strategies_and_multipliers.sort_by_key(|(strategy, _)| *strategy);
|
||||
|
||||
let calldata = encode_rewards_calldata(
|
||||
rewards_utils.period_index,
|
||||
whave_token_address,
|
||||
&strategies_and_multipliers,
|
||||
&operator_rewards,
|
||||
|
|
@ -267,7 +266,7 @@ pub fn points_to_rewards(
|
|||
/// ABI-encode the submitRewards calldata for DataHavenServiceManager.
|
||||
///
|
||||
/// Uses alloy's type-safe ABI encoding to generate the calldata for
|
||||
/// `submitRewards(uint32,OperatorDirectedRewardsSubmission)`.
|
||||
/// `submitRewards(OperatorDirectedRewardsSubmission)`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `token` - ERC20 reward token address
|
||||
|
|
@ -281,7 +280,6 @@ pub fn points_to_rewards(
|
|||
/// `Ok(Vec<u8>)` with the ABI-encoded calldata, or `Err` if encoding fails
|
||||
/// (e.g., multiplier exceeds uint96 max).
|
||||
pub fn encode_rewards_calldata(
|
||||
era_index: u32,
|
||||
token: H160,
|
||||
strategies_and_multipliers: &[(H160, u128)],
|
||||
operator_rewards: &[(H160, u128)],
|
||||
|
|
@ -330,11 +328,7 @@ pub fn encode_rewards_calldata(
|
|||
description: description.into(),
|
||||
};
|
||||
|
||||
Ok(submitRewardsCall {
|
||||
eraIndex: era_index,
|
||||
submission,
|
||||
}
|
||||
.abi_encode())
|
||||
Ok(submitRewardsCall { submission }.abi_encode())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -625,9 +619,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_encode_submit_rewards_calldata_selector() {
|
||||
// Verify the function selector matches the expected value
|
||||
// cast sig "submitRewards(uint32,((address,uint96)[],address,(address,uint256)[],uint32,uint32,string))" = 0x61115ba2
|
||||
// cast sig "submitRewards(((address,uint96)[],address,(address,uint256)[],uint32,uint32,string))" = 0x83821e8e
|
||||
let calldata = encode_rewards_calldata(
|
||||
7,
|
||||
H160::from_low_u64_be(0x1234),
|
||||
&[],
|
||||
&[(H160::from_low_u64_be(0x5678), 1000)],
|
||||
|
|
@ -638,7 +631,7 @@ mod tests {
|
|||
.expect("Encoding should succeed");
|
||||
|
||||
// Check the function selector (first 4 bytes)
|
||||
assert_eq!(&calldata[0..4], &[0x61, 0x11, 0x5b, 0xa2]);
|
||||
assert_eq!(&calldata[0..4], &[0x83, 0x82, 0x1e, 0x8e]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -647,7 +640,6 @@ mod tests {
|
|||
let invalid_multiplier = MAX_UINT96 + 1;
|
||||
|
||||
let result = encode_rewards_calldata(
|
||||
7,
|
||||
H160::from_low_u64_be(0x1234),
|
||||
&[(H160::from_low_u64_be(0x9999), invalid_multiplier)],
|
||||
&[(H160::from_low_u64_be(0x5678), 1000u128)],
|
||||
|
|
@ -666,13 +658,11 @@ mod tests {
|
|||
let multiplier = (1u128 << 80) + 123u128;
|
||||
let operator = H160::from_low_u64_be(0x5678);
|
||||
let amount = 1000u128;
|
||||
let era_index = 7u32;
|
||||
let start_timestamp = 86_400u32;
|
||||
let duration = 86_400u32;
|
||||
let description = "round trip";
|
||||
|
||||
let calldata = encode_rewards_calldata(
|
||||
era_index,
|
||||
token,
|
||||
&[(strategy, multiplier)],
|
||||
&[(operator, amount)],
|
||||
|
|
@ -683,7 +673,6 @@ mod tests {
|
|||
.expect("Encoding should succeed");
|
||||
|
||||
let decoded = submitRewardsCall::abi_decode(&calldata, true).expect("Decoding should work");
|
||||
assert_eq!(decoded.eraIndex, era_index);
|
||||
let submission = decoded.submission;
|
||||
|
||||
assert_eq!(submission.token, Address::from(token.as_fixed_bytes()));
|
||||
|
|
@ -712,7 +701,6 @@ mod tests {
|
|||
);
|
||||
|
||||
let empty_calldata = encode_rewards_calldata(
|
||||
era_index,
|
||||
token,
|
||||
&[],
|
||||
&[],
|
||||
|
|
@ -723,7 +711,6 @@ mod tests {
|
|||
.expect("Encoding should succeed");
|
||||
let empty_decoded =
|
||||
submitRewardsCall::abi_decode(&empty_calldata, true).expect("Decoding should work");
|
||||
assert_eq!(empty_decoded.eraIndex, era_index);
|
||||
let empty_submission = empty_decoded.submission;
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -771,7 +758,6 @@ mod tests {
|
|||
.0;
|
||||
|
||||
let expected_calldata = encode_rewards_calldata(
|
||||
rewards_utils.period_index,
|
||||
HappyPathConfig::whave_token_address(),
|
||||
&HappyPathConfig::strategies_and_multipliers(),
|
||||
&expected_operator_rewards,
|
||||
|
|
@ -821,7 +807,6 @@ mod tests {
|
|||
.expect("Expected message to be built");
|
||||
|
||||
let expected_calldata = encode_rewards_calldata(
|
||||
rewards_utils.period_index,
|
||||
HappyPathConfig::whave_token_address(),
|
||||
&HappyPathConfig::strategies_and_multipliers(),
|
||||
&operator_rewards,
|
||||
|
|
|
|||
Loading…
Reference in a new issue