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

fix: prevent provision validity grief attack (OZ L-03) #1132

Open
wants to merge 1 commit into
base: horizon-oz2/l02-document-temp-blockage
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,10 @@ interface IHorizonStakingMain {
*/
function stakeTo(address serviceProvider, uint256 tokens) external;

// can be called by anyone if the service provider has provisioned stake to this verifier
/**
* @notice Deposit tokens on the service provider stake, on behalf of the service provider,
* provisioned to a specific verifier.
* @dev This function can be called by the service provider, by an authorized operator or by the verifier itself.
* @dev Requirements:
* - The `serviceProvider` must have previously provisioned stake to `verifier`.
* - `_tokens` cannot be zero.
Expand Down
19 changes: 18 additions & 1 deletion packages/horizon/contracts/staking/HorizonStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
_;
}

/**
* @notice Checks that the caller is authorized to operate over a provision or it is the verifier.
* @param serviceProvider The address of the service provider.
* @param verifier The address of the verifier.
*/
modifier onlyAuthorizedOrVerifier(address serviceProvider, address verifier) {
require(
_isAuthorized(serviceProvider, verifier, msg.sender) || msg.sender == verifier,
HorizonStakingNotAuthorized(serviceProvider, verifier, msg.sender)
);
_;
}

/**
* @dev The staking contract is upgradeable however we still use the constructor to set
* a few immutable variables.
Expand Down Expand Up @@ -121,7 +134,11 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain {
}

/// @inheritdoc IHorizonStakingMain
function stakeToProvision(address serviceProvider, address verifier, uint256 tokens) external override notPaused {
function stakeToProvision(
address serviceProvider,
address verifier,
uint256 tokens
) external override notPaused onlyAuthorizedOrVerifier(serviceProvider, verifier) {
_stakeTo(serviceProvider, tokens);
_addToProvision(serviceProvider, verifier, tokens);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,62 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest {
);
}

function _stakeToProvision(address serviceProvider, address verifier, uint256 tokens) internal {
(, address msgSender, ) = vm.readCallers();

// before
uint256 beforeStakingBalance = token.balanceOf(address(staking));
uint256 beforeSenderBalance = token.balanceOf(msgSender);
ServiceProviderInternal memory beforeServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider);
Provision memory beforeProvision = staking.getProvision(serviceProvider, verifier);

// stakeTo
token.approve(address(staking), tokens);
vm.expectEmit();
emit IHorizonStakingBase.HorizonStakeDeposited(serviceProvider, tokens);
vm.expectEmit();
emit IHorizonStakingMain.ProvisionIncreased(serviceProvider, verifier, tokens);
staking.stakeToProvision(serviceProvider, verifier, tokens);

// after
uint256 afterStakingBalance = token.balanceOf(address(staking));
uint256 afterSenderBalance = token.balanceOf(msgSender);
ServiceProviderInternal memory afterServiceProvider = _getStorage_ServiceProviderInternal(serviceProvider);
Provision memory afterProvision = staking.getProvision(serviceProvider, verifier);

// assert - stakeTo
assertEq(afterStakingBalance, beforeStakingBalance + tokens);
assertEq(afterSenderBalance, beforeSenderBalance - tokens);
assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked + tokens);
assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned + tokens);
assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated);
assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked);
assertEq(
afterServiceProvider.__DEPRECATED_tokensLockedUntil,
beforeServiceProvider.__DEPRECATED_tokensLockedUntil
);

// assert - addToProvision
assertEq(afterProvision.tokens, beforeProvision.tokens + tokens);
assertEq(afterProvision.tokensThawing, beforeProvision.tokensThawing);
assertEq(afterProvision.sharesThawing, beforeProvision.sharesThawing);
assertEq(afterProvision.maxVerifierCut, beforeProvision.maxVerifierCut);
assertEq(afterProvision.thawingPeriod, beforeProvision.thawingPeriod);
assertEq(afterProvision.createdAt, beforeProvision.createdAt);
assertEq(afterProvision.lastParametersStagedAt, beforeProvision.lastParametersStagedAt);
assertEq(afterProvision.maxVerifierCutPending, beforeProvision.maxVerifierCutPending);
assertEq(afterProvision.thawingPeriodPending, beforeProvision.thawingPeriodPending);
assertEq(afterProvision.thawingNonce, beforeProvision.thawingNonce);
assertEq(afterServiceProvider.tokensStaked, beforeServiceProvider.tokensStaked + tokens);
assertEq(afterServiceProvider.tokensProvisioned, beforeServiceProvider.tokensProvisioned + tokens);
assertEq(afterServiceProvider.__DEPRECATED_tokensAllocated, beforeServiceProvider.__DEPRECATED_tokensAllocated);
assertEq(afterServiceProvider.__DEPRECATED_tokensLocked, beforeServiceProvider.__DEPRECATED_tokensLocked);
assertEq(
afterServiceProvider.__DEPRECATED_tokensLockedUntil,
beforeServiceProvider.__DEPRECATED_tokensLockedUntil
);
}

function _unstake(uint256 _tokens) internal {
(, address msgSender, ) = vm.readCallers();

Expand Down
122 changes: 109 additions & 13 deletions packages/horizon/test/staking/provision/provision.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,6 @@ contract HorizonStakingProvisionTest is HorizonStakingTest {
staking.provision(users.indexer, subgraphDataServiceAddress, amount / 2, maxVerifierCut, thawingPeriod);
}

function testProvision_OperatorAddTokensToProvision(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useOperator {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
_stakeTo(users.indexer, tokensToAdd);
_addToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}

function testProvision_RevertWhen_OperatorNotAuthorized(
uint256 amount,
uint32 maxVerifierCut,
Expand Down Expand Up @@ -124,4 +111,113 @@ contract HorizonStakingProvisionTest is HorizonStakingTest {
vm.expectRevert(expectedError);
staking.provision(users.indexer, subgraphDataServiceAddress, amount, 0, 0);
}

function testProvision_AddTokensToProvision(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
_stakeTo(users.indexer, tokensToAdd);
_addToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}

function testProvision_OperatorAddTokensToProvision(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useOperator {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
_stakeTo(users.indexer, tokensToAdd);
_addToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}

function testProvision_AddTokensToProvision_RevertWhen_NotAuthorized(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
_stakeTo(users.indexer, tokensToAdd);

// use delegator as a non authorized operator
vm.startPrank(users.delegator);
bytes memory expectedError = abi.encodeWithSignature(
"HorizonStakingNotAuthorized(address,address,address)",
users.indexer,
subgraphDataServiceAddress,
users.delegator
);
vm.expectRevert(expectedError);
staking.addToProvision(users.indexer, subgraphDataServiceAddress, amount);
}

function testProvision_StakeToProvision(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
_stakeToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}

function testProvision_Operator_StakeToProvision(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useOperator {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
_stakeToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}

function testProvision_Verifier_StakeToProvision(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Ensure the verifier has enough tokens to then stake to the provision
token.transfer(subgraphDataServiceAddress, tokensToAdd);

// Add more tokens to the provision
resetPrank(subgraphDataServiceAddress);
_stakeToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}

function testProvision_StakeToProvision_RevertWhen_NotAuthorized(
uint256 amount,
uint32 maxVerifierCut,
uint64 thawingPeriod,
uint256 tokensToAdd
) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) {
tokensToAdd = bound(tokensToAdd, 1, MAX_STAKING_TOKENS);

// Add more tokens to the provision
vm.startPrank(users.delegator);
bytes memory expectedError = abi.encodeWithSignature(
"HorizonStakingNotAuthorized(address,address,address)",
users.indexer,
subgraphDataServiceAddress,
users.delegator
);
vm.expectRevert(expectedError);
staking.stakeToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd);
}
}