Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion contracts/config/anvil.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"randaoCommitExpiration": 24,
"minNumRequiredSignatures": 2,
"startBlock": 1,
"messageOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000",
"messageOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd",
"initialValidatorSetId": 0,
"initialValidatorHashes": [
"0xaeb47a269393297f4b0a3c9c9cfd00c7a4195255274cf39d83dabc2fcc9ff3d7",
Expand Down
2 changes: 1 addition & 1 deletion contracts/deployments/anvil-agent-info.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"Agent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}
{"Agent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}
2 changes: 1 addition & 1 deletion contracts/deployments/anvil-rewards-info.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x0000000000000000000000000000000000000000000000000000000000000000"}
{"RewardsAgent": "0xac06641381166cf085281c45292147f833C622d7","AgentOrigin": "0x56490bd3f367447bfaf57bb18e7a45e1b2db7d538fe42098e87d2aa106c6afdd"}
2 changes: 1 addition & 1 deletion contracts/deployments/state-diff.checksum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0d48ae80f212e436db23a1ba4345bc354b10b072
964f27a76c22d4dfdba46a0289eb53b94cbbeb2e
1,003 changes: 506 additions & 497 deletions contracts/deployments/state-diff.json

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion contracts/src/DataHavenServiceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,12 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
/// `contracts/deployments/<chain>.json`.
string private _version;

/// @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
uint256[42] private __GAP;
uint256[41] private __GAP;

// ============ Modifiers ============

Expand Down Expand Up @@ -565,6 +568,15 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
translatedSubmission.operatorRewards = trimmed;
}

address token = address(submission.token);
uint32 startTimestamp = submission.startTimestamp;
uint32 duration = submission.duration;
require(
!rewardsSubmittedForWindow[startTimestamp][duration][token],
RewardsAlreadySubmittedForWindow(startTimestamp, duration, token)
);
rewardsSubmittedForWindow[startTimestamp][duration][token] = true;

submission.token.safeIncreaseAllowance(address(_REWARDS_COORDINATOR), totalAmount);

IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
Expand Down
16 changes: 16 additions & 0 deletions contracts/src/interfaces/IDataHavenServiceManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ interface IDataHavenServiceManagerErrors {

/// @notice Thrown when the caller is not the ProxyAdmin
error NotProxyAdmin();

/// @notice Thrown when rewards for a reward window and token have already been submitted
error RewardsAlreadySubmittedForWindow(uint32 startTimestamp, uint32 duration, address token);
}

/**
Expand Down Expand Up @@ -186,6 +189,18 @@ interface IDataHavenServiceManager is
address solochainAddress
) external view returns (address);

/**
* @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 rewardsSubmittedForWindow(
uint32 startTimestamp,
uint32 duration,
address token
) external view returns (bool);

/**
* @notice Initializes the DataHaven Service Manager
* @param initialOwner Address of the initial owner (AVS owner)
Expand Down Expand Up @@ -339,6 +354,7 @@ interface IDataHavenServiceManager is
* @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 reward window and token
*/
function submitRewards(
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission calldata submission
Expand Down
73 changes: 50 additions & 23 deletions contracts/storage-snapshots/DataHavenServiceManager.storage.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"storage": [
{
"astId": 138,
"astId": 152,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "_initialized",
"offset": 0,
"slot": "0",
"type": "t_uint8"
},
{
"astId": 141,
"astId": 155,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "_initializing",
"offset": 1,
"slot": "0",
"type": "t_bool"
},
{
"astId": 671,
"astId": 769,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "__gap",
"offset": 0,
Expand All @@ -41,76 +41,84 @@
"type": "t_array(t_uint256)49_storage"
},
{
"astId": 23887,
"astId": 103284,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "snowbridgeInitiator",
"offset": 0,
"slot": "101",
"type": "t_address"
},
{
"astId": 23892,
"astId": 103289,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorsAllowlist",
"offset": 0,
"slot": "102",
"type": "t_mapping(t_address,t_bool)"
},
{
"astId": 23896,
"astId": 103293,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "_snowbridgeGateway",
"offset": 0,
"slot": "103",
"type": "t_contract(IGatewayV2)23591"
"type": "t_contract(IGatewayV2)95551"
},
{
"astId": 23901,
"astId": 103298,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorEthAddressToSolochainAddress",
"offset": 0,
"slot": "104",
"type": "t_mapping(t_address,t_address)"
},
{
"astId": 23905,
"astId": 103302,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorSolochainAddressToEthAddress",
"offset": 0,
"slot": "105",
"type": "t_mapping(t_address,t_address)"
},
{
"astId": 23908,
"astId": 103305,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorSetSubmitter",
"offset": 0,
"slot": "106",
"type": "t_address"
},
{
"astId": 23914,
"astId": 103311,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "strategiesAndMultipliers",
"offset": 0,
"slot": "107",
"type": "t_mapping(t_contract(IStrategy)7471,t_uint96)"
"type": "t_mapping(t_contract(IStrategy)26468,t_uint96)"
},
{
"astId": 23917,
"astId": 103314,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "_version",
"offset": 0,
"slot": "108",
"type": "t_string_storage"
},
{
"astId": 23922,
"astId": 103323,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "__GAP",
"label": "rewardsSubmittedForWindow",
"offset": 0,
"slot": "109",
"type": "t_array(t_uint256)42_storage"
"type": "t_mapping(t_uint32,t_mapping(t_uint32,t_mapping(t_address,t_bool)))"
},
{
"astId": 103328,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "__GAP",
"offset": 0,
"slot": "110",
"type": "t_array(t_uint256)41_storage"
}
],
"types": {
Expand All @@ -119,10 +127,10 @@
"label": "address",
"numberOfBytes": "20"
},
"t_array(t_uint256)42_storage": {
"t_array(t_uint256)41_storage": {
"encoding": "inplace",
"label": "uint256[42]",
"numberOfBytes": "1344",
"label": "uint256[41]",
"numberOfBytes": "1312",
"base": "t_uint256"
},
"t_array(t_uint256)49_storage": {
Expand All @@ -142,12 +150,12 @@
"label": "bool",
"numberOfBytes": "1"
},
"t_contract(IGatewayV2)23591": {
"t_contract(IGatewayV2)95551": {
"encoding": "inplace",
"label": "contract IGatewayV2",
"numberOfBytes": "20"
},
"t_contract(IStrategy)7471": {
"t_contract(IStrategy)26468": {
"encoding": "inplace",
"label": "contract IStrategy",
"numberOfBytes": "20"
Expand All @@ -166,13 +174,27 @@
"numberOfBytes": "32",
"value": "t_bool"
},
"t_mapping(t_contract(IStrategy)7471,t_uint96)": {
"t_mapping(t_contract(IStrategy)26468,t_uint96)": {
"encoding": "mapping",
"key": "t_contract(IStrategy)7471",
"key": "t_contract(IStrategy)26468",
"label": "mapping(contract IStrategy => uint96)",
"numberOfBytes": "32",
"value": "t_uint96"
},
"t_mapping(t_uint32,t_mapping(t_address,t_bool))": {
"encoding": "mapping",
"key": "t_uint32",
"label": "mapping(uint32 => mapping(address => bool))",
"numberOfBytes": "32",
"value": "t_mapping(t_address,t_bool)"
},
"t_mapping(t_uint32,t_mapping(t_uint32,t_mapping(t_address,t_bool)))": {
"encoding": "mapping",
"key": "t_uint32",
"label": "mapping(uint32 => mapping(uint32 => mapping(address => bool)))",
"numberOfBytes": "32",
"value": "t_mapping(t_uint32,t_mapping(t_address,t_bool))"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
Expand All @@ -183,6 +205,11 @@
"label": "uint256",
"numberOfBytes": "32"
},
"t_uint32": {
"encoding": "inplace",
"label": "uint32",
"numberOfBytes": "4"
},
"t_uint8": {
"encoding": "inplace",
"label": "uint8",
Expand Down
80 changes: 80 additions & 0 deletions contracts/test/RewardsSubmitter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,67 @@ contract RewardsSubmitterTest is AVSDeployer {
serviceManager.submitRewards(submission2);
}

function test_submitRewards_revertsIfWindowAlreadySubmittedForToken() public {
_registerOperator(operator1, operator1);
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
_buildSubmission(1000e18, operator1);

vm.warp(submission.startTimestamp + submission.duration + 1);

vm.prank(snowbridgeAgent);
serviceManager.submitRewards(submission);

assertTrue(
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(
"RewardsAlreadySubmittedForWindow(uint32,uint32,address)",
submission.startTimestamp,
submission.duration,
address(rewardToken)
)
);
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 {
_registerOperator(operator1, operator1);
// Build submission with custom description
Expand Down Expand Up @@ -256,6 +317,9 @@ contract RewardsSubmitterTest is AVSDeployer {

function test_submitRewards_withDifferentToken() public {
_registerOperator(operator1, operator1);
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory firstSubmission =
_buildSubmission(1000e18, operator1);

// Deploy a different token
ERC20FixedSupply otherToken =
new ERC20FixedSupply("Other", "OTHER", 1000000e18, address(this));
Expand Down Expand Up @@ -285,10 +349,26 @@ contract RewardsSubmitterTest is AVSDeployer {

vm.warp(submission.startTimestamp + submission.duration + 1);

vm.prank(snowbridgeAgent);
serviceManager.submitRewards(firstSubmission);

vm.prank(snowbridgeAgent);
vm.expectEmit(false, false, false, true);
emit IDataHavenServiceManagerEvents.RewardsSubmitted(500e18, 1);
serviceManager.submitRewards(submission);

assertTrue(
serviceManager.rewardsSubmittedForWindow(
submission.startTimestamp, submission.duration, address(rewardToken)
),
"original token should be marked as submitted for the window"
);
assertTrue(
serviceManager.rewardsSubmittedForWindow(
submission.startTimestamp, submission.duration, address(otherToken)
),
"different token should be independently tracked for the same window"
);
}

function test_submitRewards_translatesSolochainOperatorToEthOperator() public {
Expand Down
1 change: 1 addition & 0 deletions operator/pallets/external-validator-slashes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ std = [
"parity-scale-codec/std",
"pallet-external-validators/std",
"scale-info/std",
"serde/std",
"snowbridge-core/std",
"snowbridge-outbound-queue-primitives/std",
"sp-core/std",
Expand Down
Loading
Loading